From 402274a35c8e41b402f6b0bba8127d77d7301fd2 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:26:26 -0700 Subject: [PATCH] perf(renderer): scope editor Git subscriptions by worktree (#8107) --- .../src/components/editor/EditorPanel.tsx | 21 ++++-- .../editor-panel-git-entry-selector.test.ts | 74 +++++++++++++++++++ .../editor/editor-panel-git-entry-selector.ts | 17 +++++ .../editor/editor-panel-render-model.test.ts | 7 +- .../editor/editor-panel-render-model.ts | 12 +-- .../useEditorPanelContentState.test.tsx | 2 +- .../editor/useEditorPanelContentState.ts | 12 ++- 7 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 src/renderer/src/components/editor/editor-panel-git-entry-selector.test.ts create mode 100644 src/renderer/src/components/editor/editor-panel-git-entry-selector.ts diff --git a/src/renderer/src/components/editor/EditorPanel.tsx b/src/renderer/src/components/editor/EditorPanel.tsx index 03fde8425c5..dd89fd7ab39 100644 --- a/src/renderer/src/components/editor/EditorPanel.tsx +++ b/src/renderer/src/components/editor/EditorPanel.tsx @@ -18,6 +18,10 @@ import { useEditorPanelContentState } from './useEditorPanelContentState' import { useMarkdownPreviewShortcut } from './useMarkdownPreviewShortcut' import { useUntitledFileRename } from './useUntitledFileRename' import { extractFrontMatter } from './markdown-frontmatter' +import { + selectEditorPanelGitBranchEntries, + selectEditorPanelGitStatusEntries +} from './editor-panel-git-entry-selector' function EditorPanelInner({ activeFileId: activeFileIdProp, @@ -33,10 +37,17 @@ function EditorPanelInner({ const activeFileId = activeFileIdProp ?? globalActiveFileId const activeViewStateId = activeViewStateIdProp ?? activeFileId const activeFile = openFiles.find((f) => f.id === activeFileId) ?? null + const activeWorktreeId = activeFile?.worktreeId const markFileDirty = useAppStore((s) => s.markFileDirty) const pendingEditorReveal = useAppStore((s) => s.pendingEditorReveal) - const gitStatusByWorktree = useAppStore((s) => s.gitStatusByWorktree) - const gitBranchChangesByWorktree = useAppStore((s) => s.gitBranchChangesByWorktree) + // Why: background Git refreshes for other worktrees must not wake every + // mounted Monaco/rich editor pane. + const gitStatusEntries = useAppStore((s) => + selectEditorPanelGitStatusEntries(s, activeWorktreeId) + ) + const gitBranchEntries = useAppStore((s) => + selectEditorPanelGitBranchEntries(s, activeWorktreeId) + ) const markdownViewMode = useAppStore((s) => s.markdownViewMode) const setMarkdownViewMode = useAppStore((s) => s.setMarkdownViewMode) const editorViewMode = useAppStore((s) => s.editorViewMode) @@ -96,7 +107,7 @@ function EditorPanelInner({ activeFile, isChangesMode: requestedChangesMode, openFiles, - gitStatusByWorktree, + gitStatusEntries, editorViewMode }) const isChangesMode = @@ -225,8 +236,8 @@ function EditorPanelInner({ activeFile, fileContents, editorDrafts, - gitStatusByWorktree, - gitBranchChangesByWorktree, + gitStatusEntries, + gitBranchEntries, markdownViewMode, isChangesMode }) diff --git a/src/renderer/src/components/editor/editor-panel-git-entry-selector.test.ts b/src/renderer/src/components/editor/editor-panel-git-entry-selector.test.ts new file mode 100644 index 00000000000..524776853d5 --- /dev/null +++ b/src/renderer/src/components/editor/editor-panel-git-entry-selector.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest' +import type { GitBranchChangeEntry, GitStatusEntry } from '../../../../shared/types' +import { + selectEditorPanelGitBranchEntries, + selectEditorPanelGitStatusEntries +} from './editor-panel-git-entry-selector' + +describe('editor panel Git entry selectors', () => { + it('ignore background worktree writes across mounted editor panels', () => { + const panelCount = 200 + const worktreeId = 'worktree-active' + const statusEntries = [{ path: 'src/index.ts' }] as GitStatusEntry[] + const branchEntries = [{ path: 'src/index.ts' }] as GitBranchChangeEntry[] + let gitStatusByWorktree = { [worktreeId]: statusEntries } + let gitBranchChangesByWorktree = { [worktreeId]: branchEntries } + let wholeMapInvalidations = 0 + let scopedEntryInvalidations = 0 + + for (let write = 0; write < 200; write += 1) { + const previousStatusMap = gitStatusByWorktree + gitStatusByWorktree = { + ...gitStatusByWorktree, + [`background-${write}`]: [{ path: `generated-${write}.ts` } as GitStatusEntry] + } + const previousBranchMap = gitBranchChangesByWorktree + gitBranchChangesByWorktree = { + ...gitBranchChangesByWorktree, + [`background-${write}`]: [{ path: `generated-${write}.ts` } as GitBranchChangeEntry] + } + + for (let panel = 0; panel < panelCount; panel += 1) { + wholeMapInvalidations += Number(previousStatusMap !== gitStatusByWorktree) + wholeMapInvalidations += Number(previousBranchMap !== gitBranchChangesByWorktree) + scopedEntryInvalidations += Number( + selectEditorPanelGitStatusEntries({ gitStatusByWorktree }, worktreeId) !== statusEntries + ) + scopedEntryInvalidations += Number( + selectEditorPanelGitBranchEntries({ gitBranchChangesByWorktree }, worktreeId) !== + branchEntries + ) + } + } + + expect(wholeMapInvalidations).toBe(80_000) + expect(scopedEntryInvalidations).toBe(0) + }) + + it('publishes owning entry replacements and handles an absent worktree', () => { + const firstStatus = [{ path: 'src/old.ts' }] as GitStatusEntry[] + const nextStatus = [{ path: 'src/new.ts' }] as GitStatusEntry[] + const firstBranch = [{ path: 'src/old.ts' }] as GitBranchChangeEntry[] + const nextBranch = [{ path: 'src/new.ts' }] as GitBranchChangeEntry[] + + expect( + selectEditorPanelGitStatusEntries({ gitStatusByWorktree: { active: firstStatus } }, 'active') + ).toBe(firstStatus) + expect( + selectEditorPanelGitStatusEntries({ gitStatusByWorktree: { active: nextStatus } }, 'active') + ).toBe(nextStatus) + expect( + selectEditorPanelGitBranchEntries( + { gitBranchChangesByWorktree: { active: firstBranch } }, + 'active' + ) + ).toBe(firstBranch) + expect( + selectEditorPanelGitBranchEntries( + { gitBranchChangesByWorktree: { active: nextBranch } }, + 'active' + ) + ).toBe(nextBranch) + expect(selectEditorPanelGitStatusEntries({ gitStatusByWorktree: {} }, null)).toBeUndefined() + }) +}) diff --git a/src/renderer/src/components/editor/editor-panel-git-entry-selector.ts b/src/renderer/src/components/editor/editor-panel-git-entry-selector.ts new file mode 100644 index 00000000000..a705c0d65aa --- /dev/null +++ b/src/renderer/src/components/editor/editor-panel-git-entry-selector.ts @@ -0,0 +1,17 @@ +import type { AppState } from '@/store' + +type EditorPanelGitEntryState = Pick + +export function selectEditorPanelGitStatusEntries( + state: Pick, + worktreeId: string | null | undefined +): AppState['gitStatusByWorktree'][string] | undefined { + return worktreeId ? state.gitStatusByWorktree[worktreeId] : undefined +} + +export function selectEditorPanelGitBranchEntries( + state: Pick, + worktreeId: string | null | undefined +): AppState['gitBranchChangesByWorktree'][string] | undefined { + return worktreeId ? state.gitBranchChangesByWorktree[worktreeId] : undefined +} diff --git a/src/renderer/src/components/editor/editor-panel-render-model.test.ts b/src/renderer/src/components/editor/editor-panel-render-model.test.ts index 88e3a539e63..adbb6eafb9d 100644 --- a/src/renderer/src/components/editor/editor-panel-render-model.test.ts +++ b/src/renderer/src/components/editor/editor-panel-render-model.test.ts @@ -34,12 +34,13 @@ function renderModel(args: { isChangesMode?: boolean gitStatusByWorktree?: Record }) { + const activeFile = args.activeFile ?? markdownFile() return getEditorPanelRenderModel({ - activeFile: args.activeFile ?? markdownFile(), + activeFile, fileContents: args.fileContents ?? { '/repo/README.md': textContent() }, editorDrafts: args.editorDrafts ?? {}, - gitStatusByWorktree: args.gitStatusByWorktree ?? {}, - gitBranchChangesByWorktree: {}, + gitStatusEntries: args.gitStatusByWorktree?.[activeFile.worktreeId], + gitBranchEntries: undefined, markdownViewMode: args.markdownViewMode ?? {}, isChangesMode: args.isChangesMode ?? false }) diff --git a/src/renderer/src/components/editor/editor-panel-render-model.ts b/src/renderer/src/components/editor/editor-panel-render-model.ts index c471b2e73ad..901c694fad7 100644 --- a/src/renderer/src/components/editor/editor-panel-render-model.ts +++ b/src/renderer/src/components/editor/editor-panel-render-model.ts @@ -22,8 +22,8 @@ type EditorPanelRenderModelParams = { activeFile: OpenFile fileContents: Record editorDrafts: StoreState['editorDrafts'] - gitStatusByWorktree: StoreState['gitStatusByWorktree'] - gitBranchChangesByWorktree: StoreState['gitBranchChangesByWorktree'] + gitStatusEntries: StoreState['gitStatusByWorktree'][string] | undefined + gitBranchEntries: StoreState['gitBranchChangesByWorktree'][string] | undefined markdownViewMode: StoreState['markdownViewMode'] isChangesMode: boolean } @@ -32,8 +32,8 @@ export function getEditorPanelRenderModel({ activeFile, fileContents, editorDrafts, - gitStatusByWorktree, - gitBranchChangesByWorktree, + gitStatusEntries, + gitBranchEntries, markdownViewMode, isChangesMode }: EditorPanelRenderModelParams) { @@ -54,8 +54,8 @@ export function getEditorPanelRenderModel({ activeFile.mode === 'diff' ? detectLanguage(activeFile.relativePath) : detectLanguage(activeFile.filePath) - const worktreeEntries = gitStatusByWorktree[activeFile.worktreeId] ?? [] - const branchEntries = gitBranchChangesByWorktree[activeFile.worktreeId] ?? [] + const worktreeEntries = gitStatusEntries ?? [] + const branchEntries = gitBranchEntries ?? [] const matchingWorktreeEntry = activeFile.mode === 'diff' && (activeFile.diffSource === 'staged' || activeFile.diffSource === 'unstaged') diff --git a/src/renderer/src/components/editor/useEditorPanelContentState.test.tsx b/src/renderer/src/components/editor/useEditorPanelContentState.test.tsx index 90807e66cc7..4ac009d24f1 100644 --- a/src/renderer/src/components/editor/useEditorPanelContentState.test.tsx +++ b/src/renderer/src/components/editor/useEditorPanelContentState.test.tsx @@ -101,7 +101,7 @@ function HookProbe({ activeFile, isChangesMode: false, openFiles, - gitStatusByWorktree, + gitStatusEntries: activeFile ? gitStatusByWorktree[activeFile.worktreeId] : undefined, editorViewMode: {} }) latestFileContents = state.fileContents diff --git a/src/renderer/src/components/editor/useEditorPanelContentState.ts b/src/renderer/src/components/editor/useEditorPanelContentState.ts index f4f668a5a4c..6e60afbf833 100644 --- a/src/renderer/src/components/editor/useEditorPanelContentState.ts +++ b/src/renderer/src/components/editor/useEditorPanelContentState.ts @@ -41,7 +41,7 @@ type UseEditorPanelContentStateParams = { activeFile: OpenFile | null isChangesMode: boolean openFiles: OpenFile[] - gitStatusByWorktree: GitStatusByWorktree + gitStatusEntries: GitStatusByWorktree[string] | undefined editorViewMode: EditorViewModeByFile } @@ -94,7 +94,7 @@ export function useEditorPanelContentState({ activeFile, isChangesMode, openFiles, - gitStatusByWorktree, + gitStatusEntries, editorViewMode }: UseEditorPanelContentStateParams): UseEditorPanelContentStateResult { const [fileContents, setFileContents] = useState>({}) @@ -362,7 +362,7 @@ export function useEditorPanelContentState({ } const snapshotPaths = new Set(snapshotEntries.map((entry) => entry.path)) - const liveEntries = gitStatusByWorktree[activeFile.worktreeId] ?? [] + const liveEntries = gitStatusEntries ?? [] for (const entry of liveEntries) { if ( !snapshotPaths.has(entry.path) || @@ -411,7 +411,7 @@ export function useEditorPanelContentState({ activeFile?.conflictReview?.snapshotTimestamp, selectedConflictReviewFile?.id, isChangesMode, - gitStatusByWorktree + gitStatusEntries ]) useEditorPanelFileLoadRetry({ @@ -423,9 +423,7 @@ export function useEditorPanelContentState({ setFileContents }) - const changesStatusEntries = activeFile?.worktreeId - ? gitStatusByWorktree[activeFile.worktreeId] - : undefined + const changesStatusEntries = activeFile?.worktreeId ? gitStatusEntries : undefined const activeFileGitStatusEntries = useMemo(() => { if (!activeFile?.relativePath || !changesStatusEntries) { return undefined