package editor // WrapIndex tracks, for each logical line, how many visual (wrapped) lines // it occupies when the editor renders with word wrap enabled. // // Why it exists: every scroll-to-content mapping in the editor runs in // visual-line space. A logical line k that wraps into W(k) visual lines has // document height V(k)*lineHeight where V(k) = sum of W over lines [0,k). // The scroll offset, the window start line, the sub-line draw offset, the // max scroll and tap positioning all decompose against V, so scrolling past // a wrapped line advances the viewport by the wrapped line's full height // instead of jumping over its wrapped remainder. // // Counts start as estimates of 1 (one visual line per logical line) until // the renderer shapes the line; the shaped window's glyph layout corrects // the counts for the lines it covered, every frame (applyWrapCounts). An // all-ones index is exactly the legacy no-wrap mapping, so behavior before // the first shaping pass — and with word wrap off — is unchanged. // // A Fenwick tree answers prefix sums (VisualsBefore, TotalVisuals) and // per-line corrections in O(log n). Line insertions and deletions rebuild // the structure in O(n), the same cost class as LineIndex maintenance. // // int32 is safe for the values stored: the max total visual lines is bounded // by the file size (every visual line holds at least one byte; the editable // cap is 50 MB), and per-line counts are bounded by the same amount. // // Memory: two int32 arrays, i.e. 8 bytes per logical line on top of the // existing LineIndex (4 bytes/line). Realistic note files are negligible; // a 50 MB file of one-character lines (~50M lines) would cost ~400 MB here // and ~200 MB in LineIndex already. type WrapIndex struct { counts []int32 // visual lines per logical line (1 = estimate or single line) tree []int32 // Fenwick tree over counts (1-indexed, len = n+1) } // NewWrapIndex returns an index for nLines logical lines, all estimated at // one visual line. func NewWrapIndex(nLines int) *WrapIndex { if nLines < 0 { nLines = 0 } w := &WrapIndex{ counts: make([]int32, nLines), tree: make([]int32, nLines+1), } for i := range w.counts { w.counts[i] = 1 } w.rebuild() return w } // rebuild recomputes the Fenwick tree from counts (linear build). func (w *WrapIndex) rebuild() { n := len(w.counts) copy(w.tree[1:n+1], w.counts) for i := 1; i <= n; i++ { j := i + (i & -i) if j <= n { w.tree[j] += w.tree[i] } } } // Len is the number of logical lines tracked. func (w *WrapIndex) Len() int { return len(w.counts) } // Get returns the current visual-line count for line i. func (w *WrapIndex) Get(i int) int32 { return w.counts[i] } // Set updates the visual-line count for line i. No-op if unchanged, so the // per-frame correction pass only touches lines whose wrap actually changed. func (w *WrapIndex) Set(i int, c int32) { if i < 0 || i >= len(w.counts) || c <= 0 { return } if w.counts[i] == c { return } delta := c - w.counts[i] w.counts[i] = c for j := i + 1; j < len(w.tree); j += j & -j { w.tree[j] += delta } } // SetRange updates consecutive lines' counts (the shaped-window correction // pass). Bounds are clamped. func (w *WrapIndex) SetRange(start int, counts []int32) { for i, c := range counts { w.Set(start+i, c) } } // VisualsBefore returns V(k): the total number of visual lines occupied by // logical lines [0,k). Out-of-range k is clamped. func (w *WrapIndex) VisualsBefore(k int) int32 { if k < 0 { return 0 } if k > len(w.counts) { k = len(w.counts) } var s int32 for i := k; i > 0; i -= i & -i { s += w.tree[i] } return s } // TotalVisuals returns the total number of visual lines in the document. func (w *WrapIndex) TotalVisuals() int32 { return w.VisualsBefore(len(w.counts)) } // LineForVisual returns the logical line that contains visual line v // (0-indexed): the smallest k with V(k+1) > v. Clamped to [0, n-1]; // for an empty index it returns 0. // // Implemented as a binary search over VisualsBefore (O(log^2 n)); at the // file sizes this app supports (tens of thousands of lines) that is far // below a microsecond and keeps the Fenwick code simple. func (w *WrapIndex) LineForVisual(v int32) int { n := len(w.counts) if n == 0 { return 0 } if v <= 0 { return 0 } if v >= w.TotalVisuals() { return n - 1 } // Invariant: V(k) is monotone non-decreasing in k, and V(k+1) > V(k) // (counts >= 1), so the search is well-defined. lo, hi := 0, n-1 for lo < hi { mid := (lo + hi) / 2 if w.VisualsBefore(mid+1) > v { hi = mid } else { lo = mid + 1 } } return lo } // InsertLines adds n lines (each estimated at one visual line) at position // pos. pos is clamped to [0, Len]. func (w *WrapIndex) InsertLines(pos, n int) { if n <= 0 { return } if pos < 0 { pos = 0 } if pos > len(w.counts) { pos = len(w.counts) } newCounts := make([]int32, 0, len(w.counts)+n) newCounts = append(newCounts, w.counts[:pos]...) for i := 0; i < n; i++ { newCounts = append(newCounts, 1) } newCounts = append(newCounts, w.counts[pos:]...) w.counts = newCounts w.tree = make([]int32, len(w.counts)+1) w.rebuild() } // DeleteLines removes n lines at position pos. Clamped to what exists. func (w *WrapIndex) DeleteLines(pos, n int) { if n <= 0 { return } if pos < 0 { pos = 0 } if pos >= len(w.counts) { return } if pos+n > len(w.counts) { n = len(w.counts) - pos } w.counts = append(w.counts[:pos], w.counts[pos+n:]...) w.tree = make([]int32, len(w.counts)+1) w.rebuild() } // ResetAll sets every line back to the estimate of one visual line. Used // when the wrap width changes (window resize), which invalidates every // shaped count; the visible window is re-corrected on the next frame. func (w *WrapIndex) ResetAll() { for i := range w.counts { w.counts[i] = 1 } w.rebuild() }