feat: optimize file handling and component performance in editor
- Enhanced FileSearchService to implement parallel subdirectory traversal, improving efficiency in file collection. - Refactored ProjectFileService to reuse directory stats, reducing unnecessary file system calls. - Updated multiple components (EditorStatusBar, EditorTabBar, EditorToolbar, GitStatusBadge, QuickOpenDialog, SearchInFilesPanel) to utilize Zustand's shallow comparison for improved state management and performance. - Introduced a module-level cache for QuickOpenDialog to minimize redundant API calls and enhance user experience. - Added cache invalidation logic in editorSlice to ensure fresh data on file system changes.
This commit is contained in:
parent
533f9b9e06
commit
91412dccc1
11 changed files with 120 additions and 60 deletions
|
|
@ -248,10 +248,12 @@ export class FileSearchService {
|
|||
}
|
||||
}
|
||||
|
||||
// Recurse into subdirectories
|
||||
for (const subdir of subdirs) {
|
||||
// Parallel subdirectory traversal (batched)
|
||||
const DIR_CONCURRENCY = 10;
|
||||
for (let i = 0; i < subdirs.length; i += DIR_CONCURRENCY) {
|
||||
if (signal?.aborted || files.length >= MAX_FILES) break;
|
||||
await this.collectFiles(projectRoot, subdir, files, signal);
|
||||
const batch = subdirs.slice(i, i + DIR_CONCURRENCY);
|
||||
await Promise.all(batch.map((dir) => this.collectFiles(projectRoot, dir, files, signal)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -536,8 +536,8 @@ export class ProjectFileService {
|
|||
await fs.rename(normalizedSrc, newPath);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === 'EXDEV') {
|
||||
const stat = await fs.lstat(normalizedSrc);
|
||||
if (stat.isDirectory()) {
|
||||
// Reuse srcStat from step 5 — no need for another fs.lstat
|
||||
if (isDirectory) {
|
||||
await fs.cp(normalizedSrc, newPath, { recursive: true });
|
||||
} else {
|
||||
await fs.copyFile(normalizedSrc, newPath);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
* Status bar: cursor position, language, encoding, indent style, git branch.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||
import { useStore } from '@renderer/store';
|
||||
import { GitBranch } from 'lucide-react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
interface EditorStatusBarProps {
|
||||
line: number;
|
||||
|
|
@ -12,14 +15,18 @@ interface EditorStatusBarProps {
|
|||
language: string;
|
||||
}
|
||||
|
||||
export const EditorStatusBar = ({
|
||||
export const EditorStatusBar = React.memo(function EditorStatusBar({
|
||||
line,
|
||||
col,
|
||||
language,
|
||||
}: EditorStatusBarProps): React.ReactElement => {
|
||||
const gitBranch = useStore((s) => s.editorGitBranch);
|
||||
const isGitRepo = useStore((s) => s.editorIsGitRepo);
|
||||
const watcherEnabled = useStore((s) => s.editorWatcherEnabled);
|
||||
}: EditorStatusBarProps): React.ReactElement {
|
||||
const { gitBranch, isGitRepo, watcherEnabled } = useStore(
|
||||
useShallow((s) => ({
|
||||
gitBranch: s.editorGitBranch,
|
||||
isGitRepo: s.editorIsGitRepo,
|
||||
watcherEnabled: s.editorWatcherEnabled,
|
||||
}))
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-6 shrink-0 items-center justify-between border-t border-border bg-surface-sidebar px-3 text-[11px] text-text-muted">
|
||||
|
|
@ -49,4 +56,4 @@ export const EditorStatusBar = ({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { CSS } from '@dnd-kit/utilities';
|
|||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||
import { useStore } from '@renderer/store';
|
||||
import { X } from 'lucide-react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { EditorTabContextMenu } from './EditorTabContextMenu';
|
||||
import { FileIcon } from './FileIcon';
|
||||
|
|
@ -43,9 +44,13 @@ interface EditorTabBarProps {
|
|||
export const EditorTabBar = ({
|
||||
onRequestCloseTab,
|
||||
}: EditorTabBarProps): React.ReactElement | null => {
|
||||
const tabs = useStore((s) => s.editorOpenTabs);
|
||||
const activeTabId = useStore((s) => s.editorActiveTabId);
|
||||
const modifiedFiles = useStore((s) => s.editorModifiedFiles);
|
||||
const { tabs, activeTabId, modifiedFiles } = useStore(
|
||||
useShallow((s) => ({
|
||||
tabs: s.editorOpenTabs,
|
||||
activeTabId: s.editorActiveTabId,
|
||||
modifiedFiles: s.editorModifiedFiles,
|
||||
}))
|
||||
);
|
||||
const setActiveEditorTab = useStore((s) => s.setActiveEditorTab);
|
||||
const reorderEditorTabs = useStore((s) => s.reorderEditorTabs);
|
||||
const closeOtherEditorTabs = useStore((s) => s.closeOtherEditorTabs);
|
||||
|
|
|
|||
|
|
@ -9,17 +9,22 @@ import { useStore } from '@renderer/store';
|
|||
import { editorBridge } from '@renderer/utils/editorBridge';
|
||||
import { shortcutLabel } from '@renderer/utils/platformKeys';
|
||||
import { Redo2, Save, Undo2, WrapText } from 'lucide-react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
// =============================================================================
|
||||
// Component
|
||||
// =============================================================================
|
||||
|
||||
export const EditorToolbar = (): React.ReactElement | null => {
|
||||
const activeTabId = useStore((s) => s.editorActiveTabId);
|
||||
const modifiedFiles = useStore((s) => s.editorModifiedFiles);
|
||||
const saving = useStore((s) => s.editorSaving);
|
||||
const { activeTabId, modifiedFiles, saving, lineWrap } = useStore(
|
||||
useShallow((s) => ({
|
||||
activeTabId: s.editorActiveTabId,
|
||||
modifiedFiles: s.editorModifiedFiles,
|
||||
saving: s.editorSaving,
|
||||
lineWrap: s.editorLineWrap,
|
||||
}))
|
||||
);
|
||||
const saveFile = useStore((s) => s.saveFile);
|
||||
const lineWrap = useStore((s) => s.editorLineWrap);
|
||||
const toggleLineWrap = useStore((s) => s.toggleLineWrap);
|
||||
|
||||
if (!activeTabId) return null;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
* - R (renamed) — cyan
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { GitFileStatusType } from '@shared/types/editor';
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -33,11 +35,13 @@ interface GitStatusBadgeProps {
|
|||
status: GitFileStatusType;
|
||||
}
|
||||
|
||||
export const GitStatusBadge = ({ status }: GitStatusBadgeProps): React.ReactElement => {
|
||||
export const GitStatusBadge = React.memo(function GitStatusBadge({
|
||||
status,
|
||||
}: GitStatusBadgeProps): React.ReactElement {
|
||||
const config = STATUS_CONFIG[status];
|
||||
return (
|
||||
<span className={`ml-auto shrink-0 text-[10px] leading-none ${config.color}`} title={status}>
|
||||
{config.letter}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useStore } from '@renderer/store';
|
||||
import { getQuickOpenCache, setQuickOpenCache } from '@renderer/utils/quickOpenCache';
|
||||
import { Command } from 'cmdk';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
|
|
@ -47,14 +48,23 @@ export const QuickOpenDialog = ({
|
|||
setAllFiles([]);
|
||||
}
|
||||
|
||||
// Load all project files via backend API
|
||||
// Load all project files via backend API (with module-level cache)
|
||||
useEffect(() => {
|
||||
// Use cache if fresh and for the same project
|
||||
const cached = projectPath ? getQuickOpenCache(projectPath) : null;
|
||||
if (cached) {
|
||||
setAllFiles(cached.files);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const fetchFiles = async (): Promise<void> => {
|
||||
try {
|
||||
const files = await window.electronAPI.editor.listFiles();
|
||||
if (!cancelled) {
|
||||
if (projectPath) setQuickOpenCache(projectPath, files);
|
||||
setAllFiles(files);
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Results are clickable to open the file at the matched line.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { api } from '@renderer/api';
|
||||
import { Button } from '@renderer/components/ui/button';
|
||||
|
|
@ -315,11 +315,11 @@ interface HighlightedLineProps {
|
|||
caseSensitive: boolean;
|
||||
}
|
||||
|
||||
const HighlightedLine = ({
|
||||
const HighlightedLine = React.memo(function HighlightedLine({
|
||||
text,
|
||||
query,
|
||||
caseSensitive,
|
||||
}: HighlightedLineProps): React.ReactElement => {
|
||||
}: HighlightedLineProps): React.ReactElement {
|
||||
if (!query) {
|
||||
return <span className="truncate text-[11px] text-text-secondary">{text}</span>;
|
||||
}
|
||||
|
|
@ -356,4 +356,4 @@ const HighlightedLine = ({
|
|||
}
|
||||
|
||||
return <span className="truncate text-[11px]">{parts}</span>;
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@
|
|||
* CM6-internal shortcuts (Cmd+Z, Cmd+Shift+Z, Cmd+A, Cmd+D) are handled by CodeMirror directly.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { openSearchPanel } from '@codemirror/search';
|
||||
import { useStore } from '@renderer/store';
|
||||
import { editorBridge } from '@renderer/utils/editorBridge';
|
||||
import { physicalKey } from '@renderer/utils/keyboardUtils';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import type { EditorFileTab } from '@shared/types/editor';
|
||||
|
||||
|
|
@ -190,46 +191,39 @@ export function useEditorKeyboardShortcuts({
|
|||
onToggleSidebar,
|
||||
onClose: _onClose,
|
||||
}: UseEditorKeyboardShortcutsOptions): void {
|
||||
const openTabs = useStore((s) => s.editorOpenTabs);
|
||||
const activeTabId = useStore((s) => s.editorActiveTabId);
|
||||
const { openTabs, activeTabId } = useStore(
|
||||
useShallow((s) => ({
|
||||
openTabs: s.editorOpenTabs,
|
||||
activeTabId: s.editorActiveTabId,
|
||||
}))
|
||||
);
|
||||
const setActiveEditorTab = useStore((s) => s.setActiveEditorTab);
|
||||
const saveFile = useStore((s) => s.saveFile);
|
||||
const saveAllFiles = useStore((s) => s.saveAllFiles);
|
||||
const hasUnsavedChanges = useStore((s) => s.hasUnsavedChanges);
|
||||
const toggleLineWrap = useStore((s) => s.toggleLineWrap);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const handler = createEditorKeyHandler({
|
||||
activeTabId,
|
||||
openTabs,
|
||||
setActiveEditorTab,
|
||||
saveFile,
|
||||
saveAllFiles,
|
||||
hasUnsavedChanges,
|
||||
onToggleQuickOpen,
|
||||
onToggleSearchPanel,
|
||||
onToggleGoToLine,
|
||||
onToggleSidebar,
|
||||
onToggleLineWrap: toggleLineWrap,
|
||||
getEditorView: () => editorBridge.getView(),
|
||||
});
|
||||
handler(e);
|
||||
},
|
||||
[
|
||||
activeTabId,
|
||||
openTabs,
|
||||
setActiveEditorTab,
|
||||
saveFile,
|
||||
saveAllFiles,
|
||||
hasUnsavedChanges,
|
||||
onToggleQuickOpen,
|
||||
onToggleSearchPanel,
|
||||
onToggleGoToLine,
|
||||
onToggleSidebar,
|
||||
toggleLineWrap,
|
||||
]
|
||||
);
|
||||
// Store all deps in a ref so the keydown handler has a stable identity
|
||||
const depsRef = useRef<EditorKeyHandlerDeps>(null!);
|
||||
depsRef.current = {
|
||||
activeTabId,
|
||||
openTabs,
|
||||
setActiveEditorTab,
|
||||
saveFile,
|
||||
saveAllFiles,
|
||||
hasUnsavedChanges,
|
||||
onToggleQuickOpen,
|
||||
onToggleSearchPanel,
|
||||
onToggleGoToLine,
|
||||
onToggleSidebar,
|
||||
onToggleLineWrap: toggleLineWrap,
|
||||
getEditorView: () => editorBridge.getView(),
|
||||
};
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
const handler = createEditorKeyHandler(depsRef.current);
|
||||
handler(e);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown, true); // capture phase
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import { api } from '@renderer/api';
|
||||
import { getLanguageFromFileName } from '@renderer/utils/codemirrorLanguages';
|
||||
import { editorBridge } from '@renderer/utils/editorBridge';
|
||||
import { invalidateQuickOpenCache } from '@renderer/utils/quickOpenCache';
|
||||
import { computeDisambiguatedTabs } from '@renderer/utils/tabLabelDisambiguation';
|
||||
import { createLogger } from '@shared/utils/logger';
|
||||
|
||||
|
|
@ -920,6 +921,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
|||
|
||||
// Refresh parent directory in tree for create/delete
|
||||
if (event.type === 'create' || event.type === 'delete') {
|
||||
invalidateQuickOpenCache();
|
||||
const parentDir = event.path.substring(0, event.path.lastIndexOf('/'));
|
||||
if (parentDir && editorProjectPath) {
|
||||
void refreshDirectory(get, set, parentDir);
|
||||
|
|
|
|||
31
src/renderer/utils/quickOpenCache.ts
Normal file
31
src/renderer/utils/quickOpenCache.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Module-level cache for Quick Open file list.
|
||||
* Separated from QuickOpenDialog to avoid circular dependency with editorSlice.
|
||||
*/
|
||||
|
||||
import type { QuickOpenFile } from '@shared/types/editor';
|
||||
|
||||
const FILE_LIST_CACHE_TTL = 10_000; // 10 seconds
|
||||
|
||||
let fileListCache: { files: QuickOpenFile[]; projectPath: string; timestamp: number } | null = null;
|
||||
|
||||
/** Invalidate file list cache (call on file watcher create/delete events) */
|
||||
export function invalidateQuickOpenCache(): void {
|
||||
fileListCache = null;
|
||||
}
|
||||
|
||||
/** Get cached file list if fresh and for the same project */
|
||||
export function getQuickOpenCache(projectPath: string): { files: QuickOpenFile[] } | null {
|
||||
if (
|
||||
fileListCache?.projectPath === projectPath &&
|
||||
Date.now() - fileListCache.timestamp < FILE_LIST_CACHE_TTL
|
||||
) {
|
||||
return { files: fileListCache.files };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Store file list in cache */
|
||||
export function setQuickOpenCache(projectPath: string, files: QuickOpenFile[]): void {
|
||||
fileListCache = { files, projectPath, timestamp: Date.now() };
|
||||
}
|
||||
Loading…
Reference in a new issue