perf: tag the center column instead of a :has() selector; stop re-stringifying the editor doc per keystroke
The bottom-push rule anchored on #root :has(> [data-slot=conversation]): a :has() match cache invalidates on every #root subtree mutation, and the streaming chat mutates that subtree constantly — every layout-push frame (the bottom drag writes --dsh-sidebar-height per frame) paid a whole-document selector re-evaluation. The shell's locator (which already holds the measured column node for the ResizeObserver) now tags it with [data-dsh-center-col]; the rule evaluates once per node. The tag retargets with the ref on boot swap / HMR node replacement and is removed on unmount; the drag lane pins exactly-one-tagged-node and its identity as the conversation slot wrapper's parent. The dead [data-pane] branch (renamed away in alpha hosts) is gone. TextEditor: the CodeMirror updateListener re-stringified the WHOLE document into React state on every keystroke (O(docLength) per key, re-rendering the shell per key). The listener now only flips the dirty flag; the preview's draft is snapshotted from the live view when entering preview (or on a content swap), and save() already reads the live document.
This commit is contained in:
@@ -114,11 +114,13 @@ export function TextEditor(props: FileViewerProps) {
|
||||
}, [content])
|
||||
|
||||
// Create the CodeMirror editor once the content is loaded. The view owns
|
||||
// the document; React only tracks dirty/draft state through the update
|
||||
// listener. For markdown the view stays mounted while previewing (hidden),
|
||||
// so unsaved edits survive the preview/edit toggle. The theme + syntax
|
||||
// colors live in a compartment so a scheme flip reconfigures only that
|
||||
// part — the document, undo history and scroll position survive.
|
||||
// the document; React only tracks dirty state through the update listener
|
||||
// (the draft — the preview's text — is snapshotted from the live view on
|
||||
// entering preview, not re-stringified per keystroke). For markdown the
|
||||
// view stays mounted while previewing (hidden), so unsaved edits survive
|
||||
// the preview/edit toggle. The theme + syntax colors live in a compartment
|
||||
// so a scheme flip reconfigures only that part — the document, undo
|
||||
// history and scroll position survive.
|
||||
useEffect(() => {
|
||||
if (content === undefined) return
|
||||
const host = hostRef.current
|
||||
@@ -139,7 +141,6 @@ export function TextEditor(props: FileViewerProps) {
|
||||
...(language !== null ? [language] : []),
|
||||
CodeMirrorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
setDraft(update.state.doc.toString())
|
||||
setDirty(true)
|
||||
}
|
||||
}),
|
||||
@@ -226,6 +227,17 @@ export function TextEditor(props: FileViewerProps) {
|
||||
if (mode === 'edit') viewRef.current?.requestMeasure()
|
||||
}, [mode])
|
||||
|
||||
// Snapshot the live document into the draft whenever the preview needs
|
||||
// it: entering preview (the markdown preview renders `draft ?? content`)
|
||||
// and a content swap (the view was just re-created above, so the read is
|
||||
// the new document — matching the reset-to-null of a clean tab). The
|
||||
// updateListener used to re-stringify the WHOLE document on every
|
||||
// keystroke (O(docLength) per key) for a draft only preview reads.
|
||||
useEffect(() => {
|
||||
const view = viewRef.current
|
||||
setDraft(view === null ? null : view.state.doc.toString())
|
||||
}, [mode, content])
|
||||
|
||||
const save = (): void => {
|
||||
const view = viewRef.current
|
||||
if (view === null || savingRef.current) return
|
||||
|
||||
Reference in New Issue
Block a user