Summary
Editor.OnDrawingContent walks the document directly every frame and inlines three concerns that the spec splits cleanly: per-line rendering, syntax highlighting, and selection background. The shape works pre-MVP but several scaling and correctness gaps are baked in. The fix is the planned VisualLineBuilder → CellVisualLine → IVisualLineTransformer / IBackgroundRenderer pipeline (specs/00-plan.md §6); these symptoms collapse together when it lands.
Symptoms (each will resolve when §6 ships)
1. UpdateContentSize is O(N lines) per edit
Editor.cs:167-178 walks every DocumentLine to recompute maxWidth on every Document.Changed. Fine on small files; on the editing hot path of a 10k-line document this is per-keystroke.
2. Duplicate line lookups per frame
UpdateCursor (Editor.Drawing.cs:213-237) and EnsureCaretVisible (Editor.cs:208-244) each call GetCaretLineIndex() and GetCaretColumn() back-to-back; both walk the line index. Same wasted work in the loop body of OnDrawingContent (rebuilding line text each frame).
3. Char-length, not cell-width
Editor.Drawing.cs:44-50 and Editor.Mouse.cs:91-96 both treat the viewport as character-indexed. Wide CJK / emoji / combining marks misalign rendering AND make mouse clicks hit the wrong offset. Spec §6 calls this out — measurement is grapheme + string.GetColumns().
4. Syntax highlighter re-tokenizes from line 0 every frame
Editor.Drawing.cs:72-86 (PrepareSyntaxHighlighter) calls Highlight() on every line from 0 up to the first visible line on every redraw. Scroll to line 9,000 of a 10k-line file → 9,000 highlight calls per redraw. The highlighter is re-fed the entire above-viewport text every time the cursor blinks (or a single keystroke arrives).
5. Selection rendered inline as 3-way string slicing
DrawLineRuns / DrawRun / DrawRunPart (Editor.Drawing.cs:88-211) split each visible run into pre-selection / in-selection / post-selection sub-runs and switch attributes manually. Per spec §6 this is exactly what an IBackgroundRenderer exists for — paint cell rectangles for the selection, draw text once with the syntax-derived attribute. Today's implementation also can't compose with future renderers (current-line highlight, search-hit highlight) without each one reaching back into the same string-slicing code.
What §6 changes
DocumentLine ──▶ VisualLineBuilder ──▶ CellVisualLine (one or more CellVisualLineElements)
IVisualLineTransformer[] ── set Attribute on element ranges
IBackgroundRenderer[] ── paint cell rectangles (selection, current line, search hits)
A HighlightingColorizer : IVisualLineTransformer replaces today's PrepareSyntaxHighlighter + inline run-splitting. A SelectionBackgroundRenderer : IBackgroundRenderer replaces the 3-way slicing. Visual lines cache; Document.Changed invalidates only the affected range; line-width tracking updates incrementally.
Recommendation
Treat the §6 pipeline as the next big architectural milestone. Until then, add // TODO: cells, not chars — see specs/00-plan.md §6 markers to (3) and tracking-issue links from each touch point so it doesn't become invisible tech debt.
Summary
Editor.OnDrawingContentwalks the document directly every frame and inlines three concerns that the spec splits cleanly: per-line rendering, syntax highlighting, and selection background. The shape works pre-MVP but several scaling and correctness gaps are baked in. The fix is the plannedVisualLineBuilder→CellVisualLine→IVisualLineTransformer/IBackgroundRendererpipeline (specs/00-plan.md§6); these symptoms collapse together when it lands.Symptoms (each will resolve when §6 ships)
1.
UpdateContentSizeis O(N lines) per editEditor.cs:167-178walks everyDocumentLineto recomputemaxWidthon everyDocument.Changed. Fine on small files; on the editing hot path of a 10k-line document this is per-keystroke.2. Duplicate line lookups per frame
UpdateCursor(Editor.Drawing.cs:213-237) andEnsureCaretVisible(Editor.cs:208-244) each callGetCaretLineIndex()andGetCaretColumn()back-to-back; both walk the line index. Same wasted work in the loop body ofOnDrawingContent(rebuilding line text each frame).3. Char-length, not cell-width
Editor.Drawing.cs:44-50andEditor.Mouse.cs:91-96both treat the viewport as character-indexed. Wide CJK / emoji / combining marks misalign rendering AND make mouse clicks hit the wrong offset. Spec §6 calls this out — measurement is grapheme +string.GetColumns().4. Syntax highlighter re-tokenizes from line 0 every frame
Editor.Drawing.cs:72-86(PrepareSyntaxHighlighter) callsHighlight()on every line from 0 up to the first visible line on every redraw. Scroll to line 9,000 of a 10k-line file → 9,000 highlight calls per redraw. The highlighter is re-fed the entire above-viewport text every time the cursor blinks (or a single keystroke arrives).5. Selection rendered inline as 3-way string slicing
DrawLineRuns/DrawRun/DrawRunPart(Editor.Drawing.cs:88-211) split each visible run into pre-selection / in-selection / post-selection sub-runs and switch attributes manually. Per spec §6 this is exactly what anIBackgroundRendererexists for — paint cell rectangles for the selection, draw text once with the syntax-derived attribute. Today's implementation also can't compose with future renderers (current-line highlight, search-hit highlight) without each one reaching back into the same string-slicing code.What §6 changes
A
HighlightingColorizer : IVisualLineTransformerreplaces today'sPrepareSyntaxHighlighter+ inline run-splitting. ASelectionBackgroundRenderer : IBackgroundRendererreplaces the 3-way slicing. Visual lines cache;Document.Changedinvalidates only the affected range; line-width tracking updates incrementally.Recommendation
Treat the §6 pipeline as the next big architectural milestone. Until then, add
// TODO: cells, not chars — see specs/00-plan.md §6markers to (3) and tracking-issue links from each touch point so it doesn't become invisible tech debt.