/** * IDE Component * * 整合文件浏览器和代码编辑器的 IDE 组件 */ import { useState, useCallback, useEffect, useRef, useImperativeHandle, forwardRef } from 'react'; import { toast } from 'sonner'; import { cn } from '../utils/cn.js'; import { readFile, getWorkingDirectory } from '../api/client.js'; import { FileExplorer } from './FileExplorer.js'; import { CodeEditor, getLanguageFromFilename, type EditorTab } from './CodeEditor.js'; import { DiffEditor } from './DiffEditor.js'; import type { ActiveFileInfo, FileDiffInfo } from '../api/types.js'; // localStorage 键名 const STORAGE_KEY_TABS = 'ai-assistant-editor-tabs'; const STORAGE_KEY_ACTIVE_TAB = 'ai-assistant-editor-active-tab'; // 保存的标签页格式(只保存路径,不保存内容) interface SavedTab { path: string; name: string; } interface IDEProps { className?: string; /** 文件浏览器宽度 */ sidebarWidth?: number; /** 当前活动文件变化回调 */ onActiveFileChange?: (file: ActiveFileInfo | null) => void; /** 要显示的 Diff 信息(外部传入) */ pendingDiff?: FileDiffInfo | null; /** Diff 关闭回调 */ onDiffClose?: () => void; } /** IDE 组件暴露的方法 */ export interface IDEHandle { /** 显示文件 diff */ showDiff: (diff: FileDiffInfo) => void; /** 关闭 diff 视图 */ closeDiff: () => void; } export const IDE = forwardRef(function IDE( { className, sidebarWidth = 256, onActiveFileChange, pendingDiff, onDiffClose }, ref ) { const [tabs, setTabs] = useState([]); const [activeTabId, setActiveTabId] = useState(null); const [workingDirectory, setWorkingDirectory] = useState(''); const [currentDiff, setCurrentDiff] = useState(null); const isRestoringTabs = useRef(false); const hasRestoredTabs = useRef(false); // 获取工作目录 useEffect(() => { getWorkingDirectory() .then((result) => { if (result?.data?.workingDirectory) { setWorkingDirectory(result.data.workingDirectory); } }) .catch(() => { // 忽略错误 }); }, []); // 从 localStorage 恢复标签页 useEffect(() => { if (hasRestoredTabs.current) return; hasRestoredTabs.current = true; const restoreTabs = async () => { try { const savedTabsJson = localStorage.getItem(STORAGE_KEY_TABS); const savedActivePath = localStorage.getItem(STORAGE_KEY_ACTIVE_TAB); if (!savedTabsJson) return; const savedTabs: SavedTab[] = JSON.parse(savedTabsJson); if (savedTabs.length === 0) return; isRestoringTabs.current = true; // 逐个恢复标签页 const restoredTabs: EditorTab[] = []; for (const savedTab of savedTabs) { try { const result = await readFile(savedTab.path); if (result.success && result.data.encoding === 'utf-8') { restoredTabs.push({ id: `tab-${Date.now()}-${restoredTabs.length}`, path: savedTab.path, name: savedTab.name, content: result.data.content, originalContent: result.data.content, language: getLanguageFromFilename(savedTab.name), }); } } catch { // 文件可能已删除,跳过 } } if (restoredTabs.length > 0) { setTabs(restoredTabs); // 恢复活动标签 if (savedActivePath) { const activeTab = restoredTabs.find((tab) => tab.path === savedActivePath); if (activeTab) { setActiveTabId(activeTab.id); } else { setActiveTabId(restoredTabs[0].id); } } else { setActiveTabId(restoredTabs[0].id); } } isRestoringTabs.current = false; } catch { isRestoringTabs.current = false; } }; restoreTabs(); }, []); // 保存标签页到 localStorage useEffect(() => { // 恢复过程中不保存 if (isRestoringTabs.current) return; const tabsToSave: SavedTab[] = tabs.map((tab) => ({ path: tab.path, name: tab.name, })); localStorage.setItem(STORAGE_KEY_TABS, JSON.stringify(tabsToSave)); }, [tabs]); // 保存活动标签到 localStorage useEffect(() => { // 恢复过程中不保存 if (isRestoringTabs.current) return; const activeTab = tabs.find((tab) => tab.id === activeTabId); if (activeTab) { localStorage.setItem(STORAGE_KEY_ACTIVE_TAB, activeTab.path); } else { localStorage.removeItem(STORAGE_KEY_ACTIVE_TAB); } }, [activeTabId, tabs]); // 通知父组件活动文件变化 useEffect(() => { if (!onActiveFileChange) return; const activeTab = tabs.find((tab) => tab.id === activeTabId); if (activeTab) { onActiveFileChange({ path: activeTab.path, name: activeTab.name, content: activeTab.content, language: activeTab.language, }); } else { onActiveFileChange(null); } }, [activeTabId, tabs, onActiveFileChange]); // 打开文件 const handleFileSelect = useCallback(async (path: string, name: string) => { // 检查是否已打开 const existingTab = tabs.find((tab) => tab.path === path); if (existingTab) { setActiveTabId(existingTab.id); return; } // 读取文件内容 try { const result = await readFile(path); if (result.success && result.data.encoding === 'utf-8') { const newTab: EditorTab = { id: `tab-${Date.now()}`, path, name, content: result.data.content, originalContent: result.data.content, language: getLanguageFromFilename(name), }; setTabs((prev) => [...prev, newTab]); setActiveTabId(newTab.id); } else if (result.data.encoding === 'base64') { toast.error('Cannot edit binary files'); } else { toast.error('Failed to open file'); } } catch (error) { console.error('Failed to open file:', error); toast.error('Failed to open file'); } }, [tabs]); // 切换标签 const handleTabChange = useCallback((tabId: string) => { setActiveTabId(tabId); }, []); // 关闭标签 const handleTabClose = useCallback((tabId: string) => { const tabIndex = tabs.findIndex((tab) => tab.id === tabId); const tab = tabs[tabIndex]; // 检查是否有未保存的更改 if (tab && tab.content !== tab.originalContent) { const confirmed = window.confirm(`${tab.name} has unsaved changes. Close anyway?`); if (!confirmed) return; } setTabs((prev) => prev.filter((tab) => tab.id !== tabId)); // 如果关闭的是当前标签,切换到相邻标签 if (activeTabId === tabId) { if (tabs.length > 1) { const newIndex = tabIndex === 0 ? 1 : tabIndex - 1; setActiveTabId(tabs[newIndex].id); } else { setActiveTabId(null); } } }, [tabs, activeTabId]); // 内容更改 const handleContentChange = useCallback((tabId: string, content: string) => { setTabs((prev) => prev.map((tab) => tab.id === tabId ? { ...tab, content } : tab ) ); }, []); // 保存后更新原始内容 const handleSave = useCallback((tabId: string, _path: string, content: string) => { setTabs((prev) => prev.map((tab) => tab.id === tabId ? { ...tab, originalContent: content } : tab ) ); }, []); // 显示 diff 视图 const showDiff = useCallback((diff: FileDiffInfo) => { setCurrentDiff(diff); // 如果文件已在编辑器中打开,更新其内容 setTabs((prev) => prev.map((tab) => { if (tab.path === diff.path) { return { ...tab, content: diff.newContent, originalContent: diff.newContent, }; } return tab; }) ); }, []); // 关闭 diff 视图 const closeDiff = useCallback(() => { setCurrentDiff(null); onDiffClose?.(); }, [onDiffClose]); // 暴露方法给父组件 useImperativeHandle(ref, () => ({ showDiff, closeDiff, }), [showDiff, closeDiff]); // 处理外部传入的 pendingDiff useEffect(() => { if (pendingDiff) { showDiff(pendingDiff); } }, [pendingDiff, showDiff]); // 判断是否显示 diff 视图 const showDiffView = currentDiff !== null; return (
{/* 文件浏览器 */}
{/* 代码编辑器 / Diff 视图 */}
{showDiffView ? ( ) : ( )}
); });