Files
claudia/src/components/FileEditorEnhanced.tsx

1059 lines
35 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useCallback, useRef } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import {
X,
Save,
AlertCircle,
Check,
Loader2,
Maximize2,
Minimize2,
Settings2,
FileCode2,
Sparkles,
Bug,
Zap,
AlertTriangle,
Info
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import Editor, { Monaco } from "@monaco-editor/react";
import { motion, AnimatePresence } from "framer-motion";
import * as monaco from "monaco-editor";
import {
initializeMonaco,
formatDocument
} from "@/lib/monaco-config";
import { setupRealtimeLinting } from "@/lib/eslint-integration";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { fileSyncManager } from "@/lib/fileSyncManager";
interface FileEditorEnhancedProps {
filePath: string;
onClose: () => void;
className?: string;
}
// 根据文件扩展名获取语言
const getLanguageFromPath = (path: string): string => {
const ext = path.split(".").pop()?.toLowerCase();
const languageMap: Record<string, string> = {
// JavaScript/TypeScript
js: "javascript",
jsx: "javascript",
mjs: "javascript",
ts: "typescript",
tsx: "typescript",
// Web
html: "html",
htm: "html",
css: "css",
scss: "scss",
sass: "sass",
less: "less",
// Programming Languages
py: "python",
java: "java",
c: "c",
cpp: "cpp",
cc: "cpp",
cxx: "cpp",
cs: "csharp",
php: "php",
rb: "ruby",
go: "go",
rs: "rust",
kt: "kotlin",
swift: "swift",
m: "objective-c",
scala: "scala",
sh: "shell",
bash: "shell",
zsh: "shell",
fish: "shell",
ps1: "powershell",
r: "r",
lua: "lua",
perl: "perl",
// Data/Config
json: "json",
jsonc: "json",
xml: "xml",
yaml: "yaml",
yml: "yaml",
toml: "toml",
ini: "ini",
cfg: "ini",
conf: "ini",
// Documentation
md: "markdown",
markdown: "markdown",
rst: "restructuredtext",
tex: "latex",
// Database
sql: "sql",
mysql: "mysql",
pgsql: "pgsql",
// Others
dockerfile: "dockerfile",
makefile: "makefile",
cmake: "cmake",
gradle: "gradle",
graphql: "graphql",
proto: "protobuf",
};
return languageMap[ext || ""] || "plaintext";
};
// 诊断信息接口
interface DiagnosticInfo {
line: number;
column: number;
message: string;
severity: 'error' | 'warning' | 'info';
source?: string;
}
export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
filePath,
onClose,
className,
}) => {
const { t } = useTranslation();
const [content, setContent] = useState<string>("");
const [originalContent, setOriginalContent] = useState<string>("");
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [saved, setSaved] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [diagnostics, setDiagnostics] = useState<DiagnosticInfo[]>([]);
const [showDiagnostics, setShowDiagnostics] = useState(true);
const [theme, setTheme] = useState<'vs-dark' | 'vs' | 'hc-black'>('vs-dark');
const [fontSize, setFontSize] = useState(14);
const [minimap, setMinimap] = useState(true);
const [wordWrap, setWordWrap] = useState<'on' | 'off'>('on');
const [autoSave, setAutoSave] = useState(false);
const [lastCheckTime, setLastCheckTime] = useState<number>(Date.now());
const [fileChanged, setFileChanged] = useState(false);
const [cursorPosition, setCursorPosition] = useState({ line: 1, column: 1 });
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const monacoRef = useRef<Monaco | null>(null);
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
const fileCheckIntervalRef = useRef<NodeJS.Timeout | null>(null);
const isApplyingContentRef = useRef(false);
const unlistenRef = useRef<UnlistenFn | null>(null);
const fileName = filePath.split("/").pop() || filePath;
const language = getLanguageFromPath(filePath);
// 加载文件内容
const loadFile = useCallback(async () => {
if (!filePath) return;
console.log('[FileEditor] Loading file:', filePath);
try {
setLoading(true);
setError(null);
const fileContent = await invoke<string>("read_file", {
path: filePath,
});
console.log('[FileEditor] File loaded, content length:', fileContent.length);
setContent(fileContent);
setOriginalContent(fileContent);
setHasChanges(false);
setFileChanged(false);
setLastCheckTime(Date.now());
} catch (err) {
console.error("[FileEditor] Failed to load file:", err);
setError(err instanceof Error ? err.message : "Failed to load file");
} finally {
setLoading(false);
}
}, [filePath]);
// 保存文件
const saveFile = useCallback(async () => {
if (!filePath || !hasChanges) return;
try {
setSaving(true);
setError(null);
await invoke("write_file", {
path: filePath,
content: content,
});
setOriginalContent(content);
setHasChanges(false);
setSaved(true);
setLastCheckTime(Date.now());
setFileChanged(false);
// 显示保存成功提示
setTimeout(() => setSaved(false), 2000);
} catch (err) {
console.error("Failed to save file:", err);
setError(err instanceof Error ? err.message : "Failed to save file");
} finally {
setSaving(false);
}
}, [filePath, content, hasChanges]);
// 自动保存
useEffect(() => {
if (autoSave && hasChanges) {
if (autoSaveTimerRef.current) {
clearTimeout(autoSaveTimerRef.current);
}
autoSaveTimerRef.current = setTimeout(() => {
saveFile();
}, 2000);
}
return () => {
if (autoSaveTimerRef.current) {
clearTimeout(autoSaveTimerRef.current);
}
};
}, [autoSave, hasChanges, saveFile]);
// 处理内容变化
const handleContentChange = useCallback((value: string | undefined) => {
if (isApplyingContentRef.current) {
return;
}
console.log('[FileEditor] Content change detected, new length:', value?.length);
if (value !== undefined) {
setContent(value);
const changed = value !== originalContent;
setHasChanges(changed);
console.log('[FileEditor] Has changes:', changed);
// 触发语法检查
if (editorRef.current && (language === 'typescript' || language === 'javascript')) {
validateCode(value);
}
}
}, [originalContent, language]);
// 确保 Monaco 模型与 React state 同步,避免初始不显示或切换文件后不同步
useEffect(() => {
const ed = editorRef.current;
if (!ed) return;
const model = ed.getModel();
if (!model) return;
const current = model.getValue();
if (content !== undefined && current !== content) {
console.log('[FileEditor] Syncing editor model from state');
isApplyingContentRef.current = true;
model.setValue(content);
isApplyingContentRef.current = false;
}
}, [content, filePath]);
// 验证代码
const validateCode = async (_code: string) => {
if (!monacoRef.current || !editorRef.current) return;
const model = editorRef.current.getModel();
if (!model) return;
// 获取 Monaco 的内置诊断
const markers = monacoRef.current.editor.getModelMarkers({ resource: model.uri });
const newDiagnostics: DiagnosticInfo[] = markers.map(marker => ({
line: marker.startLineNumber,
column: marker.startColumn,
message: marker.message,
severity: marker.severity === 8 ? 'error' :
marker.severity === 4 ? 'warning' : 'info',
source: marker.source || 'typescript'
}));
setDiagnostics(newDiagnostics);
};
// 格式化代码
const handleFormat = () => {
if (editorRef.current) {
formatDocument(editorRef.current);
}
};
// 处理关闭
const handleClose = () => {
if (hasChanges) {
if (confirm(t("app.unsavedChangesConfirm"))) {
onClose();
}
} else {
onClose();
}
};
// 切换全屏
const toggleFullscreen = () => {
setIsFullscreen(!isFullscreen);
};
// Monaco Editor 挂载时的处理
const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) => {
console.log('[FileEditor] Editor mounted successfully');
editorRef.current = editor;
monacoRef.current = monaco;
// 检查编辑器是否可编辑
const model = editor.getModel();
if (model) {
const options = editor.getOptions();
console.log('[FileEditor] Editor readOnly:', options.get(monaco.editor.EditorOption.readOnly));
console.log('[FileEditor] Editor value length:', model.getValue().length);
// 强制设置模型可编辑
model.updateOptions({ tabSize: 2, insertSpaces: true });
}
// 确保编辑器获得焦点
editor.focus();
// 手动处理回车键
editor.addCommand(monaco.KeyCode.Enter, () => {
const position = editor.getPosition();
if (position) {
const model = editor.getModel();
if (model) {
// 获取当前行内容
const lineContent = model.getLineContent(position.lineNumber);
const beforeCursor = lineContent.substring(0, position.column - 1);
// 计算缩进
const indent = beforeCursor.match(/^\s*/)?.[0] || '';
// 插入新行
editor.executeEdits('enter', [{
range: new monaco.Range(
position.lineNumber,
position.column,
position.lineNumber,
position.column
),
text: '\n' + indent,
forceMoveMarkers: true
}]);
// 移动光标到新行
editor.setPosition({
lineNumber: position.lineNumber + 1,
column: indent.length + 1
});
}
}
});
// 监听光标位置变化
editor.onDidChangeCursorPosition((e) => {
setCursorPosition({
line: e.position.lineNumber,
column: e.position.column
});
});
// 使用系统默认的复制快捷键,避免拦截导致权限/聚焦问题
// 使用系统默认的粘贴快捷键Cmd/Ctrl+V避免拦截导致无法粘贴
// 使用系统默认的剪切快捷键,避免拦截导致不一致行为
// 初始化 Monaco 配置
initializeMonaco();
// 设置实时语法检查
setupRealtimeLinting(editor, {
enabled: true,
delay: 500,
showInlineErrors: true,
showErrorsInScrollbar: true,
showErrorsInMinimap: true,
});
// 移除原有的快捷键绑定,使用 Monaco 内置的
// 这些快捷键会自动工作,不需要额外处理
// 监听内容变化事件(作为备用)
editor.onDidChangeModelContent(() => {
const value = editor.getValue();
if (value !== content) {
console.log('[FileEditor] Content changed via editor event');
handleContentChange(value);
}
});
// 监听光标位置变化
editor.onDidChangeCursorPosition(() => {
// 可以在这里更新状态栏信息
});
// 初始验证
if (language === 'typescript' || language === 'javascript') {
setTimeout(() => validateCode(content), 1000);
}
};
// 快捷键处理
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// 如果焦点在编辑器内,除了特定快捷键外不处理其他按键
const activeElement = document.activeElement;
const isInEditor = activeElement?.closest('.monaco-editor');
// Ctrl/Cmd + S 保存
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
saveFile();
return;
}
// Ctrl/Cmd + Shift + F 格式化
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "f") {
e.preventDefault();
handleFormat();
return;
}
// 如果在编辑器内,不处理其他快捷键
if (isInEditor) {
return;
}
// F11 全屏
if (e.key === "F11") {
e.preventDefault();
toggleFullscreen();
}
// Esc 退出全屏
if (e.key === "Escape" && isFullscreen) {
setIsFullscreen(false);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [hasChanges, saveFile, isFullscreen]);
// 使用文件同步管理器监听文件变化
useEffect(() => {
if (!filePath) return;
const listenerId = `file-editor-${filePath}`;
// 注册文件变化监听器
fileSyncManager.registerChangeListener(
listenerId,
filePath,
(changedPath, changeType) => {
// 检查是否是当前文件的变化
if (changedPath === filePath && (changeType === 'modified' || changeType === 'created')) {
// 检查时间间隔,避免自己保存触发的事件
const timeSinceLastSave = Date.now() - lastCheckTime;
if (timeSinceLastSave > 1000) { // 超过1秒可能是外部修改
console.log('[FileEditor] File changed externally via FileSyncManager:', changedPath, changeType);
setFileChanged(true);
// 如果没有未保存的更改,自动重新加载
if (!hasChanges) {
console.log('[FileEditor] Auto-reloading file content');
loadFile();
} else {
// 显示提示
setError("文件已被外部程序修改,点击重新加载按钮查看最新内容");
}
}
}
}
);
const setupFileWatcher = async () => {
if (!filePath) return;
try {
// 监听文件所在目录
const dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
await invoke('watch_directory', {
path: dirPath,
recursive: false
});
// 监听文件变化事件(作为备用)
unlistenRef.current = await listen('file-system-change', (event: any) => {
const { path, change_type } = event.payload;
// 检查是否是当前文件的变化
if (path === filePath && (change_type === 'modified' || change_type === 'created')) {
// 检查时间间隔,避免自己保存触发的事件
const timeSinceLastSave = Date.now() - lastCheckTime;
if (timeSinceLastSave > 1000) { // 超过1秒可能是外部修改
console.log('[FileEditor] File changed externally (fallback):', path, change_type);
setFileChanged(true);
// 如果没有未保存的更改,自动重新加载
if (!hasChanges) {
loadFile();
} else {
// 显示提示
setError("文件已被外部程序修改,点击重新加载按钮查看最新内容");
}
}
}
});
} catch (err) {
console.error('Failed to setup file watcher:', err);
// 如果文件监听失败,回退到轮询模式
fallbackToPolling();
}
};
// 回退到轮询模式
const fallbackToPolling = () => {
const checkFileChanges = async () => {
if (!filePath || !editorRef.current) return;
try {
const fileInfo = await invoke<any>('get_file_info', { path: filePath });
if (fileInfo && fileInfo.modified) {
const fileModifiedTime = new Date(fileInfo.modified).getTime();
if (fileModifiedTime > lastCheckTime && !hasChanges) {
const newContent = await invoke<string>('read_file', { path: filePath });
if (newContent !== originalContent) {
setFileChanged(true);
if (!hasChanges) {
setContent(newContent);
setOriginalContent(newContent);
setFileChanged(false);
setLastCheckTime(Date.now());
}
}
}
}
} catch (err) {
console.debug('File check error:', err);
}
};
// 每3秒检查一次文件变化
fileCheckIntervalRef.current = setInterval(checkFileChanges, 3000);
};
setupFileWatcher();
// 清理函数
return () => {
// 注销文件同步管理器监听器
fileSyncManager.unregisterListener(listenerId, filePath);
// 停止监听
if (filePath) {
const dirPath = filePath.substring(0, filePath.lastIndexOf('/'));
invoke('unwatch_directory', { path: dirPath }).catch(console.error);
}
// 清理事件监听
if (unlistenRef.current) {
unlistenRef.current();
unlistenRef.current = null;
}
// 清理轮询定时器
if (fileCheckIntervalRef.current) {
clearInterval(fileCheckIntervalRef.current);
}
};
}, [filePath, hasChanges, lastCheckTime, originalContent, loadFile]);
// 移除旧的轮询实现
// 重新加载文件
const reloadFile = useCallback(async () => {
if (!filePath) return;
if (hasChanges) {
const shouldReload = window.confirm(
"您有未保存的更改。重新加载将丢失这些更改。是否继续?"
);
if (!shouldReload) return;
}
await loadFile();
}, [filePath, hasChanges, loadFile]);
// 加载文件
useEffect(() => {
if (filePath) {
loadFile();
}
}, [filePath]); // 移除 loadFile 依赖,避免循环
// 计算诊断统计
const diagnosticStats = {
errors: diagnostics.filter(d => d.severity === 'error').length,
warnings: diagnostics.filter(d => d.severity === 'warning').length,
infos: diagnostics.filter(d => d.severity === 'info').length,
};
return (
<div className={cn(
"flex flex-col h-full bg-background",
isFullscreen && "fixed inset-0 z-50",
className
)}>
{/* Header */}
<div className="flex items-center justify-between px-4 py-2 border-b shrink-0">
<div className="flex items-center gap-3">
<FileCode2 className="h-4 w-4 text-muted-foreground" />
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{fileName}</span>
<span className="text-xs text-muted-foreground">({language})</span>
{hasChanges && (
<span className="text-xs px-1.5 py-0.5 bg-yellow-500/10 text-yellow-600 rounded">
{t("app.modified")}
</span>
)}
<AnimatePresence>
{saved && (
<motion.span
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0 }}
className="text-xs px-1.5 py-0.5 bg-green-500/10 text-green-600 rounded flex items-center gap-1"
>
<Check className="h-3 w-3" />
{t("app.saved")}
</motion.span>
)}
</AnimatePresence>
</div>
</div>
<div className="flex items-center gap-2">
{/* 诊断信息 */}
{showDiagnostics && diagnostics.length > 0 && (
<div className="flex items-center gap-2 text-xs">
{diagnosticStats.errors > 0 && (
<span className="flex items-center gap-1 text-red-500">
<Bug className="h-3 w-3" />
{diagnosticStats.errors}
</span>
)}
{diagnosticStats.warnings > 0 && (
<span className="flex items-center gap-1 text-yellow-500">
<AlertTriangle className="h-3 w-3" />
{diagnosticStats.warnings}
</span>
)}
{diagnosticStats.infos > 0 && (
<span className="flex items-center gap-1 text-blue-500">
<Info className="h-3 w-3" />
{diagnosticStats.infos}
</span>
)}
</div>
)}
{/* 自动保存指示器 */}
{autoSave && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 text-xs text-green-500">
<Zap className="h-3 w-3" />
Auto
</div>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* 格式化按钮 */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={handleFormat}
>
<Sparkles className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p> (Alt+Shift+F)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/* 功能信息按钮 */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="sm">
<Info className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-md p-4">
<div className="space-y-3">
<div>
<h4 className="font-semibold mb-1">🎨 </h4>
<p className="text-xs text-muted-foreground">
JavaScript, TypeScript, Python, Java, C++, C#, Go, Rust, Ruby, PHP, Swift, Kotlin, Dart, Scala, R, MATLAB, SQL, HTML, CSS, JSON, XML, YAML, Markdown 40+
</p>
</div>
<div>
<h4 className="font-semibold mb-1">🔧 </h4>
<p className="text-xs text-muted-foreground">
快捷键: Ctrl/Cmd + Shift + F<br/>
支持: JS/TS (Prettier), Python (Black), Java, C/C++, Go (gofmt), Rust (rustfmt), HTML/CSS/JSON
</p>
</div>
<div>
<h4 className="font-semibold mb-1">💡 </h4>
<p className="text-xs text-muted-foreground">
(IntelliSense)<br/>
<br/>
<br/>
<br/>
</p>
</div>
<div>
<h4 className="font-semibold mb-1">🔍 </h4>
<p className="text-xs text-muted-foreground">
(TypeScript/Flow)Linting (ESLint/TSLint)
</p>
</div>
<div>
<h4 className="font-semibold mb-1"> </h4>
<p className="text-xs text-muted-foreground">
<br/>
<br/>
<br/>
<br/>
(Alt + )<br/>
<br/>
Sticky Scroll ()
</p>
</div>
<div>
<h4 className="font-semibold mb-1"> </h4>
<p className="text-xs text-muted-foreground">
Ctrl/Cmd + S: 保存<br/>
Ctrl/Cmd + Shift + F: 格式化<br/>
Ctrl/Cmd + F: 查找<br/>
Ctrl/Cmd + H: 替换<br/>
Ctrl/Cmd + /: 注释<br/>
F11: 全屏<br/>
Alt + Shift + F: 格式化选中代码
</p>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/* 设置菜单 */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<Settings2 className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('vs-dark')}>
主题: VS Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('vs')}>
主题: VS Light
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setFontSize(fontSize + 1)}>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setFontSize(fontSize - 1)}>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setMinimap(!minimap)}>
{minimap ? '隐藏' : '显示'}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setWordWrap(wordWrap === 'on' ? 'off' : 'on')}>
{wordWrap === 'on' ? '关闭' : '开启'}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setShowDiagnostics(!showDiagnostics)}>
{showDiagnostics ? '隐藏' : '显示'}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setAutoSave(!autoSave)}>
{autoSave ? '关闭' : '开启'}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* 文件外部修改提示 */}
{fileChanged && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={reloadFile}
className="flex items-center gap-1 border-yellow-500/50 text-yellow-500 hover:bg-yellow-500/10"
>
<AlertTriangle className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* 保存按钮 */}
{hasChanges && (
<Button
variant="default"
size="sm"
onClick={saveFile}
disabled={saving}
>
{saving ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<Save className="h-4 w-4 mr-1" />
{t("app.save")}
</>
)}
</Button>
)}
{/* 全屏按钮 */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={toggleFullscreen}
className="h-7 w-7"
>
{isFullscreen ? (
<Minimize2 className="h-4 w-4" />
) : (
<Maximize2 className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{isFullscreen ? '退出全屏 (Esc)' : '全屏 (F11)'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/* 关闭按钮 */}
<Button
variant="ghost"
size="icon"
onClick={handleClose}
className="h-7 w-7"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
{/* 诊断面板 */}
{showDiagnostics && diagnostics.length > 0 && (
<div className="max-h-32 overflow-y-auto border-b bg-muted/50 p-2">
<div className="space-y-1">
{diagnostics.map((diagnostic, index) => (
<div
key={index}
className={cn(
"flex items-start gap-2 text-xs p-1 rounded cursor-pointer hover:bg-background",
diagnostic.severity === 'error' && "text-red-500",
diagnostic.severity === 'warning' && "text-yellow-500",
diagnostic.severity === 'info' && "text-blue-500"
)}
onClick={() => {
// 跳转到错误位置
if (editorRef.current) {
editorRef.current.setPosition({
lineNumber: diagnostic.line,
column: diagnostic.column
});
editorRef.current.focus();
}
}}
>
{diagnostic.severity === 'error' && <Bug className="h-3 w-3 mt-0.5" />}
{diagnostic.severity === 'warning' && <AlertTriangle className="h-3 w-3 mt-0.5" />}
{diagnostic.severity === 'info' && <Info className="h-3 w-3 mt-0.5" />}
<span className="flex-1">
[{diagnostic.line}:{diagnostic.column}] {diagnostic.message}
</span>
{diagnostic.source && (
<span className="text-muted-foreground">({diagnostic.source})</span>
)}
</div>
))}
</div>
</div>
)}
{/* Editor */}
{error ? (
<div className="flex flex-col items-center justify-center flex-1 p-8">
<AlertCircle className="h-12 w-12 text-destructive mb-4" />
<p className="text-lg font-medium mb-2">{t("app.error")}</p>
<p className="text-sm text-muted-foreground text-center">{error}</p>
</div>
) : loading ? (
<div className="flex items-center justify-center flex-1">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
) : (
<div className="flex-1 min-h-0">
<Editor
key={filePath}
height="100%"
language={language}
path={filePath}
value={content}
onChange={handleContentChange}
onMount={handleEditorDidMount}
theme={theme}
options={{
readOnly: false, // 确保编辑器可编辑
fontSize: fontSize,
minimap: { enabled: minimap },
lineNumbers: "on", // 显示行号
lineNumbersMinChars: 5, // 行号最小宽度,增加到 5 以确保显示
renderLineHighlight: "all", // 高亮当前行
glyphMargin: true, // 显示字形边距(用于断点等)
wordWrap: wordWrap,
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
insertSpaces: true,
formatOnPaste: true,
formatOnType: true,
suggestOnTriggerCharacters: true,
quickSuggestions: {
other: true,
comments: true,
strings: true
},
parameterHints: { enabled: true },
folding: true,
foldingStrategy: 'indentation',
showFoldingControls: 'always',
bracketPairColorization: { enabled: true },
guides: {
indentation: true,
bracketPairs: true,
},
stickyScroll: { enabled: true },
inlineSuggest: { enabled: true },
lightbulb: { enabled: "onCodeActionsChange" as any },
hover: { enabled: true, delay: 300 },
definitionLinkOpensInPeek: true,
peekWidgetDefaultFocus: 'editor',
// 确保回车键和其他基本编辑功能正常工作
acceptSuggestionOnEnter: "off", // 关闭回车接受建议,避免冲突
autoClosingBrackets: "always",
autoClosingQuotes: "always",
autoIndent: "full",
emptySelectionClipboard: false, // 禁用空选择剪贴板
copyWithSyntaxHighlighting: false, // 禁用语法高亮复制
multiCursorModifier: "alt",
snippetSuggestions: "bottom",
tabCompletion: "on",
wordBasedSuggestions: "currentDocument",
// 确保编辑器可以接收输入
domReadOnly: false,
readOnlyMessage: undefined,
// 添加更多编辑器配置
cursorBlinking: "blink",
cursorSmoothCaretAnimation: "on",
mouseWheelZoom: true,
smoothScrolling: true,
}}
/>
</div>
)}
{/* 状态栏 */}
<div className="flex items-center justify-between px-4 py-1 border-t text-xs text-muted-foreground bg-muted/30">
<div className="flex items-center gap-4">
<span>{language.toUpperCase()}</span>
<span>UTF-8</span>
<span> {cursorPosition.line}, {cursorPosition.column}</span>
<span>LF</span>
</div>
<div className="flex items-center gap-4">
<span>Ln 1, Col 1</span>
<span>Spaces: 2</span>
</div>
</div>
</div>
);
};
export default FileEditorEnhanced;