Skip to main content

ochrepad_core/
display.rs

1//! Display rows: how a view's document lines map to rows on screen
2//! (PLAN_v0.2.0 3.2). A row is addressed relative to its line, as a
3//! [`RowPos`], so nothing needs a global row index: with word wrap that
4//! would mean wrapping the whole file. Lines hidden by a fold have no rows;
5//! with word wrap on, a line has one row per wrapped piece, otherwise one.
6//!
7//! A compare view's side has [`Padding`]: blank rows before some lines (and
8//! after the last), opposite lines only the other side has. They count as
9//! rows of the line they precede; carets never stand on them.
10//!
11//! The editor widget scrolls, paints and hit-tests through a [`DisplayMap`],
12//! and vertical motions resolve through one, so a macro's `Up` moves one
13//! row in whatever view plays it.
14
15use std::collections::HashMap;
16use std::ops::Range;
17use std::sync::Arc;
18
19use serde::{Deserialize, Serialize};
20
21use crate::buffer::Buffer;
22use crate::fold::FoldSet;
23use crate::history::Revision;
24use crate::position::{
25    ByteOffset, VisualCol, Wrapped, byte_to_visual_col, prev_grapheme_boundary, visual_col_to_byte,
26    wrap_line,
27};
28use crate::text::Document;
29use crate::view::View;
30
31/// One display row: the `sub`-th row of document line `line`. Rows order by
32/// line, then by row within the line.
33#[derive(
34    Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
35)]
36pub struct RowPos {
37    pub line: usize,
38    pub sub: usize,
39}
40
41impl RowPos {
42    pub fn new(line: usize, sub: usize) -> Self {
43        Self { line, sub }
44    }
45}
46
47/// Wrapped lines of one view, for one (width, tab width). An entry made at
48/// an older revision is still good if the line's text is unchanged, so an
49/// edit only rewraps the lines it touched (or shifted).
50#[derive(Clone, Debug, Default)]
51pub(crate) struct WrapCache {
52    key: Option<(usize, usize)>,
53    lines: HashMap<usize, WrapEntry>,
54}
55
56#[derive(Clone, Debug)]
57struct WrapEntry {
58    /// The revision this entry was last checked at.
59    revision: Revision,
60    text: String,
61    wrapped: Arc<Wrapped>,
62}
63
64/// Lines kept in a [`WrapCache`] before it starts over.
65const WRAP_CACHE_LINES: usize = 4096;
66
67/// Blank rows that align a view with another: `before[line]` rows above
68/// each line (missing entries are 0), `after_last` rows after the last.
69#[derive(Clone, Debug, Default, PartialEq, Eq)]
70pub struct Padding {
71    pub before: Vec<usize>,
72    pub after_last: usize,
73}
74
75/// Folds do not apply to a padded view.
76static NO_FOLDS: std::sync::LazyLock<FoldSet> = std::sync::LazyLock::new(FoldSet::default);
77
78/// A view's rows over its document.
79#[derive(Clone, Copy)]
80pub struct DisplayMap<'a> {
81    doc: &'a Document,
82    revision: Revision,
83    folds: &'a FoldSet,
84    wrap: Option<usize>,
85    cache: &'a std::cell::RefCell<WrapCache>,
86    padding: Option<&'a Padding>,
87    tab_width: usize,
88}
89
90impl<'a> DisplayMap<'a> {
91    pub fn new(buffer: &'a Buffer, view: &'a View) -> Self {
92        let tab_width = buffer.indent.width;
93        let padding = view.padding.as_deref();
94        Self {
95            doc: buffer.doc(),
96            revision: buffer.revision(),
97            folds: if padding.is_some() {
98                &NO_FOLDS
99            } else {
100                &view.folds
101            },
102            wrap: view.wrap_cols.filter(|_| padding.is_none()),
103            cache: &view.wrap_cache,
104            padding,
105            tab_width,
106        }
107    }
108
109    pub fn doc(&self) -> &'a Document {
110        self.doc
111    }
112
113    /// Whether lines wrap.
114    pub fn wraps(&self) -> bool {
115        self.wrap.is_some()
116    }
117
118    /// How `line` wraps, or `None` when wrap is off.
119    fn wrapped(&self, line: usize) -> Option<Arc<Wrapped>> {
120        let cols = self.wrap?;
121        let mut cache = self.cache.borrow_mut();
122        let key = (cols, self.tab_width);
123        if cache.key != Some(key) || cache.lines.len() > WRAP_CACHE_LINES {
124            cache.key = Some(key);
125            cache.lines.clear();
126        }
127        if let Some(e) = cache.lines.get_mut(&line) {
128            if e.revision == self.revision {
129                return Some(e.wrapped.clone());
130            }
131            let text = self.doc.line_text(line);
132            if text == e.text {
133                e.revision = self.revision;
134                return Some(e.wrapped.clone());
135            }
136        }
137        let text = self.doc.line_text(line);
138        let wrapped = Arc::new(wrap_line(&text, cols, self.tab_width));
139        let entry = WrapEntry {
140            revision: self.revision,
141            text,
142            wrapped: wrapped.clone(),
143        };
144        cache.lines.insert(line, entry);
145        Some(wrapped)
146    }
147
148    /// Rows that `line` takes on screen: 0 for a hidden line.
149    pub fn rows_in_line(&self, line: usize) -> usize {
150        if self.folds.is_hidden(line) {
151            0
152        } else {
153            self.pad_before(line) + self.text_rows(line) + self.pad_after(line)
154        }
155    }
156
157    /// Rows of `line`'s text (not counting padding).
158    fn text_rows(&self, line: usize) -> usize {
159        self.wrapped(line).map_or(1, |w| w.starts.len())
160    }
161
162    fn pad_before(&self, line: usize) -> usize {
163        self.padding
164            .map_or(0, |p| p.before.get(line).copied().unwrap_or(0))
165    }
166
167    fn pad_after(&self, line: usize) -> usize {
168        match self.padding {
169            Some(p) if line + 1 == self.doc.line_count() => p.after_last,
170            _ => 0,
171        }
172    }
173
174    /// Which row of its line's text `row` is, or `None` for a blank
175    /// (padding) row.
176    pub fn text_sub(&self, row: RowPos) -> Option<usize> {
177        let sub = row.sub.checked_sub(self.pad_before(row.line))?;
178        (sub < self.text_rows(row.line)).then_some(sub)
179    }
180
181    /// Whether `row` is a blank padding row.
182    pub fn is_filler(&self, row: RowPos) -> bool {
183        self.text_sub(row).is_none()
184    }
185
186    /// Lines that are not hidden. Sizes the scrollbar.
187    pub fn visible_line_count(&self) -> usize {
188        self.doc.line_count() - self.folds.hidden_count()
189    }
190
191    /// The `index`-th line that is not hidden (0-based), or the last one.
192    pub fn visible_line_at(&self, index: usize) -> usize {
193        self.folds.visible_line(index, self.doc.line_count())
194    }
195
196    /// `line`, or if it is hidden the shown line above it (a fold's header),
197    /// or below it when nothing above is shown (a filter can hide the first
198    /// lines).
199    fn shown_line(&self, line: usize) -> usize {
200        match self.folds.hidden_range(line) {
201            Some((start, end)) => start
202                .checked_sub(1)
203                .unwrap_or_else(|| (end + 1).min(self.doc.line_count() - 1)),
204            None => line,
205        }
206    }
207
208    /// How many visible lines come before `line` (which must be visible).
209    fn visible_index(&self, line: usize) -> usize {
210        line - self.folds.hidden_before(line)
211    }
212
213    fn next_visible_line(&self, line: usize) -> Option<usize> {
214        let next = match self.folds.hidden_range(line + 1) {
215            Some((_, end)) => end + 1,
216            None => line + 1,
217        };
218        (next < self.doc.line_count()).then_some(next)
219    }
220
221    fn prev_visible_line(&self, line: usize) -> Option<usize> {
222        let prev = line.checked_sub(1)?;
223        match self.folds.hidden_range(prev) {
224            Some((start, _)) => start.checked_sub(1),
225            None => Some(prev),
226        }
227    }
228
229    pub fn first_row(&self) -> RowPos {
230        RowPos::new(self.shown_line(0), 0)
231    }
232
233    pub fn last_row(&self) -> RowPos {
234        let line = self.shown_line(self.doc.line_count() - 1);
235        RowPos::new(line, self.rows_in_line(line) - 1)
236    }
237
238    /// The nearest real row: a line past the end becomes the last line, a
239    /// hidden line its fold's header, and a row past the line's last row
240    /// (after the text or the width changed) that last row.
241    pub fn clamp_row(&self, row: RowPos) -> RowPos {
242        let line = self.shown_line(row.line.min(self.doc.line_count() - 1));
243        RowPos::new(line, row.sub.min(self.rows_in_line(line) - 1))
244    }
245
246    /// The row `n` rows after `row` (before it for negative `n`), stopping at
247    /// the first and last rows, and how many rows it actually moved.
248    pub fn step(&self, row: RowPos, n: isize) -> (RowPos, isize) {
249        if !self.wraps() && self.padding.is_none() {
250            // One row per visible line: count in visible lines.
251            let from = self.visible_index(self.shown_line(row.line)) as isize;
252            let last = self.visible_line_count() as isize - 1;
253            let to = (from + n).clamp(0, last);
254            let line = self.folds.visible_line(to as usize, self.doc.line_count());
255            return (RowPos::new(line, 0), to - from);
256        }
257        let mut row = self.clamp_row(row);
258        let mut moved = 0;
259        while moved < n {
260            if row.sub + 1 < self.rows_in_line(row.line) {
261                row.sub += 1;
262            } else if let Some(line) = self.next_visible_line(row.line) {
263                row = RowPos::new(line, 0);
264            } else {
265                break;
266            }
267            moved += 1;
268        }
269        while moved > n {
270            if row.sub > 0 {
271                row.sub -= 1;
272            } else if let Some(line) = self.prev_visible_line(row.line) {
273                row = RowPos::new(line, self.rows_in_line(line) - 1);
274            } else {
275                break;
276            }
277            moved -= 1;
278        }
279        (row, moved)
280    }
281
282    /// The row `n` rows after `row`, clamped to the document.
283    pub fn advance(&self, row: RowPos, n: isize) -> RowPos {
284        self.step(row, n).0
285    }
286
287    /// The text shown on `row`, without the line break. A row that ends at a
288    /// wrap point ends where the next row starts.
289    pub fn row_range(&self, row: RowPos) -> Range<ByteOffset> {
290        let start = self.doc.line_start(row.line);
291        let end = self.doc.line_end(row.line);
292        let Some(sub) = self.text_sub(row) else {
293            // Padding: empty, before or after the line.
294            let at = if row.sub < self.pad_before(row.line) {
295                start
296            } else {
297                end
298            };
299            return at..at;
300        };
301        match self.wrapped(row.line) {
302            None => start..end,
303            Some(w) => {
304                let at = |i: usize| ByteOffset(start.0 + w.starts[i]);
305                let sub = sub.min(w.starts.len() - 1);
306                at(sub)..w.starts.get(sub + 1).map_or(end, |_| at(sub + 1))
307            }
308        }
309    }
310
311    /// Whether `row` is the last row of its line's text (padding rows count
312    /// as last).
313    pub fn is_last_in_line(&self, row: RowPos) -> bool {
314        self.text_sub(row)
315            .is_none_or(|s| s + 1 >= self.text_rows(row.line))
316    }
317
318    /// The last caret position on `row`. A position at a wrap point shows
319    /// at the start of the next row, so a row that wraps ends one grapheme
320    /// before it.
321    pub fn row_end(&self, row: RowPos) -> ByteOffset {
322        let range = self.row_range(row);
323        if self.is_last_in_line(row) {
324            range.end
325        } else {
326            prev_grapheme_boundary(self.doc, range.end).max(range.start)
327        }
328    }
329
330    /// The line's visual column where `row` starts, and the cells the row
331    /// is indented by on screen (continuation rows only).
332    pub fn row_cols(&self, row: RowPos) -> (usize, usize) {
333        match (self.wrapped(row.line), self.text_sub(row)) {
334            (Some(w), Some(sub)) if sub > 0 => {
335                let sub = sub.min(w.starts.len() - 1);
336                (w.start_cols[sub], w.indent)
337            }
338            _ => (0, 0),
339        }
340    }
341
342    /// Cells the text of `row` is indented by (continuation rows only).
343    pub fn row_indent(&self, row: RowPos) -> usize {
344        self.row_cols(row).1
345    }
346
347    /// The row showing `pos`; for a position hidden by a fold, the fold's
348    /// header row.
349    pub fn row_of(&self, pos: ByteOffset) -> RowPos {
350        let line = self.doc.line_of(pos);
351        let shown = self.shown_line(line);
352        if shown != line {
353            return RowPos::new(shown, self.rows_in_line(shown) - 1);
354        }
355        let sub = match self.wrapped(line) {
356            None => 0,
357            Some(w) => {
358                let off = pos.0 - self.doc.line_start(line).0;
359                w.starts.partition_point(|&s| s <= off) - 1
360            }
361        };
362        RowPos::new(line, self.pad_before(line) + sub)
363    }
364
365    /// The on-screen column of `pos` within its row, counting a
366    /// continuation row's indent, so that Up and Down keep carets above
367    /// each other.
368    pub fn col_in_row(&self, pos: ByteOffset) -> VisualCol {
369        let col = byte_to_visual_col(self.doc, pos, self.tab_width).0;
370        let (start_col, indent) = self.row_cols(self.row_of(pos));
371        VisualCol(indent + col - start_col)
372    }
373
374    /// The position at on-screen column `col` of `row`, or the row's end if
375    /// it is shorter.
376    pub fn byte_at_col(&self, row: RowPos, col: VisualCol) -> ByteOffset {
377        if self.is_filler(row) {
378            return self.row_range(row).start;
379        }
380        let (start_col, indent) = self.row_cols(row);
381        let target = if start_col > 0 || indent > 0 {
382            start_col + col.0.saturating_sub(indent)
383        } else {
384            col.0
385        };
386        let at = visual_col_to_byte(self.doc, row.line, VisualCol(target), self.tab_width);
387        at.clamp(self.row_range(row).start, self.row_end(row))
388    }
389
390    /// A scroll position as a number of visible lines from the top, for the
391    /// scrollbar: the visible lines above `row`'s line, plus the part of that
392    /// line scrolled past.
393    pub fn offset_of(&self, row: RowPos, frac: f32) -> f64 {
394        let rows = self.rows_in_line(row.line).max(1) as f64;
395        self.visible_index(self.shown_line(row.line)) as f64 + (row.sub as f64 + frac as f64) / rows
396    }
397
398    /// The inverse of [`offset_of`](Self::offset_of): the top row and fraction
399    /// for a scrollbar offset.
400    pub fn pos_at_offset(&self, offset: f64) -> (RowPos, f32) {
401        let max = self.visible_line_count().saturating_sub(1) as f64;
402        let offset = offset.clamp(0.0, max + 0.999_999);
403        let index = offset.floor();
404        let line = self
405            .folds
406            .visible_line(index as usize, self.doc.line_count());
407        let rows = self.rows_in_line(line).max(1) as f64;
408        let within = (offset - index) * rows;
409        let sub = (within.floor() as usize).min(self.rows_in_line(line).max(1) - 1);
410        (RowPos::new(line, sub), (within - sub as f64) as f32)
411    }
412
413    /// Scrolls a (top row, fraction) position by `rows`, which may be
414    /// fractional. Hitting the first row leaves no fraction; the caller
415    /// clamps the bottom with [`max_top`](Self::max_top).
416    pub fn scroll_by(&self, row: RowPos, frac: f32, rows: f64) -> (RowPos, f32) {
417        let total = frac as f64 + rows;
418        let whole = total.floor();
419        let (to, moved) = self.step(row, whole as isize);
420        if moved as f64 != whole {
421            // Stopped at the first or last row.
422            (to, 0.0)
423        } else {
424            (to, (total - whole) as f32)
425        }
426    }
427
428    /// The lowest top row that still fills a viewport of `rows_full` rows
429    /// (the first row when the document is shorter).
430    pub fn max_top(&self, rows_full: usize) -> RowPos {
431        self.advance(self.last_row(), -(rows_full.saturating_sub(1) as isize))
432    }
433
434    /// Rows from `top` (inclusive) down, at most `limit`.
435    pub fn rows_from(&self, top: RowPos, limit: usize) -> impl Iterator<Item = RowPos> + '_ {
436        let last = self.last_row();
437        std::iter::successors(Some(top), move |&r| (r < last).then(|| self.advance(r, 1)))
438            .take(limit)
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445    use crate::editor::Editor;
446
447    fn with_map(text: &str, wrap: Option<usize>, f: impl FnOnce(DisplayMap)) {
448        let mut ed = Editor::new();
449        let buffer = ed.add_buffer(text, Default::default());
450        let view = ed.add_view(buffer).unwrap();
451        ed.view_mut(view).unwrap().wrap_cols = wrap;
452        let v = ed.view(view).unwrap();
453        f(DisplayMap::new(ed.buffer(buffer).unwrap(), v));
454    }
455
456    #[test]
457    fn identity_rows_are_lines() {
458        with_map("a\nbb\n\tc", None, |m| {
459            assert_eq!(m.last_row(), RowPos::new(2, 0));
460            assert_eq!(m.row_of(ByteOffset(3)), RowPos::new(1, 0));
461            assert_eq!(m.row_range(RowPos::new(1, 0)), ByteOffset(2)..ByteOffset(4));
462            assert_eq!(m.step(RowPos::new(1, 0), 5), (RowPos::new(2, 0), 1));
463            assert_eq!(m.step(RowPos::new(1, 0), -5), (RowPos::new(0, 0), -1));
464            assert_eq!(m.col_in_row(ByteOffset(6)), VisualCol(4));
465            assert_eq!(
466                m.byte_at_col(RowPos::new(2, 0), VisualCol(2)),
467                ByteOffset(5)
468            );
469            let rows: Vec<_> = m.rows_from(RowPos::new(1, 0), 10).collect();
470            assert_eq!(rows, vec![RowPos::new(1, 0), RowPos::new(2, 0)]);
471        });
472    }
473
474    #[test]
475    fn scrolling_clamps_and_round_trips_offsets() {
476        with_map(&"x\n".repeat(99), None, |m| {
477            assert_eq!(m.max_top(10), RowPos::new(90, 0));
478            let (row, frac) = m.scroll_by(RowPos::new(5, 0), 0.25, 2.5);
479            assert_eq!((row, frac), (RowPos::new(7, 0), 0.75));
480            assert_eq!(
481                m.scroll_by(RowPos::new(1, 0), 0.5, -3.0),
482                (RowPos::new(0, 0), 0.0)
483            );
484            assert_eq!(m.offset_of(row, frac), 7.75);
485            assert_eq!(m.pos_at_offset(7.75), (row, frac));
486            assert_eq!(m.pos_at_offset(-1.0), (RowPos::new(0, 0), 0.0));
487        });
488    }
489
490    #[test]
491    fn wrapped_rows_step_and_map_positions() {
492        // "  aaaa bbbb cccc" at width 10: "  aaaa " / "bbbb " / "cccc",
493        // the continuation rows indented by 2.
494        with_map("x\n  aaaa bbbb cccc\ny", Some(10), |m| {
495            assert_eq!(m.rows_in_line(1), 3);
496            let line = 2; // start of line 1
497            assert_eq!(
498                m.row_range(RowPos::new(1, 1)),
499                ByteOffset(line + 7)..ByteOffset(line + 12)
500            );
501            assert_eq!(
502                m.row_of(ByteOffset(line + 7)),
503                RowPos::new(1, 1),
504                "wrap point"
505            );
506            assert_eq!(m.row_end(RowPos::new(1, 1)), ByteOffset(line + 11));
507            assert_eq!(m.row_end(RowPos::new(1, 2)), ByteOffset(line + 16));
508            assert_eq!(m.row_indent(RowPos::new(1, 2)), 2);
509            // The second "b" (line column 8) is at column 3 of its row,
510            // counting the indent.
511            assert_eq!(m.col_in_row(ByteOffset(line + 8)), VisualCol(3));
512            assert_eq!(
513                m.byte_at_col(RowPos::new(1, 2), VisualCol(3)),
514                ByteOffset(line + 13)
515            );
516            assert_eq!(m.step(RowPos::new(0, 0), 3), (RowPos::new(1, 2), 3));
517            assert_eq!(m.step(RowPos::new(2, 0), -2), (RowPos::new(1, 1), -2));
518            assert_eq!(m.last_row(), RowPos::new(2, 0));
519            assert_eq!(m.max_top(3), RowPos::new(1, 1));
520            assert_eq!(m.clamp_row(RowPos::new(1, 9)), RowPos::new(1, 2));
521        });
522    }
523}