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 = { // 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 = ({ filePath, onClose, className, }) => { const { t } = useTranslation(); const [content, setContent] = useState(""); const [originalContent, setOriginalContent] = useState(""); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [saved, setSaved] = useState(false); const [hasChanges, setHasChanges] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const [diagnostics, setDiagnostics] = useState([]); 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(Date.now()); const [fileChanged, setFileChanged] = useState(false); const [cursorPosition, setCursorPosition] = useState({ line: 1, column: 1 }); const editorRef = useRef(null); const monacoRef = useRef(null); const autoSaveTimerRef = useRef(null); const fileCheckIntervalRef = useRef(null); const isApplyingContentRef = useRef(false); const unlistenRef = useRef(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("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('get_file_info', { path: filePath }); if (fileInfo && fileInfo.modified) { const fileModifiedTime = new Date(fileInfo.modified).getTime(); if (fileModifiedTime > lastCheckTime && !hasChanges) { const newContent = await invoke('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 (
{/* Header */}
{fileName} ({language}) {hasChanges && ( {t("app.modified")} )} {saved && ( {t("app.saved")} )}
{/* 诊断信息 */} {showDiagnostics && diagnostics.length > 0 && (
{diagnosticStats.errors > 0 && ( {diagnosticStats.errors} )} {diagnosticStats.warnings > 0 && ( {diagnosticStats.warnings} )} {diagnosticStats.infos > 0 && ( {diagnosticStats.infos} )}
)} {/* 自动保存指示器 */} {autoSave && (
Auto

自动保存已启用

)} {/* 格式化按钮 */}

格式化代码 (Alt+Shift+F)

{/* 功能信息按钮 */}

🎨 语法高亮支持

JavaScript, TypeScript, Python, Java, C++, C#, Go, Rust, Ruby, PHP, Swift, Kotlin, Dart, Scala, R, MATLAB, SQL, HTML, CSS, JSON, XML, YAML, Markdown 等 40+ 语言

🔧 代码格式化

快捷键: Ctrl/Cmd + Shift + F
支持: JS/TS (Prettier), Python (Black), Java, C/C++, Go (gofmt), Rust (rustfmt), HTML/CSS/JSON

💡 智能提示

• 代码补全 (IntelliSense)
• 参数提示
• 悬浮文档
• 快速修复建议
• 重构建议

🔍 错误检查

实时语法检查、类型检查 (TypeScript/Flow)、Linting (ESLint/TSLint)

⚙️ 编辑器功能

• 行号显示
• 代码折叠
• 括号匹配高亮
• 多光标编辑
• 列选择 (Alt + 鼠标)
• 小地图导航
• Sticky Scroll (固定显示上下文)

⌨️ 快捷键

Ctrl/Cmd + S: 保存
Ctrl/Cmd + Shift + F: 格式化
Ctrl/Cmd + F: 查找
Ctrl/Cmd + H: 替换
Ctrl/Cmd + /: 注释
F11: 全屏
Alt + Shift + F: 格式化选中代码

{/* 设置菜单 */} setTheme('vs-dark')}> 主题: VS Dark setTheme('vs')}> 主题: VS Light setFontSize(fontSize + 1)}> 字体放大 setFontSize(fontSize - 1)}> 字体缩小 setMinimap(!minimap)}> {minimap ? '隐藏' : '显示'}小地图 setWordWrap(wordWrap === 'on' ? 'off' : 'on')}> {wordWrap === 'on' ? '关闭' : '开启'}自动换行 setShowDiagnostics(!showDiagnostics)}> {showDiagnostics ? '隐藏' : '显示'}诊断信息 setAutoSave(!autoSave)}> {autoSave ? '关闭' : '开启'}自动保存 {/* 文件外部修改提示 */} {fileChanged && (

文件已被外部程序修改,点击重新加载最新内容

)} {/* 保存按钮 */} {hasChanges && ( )} {/* 全屏按钮 */}

{isFullscreen ? '退出全屏 (Esc)' : '全屏 (F11)'}

{/* 关闭按钮 */}
{/* 诊断面板 */} {showDiagnostics && diagnostics.length > 0 && (
{diagnostics.map((diagnostic, index) => (
{ // 跳转到错误位置 if (editorRef.current) { editorRef.current.setPosition({ lineNumber: diagnostic.line, column: diagnostic.column }); editorRef.current.focus(); } }} > {diagnostic.severity === 'error' && } {diagnostic.severity === 'warning' && } {diagnostic.severity === 'info' && } [{diagnostic.line}:{diagnostic.column}] {diagnostic.message} {diagnostic.source && ( ({diagnostic.source}) )}
))}
)} {/* Editor */} {error ? (

{t("app.error")}

{error}

) : loading ? (
) : (
)} {/* 状态栏 */}
{language.toUpperCase()} UTF-8 行 {cursorPosition.line}, 列 {cursorPosition.column} LF
Ln 1, Col 1 Spaces: 2
); }; export default FileEditorEnhanced;