perf(renderer): scope editor draft subscriptions by panel (#8105)

* perf(renderer): scope editor draft subscriptions by panel

* fix(renderer): keep conflict overview drafts live
This commit is contained in:
Neil 2026-07-10 20:51:54 -07:00 committed by GitHub
parent d617bda234
commit e0207cb360
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 179 additions and 2 deletions

View File

@ -1,4 +1,4 @@
import React, { useCallback, useRef, useState } from 'react'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { useAppStore } from '@/store'
import { getConnectionId } from '@/lib/connection-context'
import { detectLanguage } from '@/lib/language-detect'
@ -22,6 +22,7 @@ import {
selectEditorPanelGitBranchEntries,
selectEditorPanelGitStatusEntries
} from './editor-panel-git-entry-selector'
import { createEditorPanelDraftSelector } from './editor-panel-draft-selector'
function EditorPanelInner({
activeFileId: activeFileIdProp,
@ -60,7 +61,11 @@ function EditorPanelInner({
const setMarkdownTableOfContentsVisible = useAppStore((s) => s.setMarkdownTableOfContentsVisible)
const closeFile = useAppStore((s) => s.closeFile)
const clearUntitled = useAppStore((s) => s.clearUntitled)
const editorDrafts = useAppStore((s) => s.editorDrafts)
const editorDraftSelector = useMemo(
() => createEditorPanelDraftSelector(activeFile),
[activeFile]
)
const editorDrafts = useAppStore(editorDraftSelector)
const setEditorDraft = useAppStore((s) => s.setEditorDraft)
const settings = useAppStore((s) => s.settings)
const panelRef = useRef<HTMLDivElement>(null)

View File

@ -0,0 +1,109 @@
import { describe, expect, it } from 'vitest'
import type { OpenFile } from '@/store/slices/editor'
import { createEditorPanelDraftSelector } from './editor-panel-draft-selector'
function makeFile(id: string, overrides: Partial<OpenFile> = {}): OpenFile {
return {
id,
filePath: `/repo/${id}.ts`,
relativePath: `${id}.ts`,
worktreeId: 'worktree-1',
language: 'typescript',
mode: 'edit',
isDirty: false,
...overrides
}
}
describe('createEditorPanelDraftSelector', () => {
it('limits draft invalidations to the panel that owns each keystroke', () => {
const files = Array.from({ length: 200 }, (_, index) => makeFile(`file-${index}`))
let editorDrafts: Record<string, string> = {}
let wholeMapInvalidations = 0
let scopedInvalidations = 0
let unrelatedPanelInvalidations = 0
const selectors = files.map(createEditorPanelDraftSelector)
const previousSelections = selectors.map((selector) => selector({ editorDrafts }))
for (let edit = 0; edit < 200; edit += 1) {
const previousDrafts = editorDrafts
editorDrafts = { ...editorDrafts, 'file-0': `edit-${edit}` }
for (let panelIndex = 0; panelIndex < files.length; panelIndex += 1) {
if (previousDrafts !== editorDrafts) {
wholeMapInvalidations += 1
}
const nextSelection = selectors[panelIndex]({ editorDrafts })
if (previousSelections[panelIndex] !== nextSelection) {
scopedInvalidations += 1
if (panelIndex !== 0) {
unrelatedPanelInvalidations += 1
}
previousSelections[panelIndex] = nextSelection
}
}
}
expect(wholeMapInvalidations).toBe(40_000)
expect(scopedInvalidations).toBe(200)
expect(unrelatedPanelInvalidations).toBe(0)
})
it('includes preview and selected conflict-review drafts but excludes unrelated files', () => {
const preview = makeFile('preview', { markdownPreviewSourceFileId: 'source' })
const conflictReview = makeFile('review', {
mode: 'conflict-review',
conflictReview: { selectedFileId: 'selected' } as NonNullable<OpenFile['conflictReview']>
})
const editorDrafts = {
preview: 'preview draft',
source: '',
review: 'review draft',
selected: 'selected draft',
unrelated: 'other draft'
}
const selectPreviewDrafts = createEditorPanelDraftSelector(preview)
const selectConflictDrafts = createEditorPanelDraftSelector(conflictReview)
const selectNoDrafts = createEditorPanelDraftSelector(null)
expect(selectPreviewDrafts({ editorDrafts })).toEqual({
preview: 'preview draft',
source: ''
})
expect(selectConflictDrafts({ editorDrafts })).toEqual({
review: 'review draft',
selected: 'selected draft'
})
expect(selectNoDrafts({ editorDrafts })).toEqual({})
})
it('includes overview conflict drafts and ignores unrelated draft replacements', () => {
const conflictReview = makeFile('review', {
filePath: 'C:\\repo',
mode: 'conflict-review',
conflictReview: {
source: 'live-summary',
snapshotTimestamp: 1,
entries: [
{ path: 'src/a.ts', conflictKind: 'both_modified' },
{ path: 'src\\b.ts', conflictKind: 'both_modified' }
]
}
})
const selectDrafts = createEditorPanelDraftSelector(conflictReview)
const editorDrafts = {
'C:\\repo\\src\\a.ts': 'draft a',
'C:\\repo\\src\\b.ts': '',
unrelated: 'other draft'
}
const selection = selectDrafts({ editorDrafts })
expect(selection).toEqual({
'C:\\repo\\src\\a.ts': 'draft a',
'C:\\repo\\src\\b.ts': ''
})
expect(
selectDrafts({ editorDrafts: { ...editorDrafts, unrelated: 'changed elsewhere' } })
).toBe(selection)
})
})

View File

@ -0,0 +1,63 @@
import type { AppState } from '@/store'
import { joinPath } from '@/lib/path'
import type { OpenFile } from '@/store/slices/editor'
type EditorDraftState = Pick<AppState, 'editorDrafts'>
type EditorPanelDraftSelector = (state: EditorDraftState) => Record<string, string>
const EMPTY_EDITOR_PANEL_DRAFTS = Object.freeze({}) as Record<string, string>
export function createEditorPanelDraftSelector(
activeFile: OpenFile | null
): EditorPanelDraftSelector {
// Why: previews and conflict review can render a related file, but drafts
// from every other panel must not wake this editor on each keystroke.
const fileIds = activeFile
? Array.from(
new Set(
[
activeFile.id,
activeFile.markdownPreviewSourceFileId,
activeFile.conflictReview?.selectedFileId,
...(activeFile.mode === 'conflict-review' && !activeFile.conflictReview?.selectedFileId
? (activeFile.conflictReview?.entries ?? []).map((entry) =>
joinPath(activeFile.filePath, entry.path)
)
: [])
].filter((fileId): fileId is string => Boolean(fileId))
)
)
: []
let previousDrafts: AppState['editorDrafts'] | null = null
let previousSelection = EMPTY_EDITOR_PANEL_DRAFTS
return (state) => {
// Why: every Zustand write reruns the selector. The slice identity guard
// keeps unrelated terminal/status traffic allocation-free.
if (previousDrafts === state.editorDrafts) {
return previousSelection
}
previousDrafts = state.editorDrafts
const changed = fileIds.some((fileId) => {
const draft = state.editorDrafts[fileId]
return (
draft !== previousSelection[fileId] ||
(draft === undefined && Object.prototype.hasOwnProperty.call(previousSelection, fileId))
)
})
if (!changed) {
return previousSelection
}
const nextSelection: Record<string, string> = {}
for (const fileId of fileIds) {
const draft = state.editorDrafts[fileId]
if (draft !== undefined) {
nextSelection[fileId] = draft
}
}
previousSelection = nextSelection
return previousSelection
}
}