文件管理器以及文件编辑器

This commit is contained in:
2025-08-09 18:31:17 +08:00
parent 3bf68960a1
commit 272ea62bee
4 changed files with 113 additions and 22 deletions

View File

@@ -283,7 +283,8 @@ pub async fn get_file_tree(project_path: String) -> Result<Vec<FileNode>, String
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())?;
// Return children of root node if it has any

View File

@@ -151,12 +151,13 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
const [isFullscreen, setIsFullscreen] = useState(false);
const [diagnostics, setDiagnostics] = useState<DiagnosticInfo[]>([]);
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 [minimap, setMinimap] = useState(true);
const [wordWrap, setWordWrap] = useState<'on' | 'off'>('on');
const [autoSave, setAutoSave] = 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);
@@ -296,6 +297,14 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
editorRef.current = editor;
monacoRef.current = monaco;
// 监听光标位置变化
editor.onDidChangeCursorPosition((e) => {
setCursorPosition({
line: e.position.lineNumber,
column: e.position.column
});
});
// 初始化 Monaco 配置
initializeMonaco();
@@ -467,6 +476,79 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
</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>
@@ -475,9 +557,6 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('vs-dark-plus')}>
主题: VS Dark+
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('vs-dark')}>
主题: VS Dark
</DropdownMenuItem>
@@ -624,8 +703,10 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
options={{
fontSize: fontSize,
minimap: { enabled: minimap },
lineNumbers: "on",
rulers: [80, 120],
lineNumbers: "on", // 显示行号
lineNumbersMinChars: 5, // 行号最小宽度,增加到 5 以确保显示
renderLineHighlight: "all", // 高亮当前行
glyphMargin: true, // 显示字形边距(用于断点等)
wordWrap: wordWrap,
scrollBeyondLastLine: false,
automaticLayout: true,
@@ -660,6 +741,7 @@ export const FileEditorEnhanced: React.FC<FileEditorEnhancedProps> = ({
<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">

View File

@@ -102,11 +102,14 @@ const getFileIcon = (filename: string) => {
return iconMap[ext || ""] || <File className="h-4 w-4 text-muted-foreground" />;
};
// 组织文件到文件夹结构
// 组织文件到文件夹结构(改进版,支持更深层级)
const organizeFilesByFolder = (files: FileNode[]): 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 || "根目录";
if (node.file_type === "file") {
@@ -116,17 +119,18 @@ const organizeFilesByFolder = (files: FileNode[]): Map<string, FileNode[]> => {
folderMap.get(currentPath)!.push(node);
} else {
const folderPath = parentPath ? `${parentPath}/${node.name}` : node.name;
// 创建文件夹条目,即使它没有直接包含文件
if (!folderMap.has(folderPath)) {
folderMap.set(folderPath, []);
}
if (node.children) {
node.children.forEach(child => processNode(child, folderPath));
if (node.children && node.children.length > 0) {
node.children.forEach(child => processNode(child, folderPath, depth + 1));
}
}
};
files.forEach(node => processNode(node));
files.forEach(node => processNode(node, "", 0));
return folderMap;
};
@@ -318,11 +322,8 @@ export const FileExplorerPanelEnhanced: React.FC<FileExplorerPanelEnhancedProps>
setFileTree(tree);
setFilteredTree(tree);
// 默认展开第一层目录
const firstLevelDirs = tree
.filter(node => node.file_type === 'directory')
.map(node => node.path);
setExpandedNodes(new Set(firstLevelDirs));
// 默认展开任何目录,让用户手动展开或使用展开按钮
setExpandedNodes(new Set());
} catch (err) {
console.error("Failed to load file tree:", err);
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]);
// 渲染文件节点
// 渲染文件节点(优化深层目录显示)
const renderFileNode = (node: FileNode, depth = 0) => {
const isExpanded = expandedNodes.has(node.path);
const isSelected = selectedPath === node.path;
const isDirectory = node.file_type === 'directory';
const hasChildren = node.children && node.children.length > 0;
// 计算显示的路径(处理长路径)
const displayName = node.name.length > 30
@@ -513,10 +515,10 @@ export const FileExplorerPanelEnhanced: React.FC<FileExplorerPanelEnhancedProps>
isSelected && "bg-accent",
"select-none"
)}
style={{ paddingLeft: `${depth * 16 + 8}px` }}
style={{ paddingLeft: `${Math.min(depth * 16 + 8, 200)}px` }} // 限制最大缩进
onClick={() => handleFileClick(node)}
>
{isDirectory && (
{isDirectory && hasChildren && (
<div className="w-4 h-4 flex items-center justify-center">
{isExpanded ? (
<ChevronDown className="h-3 w-3" />
@@ -525,6 +527,9 @@ export const FileExplorerPanelEnhanced: React.FC<FileExplorerPanelEnhancedProps>
)}
</div>
)}
{isDirectory && !hasChildren && (
<div className="w-4 h-4" /> // 空文件夹的占位符
)}
{isDirectory ? (
isExpanded ? (

View File

@@ -449,7 +449,7 @@ export const GitPanelEnhanced: React.FC<GitPanelEnhancedProps> = ({
}, [gitStatus]);
// 渲染文件树节点
// 渲染文件树节点(优化深层目录显示)
const renderFileTreeNode = (node: FileTreeNode, depth = 0, statusType: 'modified' | 'staged' | 'untracked' | 'conflicted') => {
const isExpanded = node.type === 'directory' && expandedNodes.has(node.path);
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",
isSelected && "bg-accent"
)}
style={{ paddingLeft: `${depth * 16 + 8}px` }}
style={{ paddingLeft: `${Math.min(depth * 16 + 8, 200)}px` }} // 限制最大缩进
onClick={() => {
setSelectedPath(node.path);
if (isDirectory) {
@@ -482,6 +482,9 @@ export const GitPanelEnhanced: React.FC<GitPanelEnhancedProps> = ({
)}
</div>
)}
{isDirectory && !hasChildren && (
<div className="w-4 h-4" /> // 空文件夹的占位符
)}
{isDirectory ? (
isExpanded ? (