Summary
In edit mode, two separate functions each independently call strings.Split(t.content, "\n") on every keypress and every render frame. syncEditOffset splits the full document to calculate scroll position, and renderEditView splits it again to build the visible line output.
Because both run in the same frame on every keystroke, a single keypress in a 1000-line file produces two full document splits (roughly 2MB of short-lived slice allocations that immediately become garbage collected). This constant GC pressure accumulates over a typing session and contributes to the subtle lag users experience in insert mode, particularly in larger files. It also might explain some of the astronomical memory use I saw when profiling the WIP v2 BubbleTea migration branches code earlier.
To make matters worse, paneGutterWidth, called from both, iterates all open tabs in the pane on every render to recompute the line-number column width, even though the digit count of a line number almost never changes during normal editing.
Proposed Fix/Enhancement
Maintain a rawLines []string field on tabShared that is kept in sync with content via a setContent() helper. Consumers that previously called strings.Split locally just reference t.rawLines instead. Gutter width is similarly cached on the pane struct and only recomputed when the tab set or line count actually changes.
Conditions of Satisfaction
- Add
rawLines []stringto thetabSharedstruct inviewer.goand introduce asetContent(s string)helper method ontabShared- The helper sets both
ts.content = sandts.rawLines = strings.Split(s, "\n")atomically, making it impossible for the two fields to fall out of sync rawLineslives ontabSharedrather thanviewerTabbecausecontentis shared across all pane views of the same file
- The helper sets both
- Replace all direct
t.content = ...assignments witht.setContent(...)at every call site- Sites to update include:
handleInsertKeysandhandleReplaceKeysinkeys.go,reloadTextareaand any other direct assignments inedit.go, the undo and redo apply paths, and the file-load path wherevertabShared.contentis first populated - A grep for
\.content =scoped totabSharedreceivers will surface all locations that need updating
- Sites to update include:
- Update
syncEditOffsetandrenderEditViewinrender.goto uset.rawLinesinstead of callingstrings.Split- The logic of both functions is otherwise unchanged; only the source of
contentLineschanges from a fresh allocation to the cached field - With both functions using the cache, zero
strings.Splitallocations occur per keystroke during editing
- The logic of both functions is otherwise unchanged; only the source of
- Add a
gutterWidth intfield to thepanestruct and introduce anupdatePaneGutterWidth(paneIdx int)method onviewer.Model- The method iterates tabs in the pane and computes the max digit count needed for line numbers, storing the result on
pane.gutterWidth paneGutterWidth()becomes a simple field read instead of an iteration, eliminating the per-frame N-tab scanupdatePaneGutterWidthis called when a tab is opened or closed in a pane, and afterbuildDisplayLinesupdatestotalLinesinrenderTab- Because line-number digit counts only change when a file crosses a power-of-10 line boundary, recomputes are extremely rare in practice
- The method iterates tabs in the pane and computes the max digit count needed for line numbers, storing the result on
External Resources
N/A