Summary
View mode rendering has major inefficiencies in how it handles fold regions for headings folding. Work that only needs to happen once per document render is being repeated per-frame or per-fold.
- every render frame calls
findFoldAtLineonce per visible line, and that function does a full linear scan over all folds in the document. This is a direct contributor to the lag users experience when jumping to the top or bottom of a long document withggorG. - whenever content or pane width changes,
matchFoldsToRendereduses a regex-based ANSI stripper on every rendered line for every fold it needs to match. While this does not fire every frame, it fires on every resize and every content change in view mode, contributing to the stutter seen when resizing panes or switching tabs.
Proposed Fix
The fix is to:
- use a
foldByLinelookup map when folds are matched, making per-frame lookups O(1) - pre-compute ANSI-stripped lines in
renderTabsomatchFoldsToRenderedhas zero regex work to do
Conditions of Satisfaction
- Add
foldByLine map[int]intandstrippedLines []stringfields totabSharedinviewer.gofoldByLinemaps a rendered line index to the index of the corresponding fold in thefoldsslice, enabling O(1) lookup instead of a linear scanstrippedLinesholds ANSI-stripped, lowercased versions oflines[], computed once inrenderTaband reused bymatchFoldsToRenderedso it never calls thestripANSIregex directly- Both fields are owned by
tabSharedbecause they are derived from shared render state (linesandfolds), not per-pane view state
- Populate
strippedLinesinrenderTab(render.go) immediately aftertab.linesis set- Iterate
tab.linesonce, applyingstrings.ToLower(stripANSI(l))to each entry and storing intotab.strippedLines - This single O(lines) pass replaces the O(lines x folds) repeated stripping inside
matchFoldsToRendered strippedLinesis naturally invalidated wheneverrenderTabruns, since it replaces the slice in the same call that replacestab.lines
- Iterate
- Update
matchFoldsToRenderedinfold.goto consumetab.strippedLinesinstead of callingstripANSIinline- The search loop replaces
strings.ToLower(stripANSI(tab.lines[j]))withtab.strippedLines[j], which is a plain slice read - At the end of the function, after all
lineIdxvalues are resolved, build thefoldByLinemap by iteratingtab.foldsand inserting entries for all folds with a non-negativelineIdx - Re-creating the map each call (rather than clearing and repopulating) is sufficient since
matchFoldsToRenderedonly runs when rendered output changes
- The search loop replaces
- Rewrite
findFoldAtLineinfold.goto use thefoldByLinemap- The function signature and return type remain unchanged so no call sites in
renderViewContentortoggleFoldAtCursorneed updating - Guard against a nil map (e.g. on first access before any render) by returning nil immediately, matching the current behavior for a fold-less document
- The function signature and return type remain unchanged so no call sites in
- Confirm
toggleFoldAtCursorremains correct after the changetoggleFoldAtCursorcallsbuildDisplayLinesafter toggling a fold but does not callmatchFoldsToRendered, sofoldByLineis not rebuilt on toggle- Verify that
findFoldAtLineresults are still correct after a toggle sincelineIdxvalues infolds[]do not change during a toggle, only thefoldedflag does -- the map remains valid
External Resources
N/A
## Summary
View mode rendering has major inefficiencies in how it handles fold regions for headings folding. Work that only needs to happen once per document render is being repeated per-frame or per-fold.
1. every render frame calls `findFoldAtLine` once per visible line, and that function does a full linear scan over all folds in the document. This is a direct contributor to the lag users experience when jumping to the top or bottom of a long document with `gg` or `G`.
2. whenever content or pane width changes, `matchFoldsToRendered` uses a regex-based ANSI stripper on _every_ rendered line for _every_ fold it needs to match. While this does not fire every frame, it fires on every resize and every content change in view mode, contributing to the stutter seen when resizing panes or switching tabs.
## Proposed Fix
The fix is to:
- use a `foldByLine` lookup map when folds are matched, making per-frame lookups O(1)
- pre-compute ANSI-stripped lines in `renderTab` so `matchFoldsToRendered` has zero regex work to do
## Conditions of Satisfaction
- [ ] Add `foldByLine map[int]int` and `strippedLines []string` fields to `tabShared` in `viewer.go`
- `foldByLine` maps a rendered line index to the index of the corresponding fold in the `folds` slice, enabling O(1) lookup instead of a linear scan
- `strippedLines` holds ANSI-stripped, lowercased versions of `lines[]`, computed once in `renderTab` and reused by `matchFoldsToRendered` so it never calls the `stripANSI` regex directly
- Both fields are owned by `tabShared` because they are derived from shared render state (`lines` and `folds`), not per-pane view state
- [ ] Populate `strippedLines` in `renderTab` (`render.go`) immediately after `tab.lines` is set
- Iterate `tab.lines` once, applying `strings.ToLower(stripANSI(l))` to each entry and storing into `tab.strippedLines`
- This single O(lines) pass replaces the O(lines x folds) repeated stripping inside `matchFoldsToRendered`
- `strippedLines` is naturally invalidated whenever `renderTab` runs, since it replaces the slice in the same call that replaces `tab.lines`
- [ ] Update `matchFoldsToRendered` in `fold.go` to consume `tab.strippedLines` instead of calling `stripANSI` inline
- The search loop replaces `strings.ToLower(stripANSI(tab.lines[j]))` with `tab.strippedLines[j]`, which is a plain slice read
- At the end of the function, after all `lineIdx` values are resolved, build the `foldByLine` map by iterating `tab.folds` and inserting entries for all folds with a non-negative `lineIdx`
- Re-creating the map each call (rather than clearing and repopulating) is sufficient since `matchFoldsToRendered` only runs when rendered output changes
- [ ] Rewrite `findFoldAtLine` in `fold.go` to use the `foldByLine` map
- The function signature and return type remain unchanged so no call sites in `renderViewContent` or `toggleFoldAtCursor` need updating
- Guard against a nil map (e.g. on first access before any render) by returning nil immediately, matching the current behavior for a fold-less document
- [ ] Confirm `toggleFoldAtCursor` remains correct after the change
- `toggleFoldAtCursor` calls `buildDisplayLines` after toggling a fold but does not call `matchFoldsToRendered`, so `foldByLine` is not rebuilt on toggle
- Verify that `findFoldAtLine` results are still correct after a toggle since `lineIdx` values in `folds[]` do not change during a toggle, only the `folded` flag does -- the map remains valid
## External Resources
N/A