文件管理器以及文件编辑器
This commit is contained in:
@@ -283,7 +283,8 @@ pub async fn get_file_tree(project_path: String) -> Result<Vec<FileNode>, String
|
|||||||
String::from(".DS_Store"),
|
String::from(".DS_Store"),
|
||||||
];
|
];
|
||||||
|
|
||||||
let root_node = read_directory_recursive(path, 0, 3, &ignore_patterns)
|
// 增加最大深度为 10,以支持更深的文件夹结构
|
||||||
|
let root_node = read_directory_recursive(path, 0, 10, &ignore_patterns)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// Return children of root node if it has any
|
// Return children of root node if it has any
|
||||||
|
@@ -151,12 +151,13 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
|
|||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [diagnostics, setDiagnostics] = useState<DiagnosticInfo[]>([]);
|
const [diagnostics, setDiagnostics] = useState<DiagnosticInfo[]>([]);
|
||||||
const [showDiagnostics, setShowDiagnostics] = useState(true);
|
const [showDiagnostics, setShowDiagnostics] = useState(true);
|
||||||
const [theme, setTheme] = useState<'vs-dark-plus' | 'vs-dark' | 'vs' | 'hc-black'>('vs-dark-plus');
|
const [theme, setTheme] = useState<'vs-dark' | 'vs' | 'hc-black'>('vs-dark');
|
||||||
const [fontSize, setFontSize] = useState(14);
|
const [fontSize, setFontSize] = useState(14);
|
||||||
const [minimap, setMinimap] = useState(true);
|
const [minimap, setMinimap] = useState(true);
|
||||||
const [wordWrap, setWordWrap] = useState<'on' | 'off'>('on');
|
const [wordWrap, setWordWrap] = useState<'on' | 'off'>('on');
|
||||||
const [autoSave, setAutoSave] = useState(false);
|
const [autoSave, setAutoSave] = useState(false);
|
||||||
|
|
||||||
|
const [cursorPosition, setCursorPosition] = useState({ line: 1, column: 1 });
|
||||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
||||||
const monacoRef = useRef<Monaco | null>(null);
|
const monacoRef = useRef<Monaco | null>(null);
|
||||||
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
@@ -296,6 +297,14 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
|
|||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
monacoRef.current = monaco;
|
monacoRef.current = monaco;
|
||||||
|
|
||||||
|
// 监听光标位置变化
|
||||||
|
editor.onDidChangeCursorPosition((e) => {
|
||||||
|
setCursorPosition({
|
||||||
|
line: e.position.lineNumber,
|
||||||
|
column: e.position.column
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化 Monaco 配置
|
// 初始化 Monaco 配置
|
||||||
initializeMonaco();
|
initializeMonaco();
|
||||||
|
|
||||||
@@ -467,6 +476,79 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</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>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -475,9 +557,6 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => setTheme('vs-dark-plus')}>
|
|
||||||
主题: VS Dark+
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setTheme('vs-dark')}>
|
<DropdownMenuItem onClick={() => setTheme('vs-dark')}>
|
||||||
主题: VS Dark
|
主题: VS Dark
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -624,8 +703,10 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
|
|||||||
options={{
|
options={{
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
minimap: { enabled: minimap },
|
minimap: { enabled: minimap },
|
||||||
lineNumbers: "on",
|
lineNumbers: "on", // 显示行号
|
||||||
rulers: [80, 120],
|
lineNumbersMinChars: 5, // 行号最小宽度,增加到 5 以确保显示
|
||||||
|
renderLineHighlight: "all", // 高亮当前行
|
||||||
|
glyphMargin: true, // 显示字形边距(用于断点等)
|
||||||
wordWrap: wordWrap,
|
wordWrap: wordWrap,
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
@@ -660,6 +741,7 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span>{language.toUpperCase()}</span>
|
<span>{language.toUpperCase()}</span>
|
||||||
<span>UTF-8</span>
|
<span>UTF-8</span>
|
||||||
|
<span>行 {cursorPosition.line}, 列 {cursorPosition.column}</span>
|
||||||
<span>LF</span>
|
<span>LF</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
@@ -102,11 +102,14 @@ const getFileIcon = (filename: string) => {
|
|||||||
return iconMap[ext || ""] || <File className="h-4 w-4 text-muted-foreground" />;
|
return iconMap[ext || ""] || <File className="h-4 w-4 text-muted-foreground" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 组织文件到文件夹结构
|
// 组织文件到文件夹结构(改进版,支持更深层级)
|
||||||
const organizeFilesByFolder = (files: FileNode[]): Map<string, FileNode[]> => {
|
const organizeFilesByFolder = (files: FileNode[]): Map<string, FileNode[]> => {
|
||||||
const folderMap = new Map<string, FileNode[]>();
|
const folderMap = new Map<string, FileNode[]>();
|
||||||
|
|
||||||
const processNode = (node: FileNode, parentPath: string = "") => {
|
const processNode = (node: FileNode, parentPath: string = "", depth: number = 0) => {
|
||||||
|
// 限制最大深度为 10 层
|
||||||
|
if (depth > 10) return;
|
||||||
|
|
||||||
const currentPath = parentPath || "根目录";
|
const currentPath = parentPath || "根目录";
|
||||||
|
|
||||||
if (node.file_type === "file") {
|
if (node.file_type === "file") {
|
||||||
@@ -116,17 +119,18 @@ const organizeFilesByFolder = (files: FileNode[]): Map<string, FileNode[]> => {
|
|||||||
folderMap.get(currentPath)!.push(node);
|
folderMap.get(currentPath)!.push(node);
|
||||||
} else {
|
} else {
|
||||||
const folderPath = parentPath ? `${parentPath}/${node.name}` : node.name;
|
const folderPath = parentPath ? `${parentPath}/${node.name}` : node.name;
|
||||||
|
// 创建文件夹条目,即使它没有直接包含文件
|
||||||
if (!folderMap.has(folderPath)) {
|
if (!folderMap.has(folderPath)) {
|
||||||
folderMap.set(folderPath, []);
|
folderMap.set(folderPath, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.children) {
|
if (node.children && node.children.length > 0) {
|
||||||
node.children.forEach(child => processNode(child, folderPath));
|
node.children.forEach(child => processNode(child, folderPath, depth + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
files.forEach(node => processNode(node));
|
files.forEach(node => processNode(node, "", 0));
|
||||||
return folderMap;
|
return folderMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -318,11 +322,8 @@ export const FileExplorerPanelEnhanced: React.FC<FileExplorerPanelEnhancedProps>
|
|||||||
setFileTree(tree);
|
setFileTree(tree);
|
||||||
setFilteredTree(tree);
|
setFilteredTree(tree);
|
||||||
|
|
||||||
// 默认展开第一层目录
|
// 不默认展开任何目录,让用户手动展开或使用展开按钮
|
||||||
const firstLevelDirs = tree
|
setExpandedNodes(new Set());
|
||||||
.filter(node => node.file_type === 'directory')
|
|
||||||
.map(node => node.path);
|
|
||||||
setExpandedNodes(new Set(firstLevelDirs));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load file tree:", err);
|
console.error("Failed to load file tree:", err);
|
||||||
setError(err instanceof Error ? err.message : "Failed to load file tree");
|
setError(err instanceof Error ? err.message : "Failed to load file tree");
|
||||||
@@ -492,11 +493,12 @@ export const FileExplorerPanelEnhanced: React.FC<FileExplorerPanelEnhancedProps>
|
|||||||
}
|
}
|
||||||
}, [onFileSelect, toggleExpand, lastClickTime, lastClickPath, handleOpenFile]);
|
}, [onFileSelect, toggleExpand, lastClickTime, lastClickPath, handleOpenFile]);
|
||||||
|
|
||||||
// 渲染文件节点
|
// 渲染文件节点(优化深层目录显示)
|
||||||
const renderFileNode = (node: FileNode, depth = 0) => {
|
const renderFileNode = (node: FileNode, depth = 0) => {
|
||||||
const isExpanded = expandedNodes.has(node.path);
|
const isExpanded = expandedNodes.has(node.path);
|
||||||
const isSelected = selectedPath === node.path;
|
const isSelected = selectedPath === node.path;
|
||||||
const isDirectory = node.file_type === 'directory';
|
const isDirectory = node.file_type === 'directory';
|
||||||
|
const hasChildren = node.children && node.children.length > 0;
|
||||||
|
|
||||||
// 计算显示的路径(处理长路径)
|
// 计算显示的路径(处理长路径)
|
||||||
const displayName = node.name.length > 30
|
const displayName = node.name.length > 30
|
||||||
@@ -513,10 +515,10 @@ export const FileExplorerPanelEnhanced: React.FC<FileExplorerPanelEnhancedProps>
|
|||||||
isSelected && "bg-accent",
|
isSelected && "bg-accent",
|
||||||
"select-none"
|
"select-none"
|
||||||
)}
|
)}
|
||||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
style={{ paddingLeft: `${Math.min(depth * 16 + 8, 200)}px` }} // 限制最大缩进
|
||||||
onClick={() => handleFileClick(node)}
|
onClick={() => handleFileClick(node)}
|
||||||
>
|
>
|
||||||
{isDirectory && (
|
{isDirectory && hasChildren && (
|
||||||
<div className="w-4 h-4 flex items-center justify-center">
|
<div className="w-4 h-4 flex items-center justify-center">
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<ChevronDown className="h-3 w-3" />
|
<ChevronDown className="h-3 w-3" />
|
||||||
@@ -525,6 +527,9 @@ export const FileExplorerPanelEnhanced: React.FC<FileExplorerPanelEnhancedProps>
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isDirectory && !hasChildren && (
|
||||||
|
<div className="w-4 h-4" /> // 空文件夹的占位符
|
||||||
|
)}
|
||||||
|
|
||||||
{isDirectory ? (
|
{isDirectory ? (
|
||||||
isExpanded ? (
|
isExpanded ? (
|
||||||
|
@@ -449,7 +449,7 @@ export const GitPanelEnhanced: React.FC<GitPanelEnhancedProps> = ({
|
|||||||
}, [gitStatus]);
|
}, [gitStatus]);
|
||||||
|
|
||||||
|
|
||||||
// 渲染文件树节点
|
// 渲染文件树节点(优化深层目录显示)
|
||||||
const renderFileTreeNode = (node: FileTreeNode, depth = 0, statusType: 'modified' | 'staged' | 'untracked' | 'conflicted') => {
|
const renderFileTreeNode = (node: FileTreeNode, depth = 0, statusType: 'modified' | 'staged' | 'untracked' | 'conflicted') => {
|
||||||
const isExpanded = node.type === 'directory' && expandedNodes.has(node.path);
|
const isExpanded = node.type === 'directory' && expandedNodes.has(node.path);
|
||||||
const isDirectory = node.type === 'directory';
|
const isDirectory = node.type === 'directory';
|
||||||
@@ -463,7 +463,7 @@ export const GitPanelEnhanced: React.FC<GitPanelEnhancedProps> = ({
|
|||||||
"flex items-center gap-1 px-2 py-1 hover:bg-accent rounded-sm cursor-pointer group",
|
"flex items-center gap-1 px-2 py-1 hover:bg-accent rounded-sm cursor-pointer group",
|
||||||
isSelected && "bg-accent"
|
isSelected && "bg-accent"
|
||||||
)}
|
)}
|
||||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
style={{ paddingLeft: `${Math.min(depth * 16 + 8, 200)}px` }} // 限制最大缩进
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedPath(node.path);
|
setSelectedPath(node.path);
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
@@ -482,6 +482,9 @@ export const GitPanelEnhanced: React.FC<GitPanelEnhancedProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isDirectory && !hasChildren && (
|
||||||
|
<div className="w-4 h-4" /> // 空文件夹的占位符
|
||||||
|
)}
|
||||||
|
|
||||||
{isDirectory ? (
|
{isDirectory ? (
|
||||||
isExpanded ? (
|
isExpanded ? (
|
||||||
|
Reference in New Issue
Block a user