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, tab_width: usize) -> Self {
92        let padding = view.padding.as_deref();
93        Self {
94            doc: buffer.doc(),
95            revision: buffer.revision(),
96            folds: if padding.is_some() {
97                &NO_FOLDS
98            } else {
99                &view.folds
100            },
101            wrap: view.wrap_cols.filter(|_| padding.is_none()),
102            cache: &view.wrap_cache,
103            padding,
104            tab_width,
105        }
106    }
107
108    pub fn doc(&self) -> &'a Document {
109        self.doc
110    }
111
112    /// Whether lines wrap.
113    pub fn wraps(&self) -> bool {
114        self.wrap.is_some()
115    }
116
117    /// How `line` wraps, or `None` when wrap is off.
118    fn wrapped(&self, line: usize) -> Option<Arc<Wrapped>> {
119        let cols = self.wrap?;
120        let mut cache = self.cache.borrow_mut();
121        let key = (cols, self.tab_width);
122        if cache.key != Some(key) || cache.lines.len() > WRAP_CACHE_LINES {
123            cache.key = Some(key);
124            cache.lines.clear();
125        }
126        if let Some(e) = cache.lines.get_mut(&line) {
127            if e.revision == self.revision {
128                return Some(e.wrapped.clone());
129            }
130            let text = self.doc.line_text(line);
131            if text == e.text {
132                e.revision = self.revision;
133                return Some(e.wrapped.clone());
134            }
135        }
136        let text = self.doc.line_text(line);
137        let wrapped = Arc::new(wrap_line(&text, cols, self.tab_width));
138        let entry = WrapEntry {
139            revision: self.revision,
140            text,
141            wrapped: wrapped.clone(),
142        };
143        cache.lines.insert(line, entry);
144        Some(wrapped)
145    }
146
147    /// Rows that `line` takes on screen: 0 for a hidden line.
148    pub fn rows_in_line(&self, line: usize) -> usize {
149        if self.folds.is_hidden(line) {
150            0
151        } else {
152            self.pad_before(line) + self.text_rows(line) + self.pad_after(line)
153        }
154    }
155
156    /// Rows of `line`'s text (not counting padding).
157    fn text_rows(&self, line: usize) -> usize {
158        self.wrapped(line).map_or(1, |w| w.starts.len())
159    }
160
161    fn pad_before(&self, line: usize) -> usize {
162        self.padding
163            .map_or(0, |p| p.before.get(line).copied().unwrap_or(0))
164    }
165
166    fn pad_after(&self, line: usize) -> usize {
167        match self.padding {
168            Some(p) if line + 1 == self.doc.line_count() => p.after_last,
169            _ => 0,
170        }
171    }
172
173    /// Which row of its line's text `row` is, or `None` for a blank
174    /// (padding) row.
175    pub fn text_sub(&self, row: RowPos) -> Option<usize> {
176        let sub = row.sub.checked_sub(self.pad_before(row.line))?;
177        (sub < self.text_rows(row.line)).then_some(sub)
178    }
179
180    /// Whether `row` is a blank padding row.
181    pub fn is_filler(&self, row: RowPos) -> bool {
182        self.text_sub(row).is_none()
183    }
184
185    /// Lines that are not hidden. Sizes the scrollbar.
186    pub fn visible_line_count(&self) -> usize {
187        self.doc.line_count() - self.folds.hidden_count()
188    }
189
190    /// `line`, or the fold header above it if it is hidden.
191    fn shown_line(&self, line: usize) -> usize {
192        match self.folds.hidden_range(line) {
193            Some((start, _)) => start - 1,
194            None => line,
195        }
196    }
197
198    /// How many visible lines come before `line` (which must be visible).
199    fn visible_index(&self, line: usize) -> usize {
200        line - self.folds.hidden_before(line)
201    }
202
203    fn next_visible_line(&self, line: usize) -> Option<usize> {
204        let next = match self.folds.hidden_range(line + 1) {
205            Some((_, end)) => end + 1,
206            None => line + 1,
207        };
208        (next < self.doc.line_count()).then_some(next)
209    }
210
211    fn prev_visible_line(&self, line: usize) -> Option<usize> {
212        let prev = line.checked_sub(1)?;
213        Some(self.shown_line(prev))
214    }
215
216    pub fn first_row(&self) -> RowPos {
217        RowPos::default()
218    }
219
220    pub fn last_row(&self) -> RowPos {
221        let line = self.shown_line(self.doc.line_count() - 1);
222        RowPos::new(line, self.rows_in_line(line) - 1)
223    }
224
225    /// The nearest real row: a line past the end becomes the last line, a
226    /// hidden line its fold's header, and a row past the line's last row
227    /// (after the text or the width changed) that last row.
228    pub fn clamp_row(&self, row: RowPos) -> RowPos {
229        let line = self.shown_line(row.line.min(self.doc.line_count() - 1));
230        RowPos::new(line, row.sub.min(self.rows_in_line(line) - 1))
231    }
232
233    /// The row `n` rows after `row` (before it for negative `n`), stopping at
234    /// the first and last rows, and how many rows it actually moved.
235    pub fn step(&self, row: RowPos, n: isize) -> (RowPos, isize) {
236        if !self.wraps() && self.padding.is_none() {
237            // One row per visible line: count in visible lines.
238            let from = self.visible_index(self.shown_line(row.line)) as isize;
239            let last = self.visible_line_count() as isize - 1;
240            let to = (from + n).clamp(0, last);
241            let line = self.folds.visible_line(to as usize, self.doc.line_count());
242            return (RowPos::new(line, 0), to - from);
243        }
244        let mut row = self.clamp_row(row);
245        let mut moved = 0;
246        while moved < n {
247            if row.sub + 1 < self.rows_in_line(row.line) {
248                row.sub += 1;
249            } else if let Some(line) = self.next_visible_line(row.line) {
250                row = RowPos::new(line, 0);
251            } else {
252                break;
253            }
254            moved += 1;
255        }
256        while moved > n {
257            if row.sub > 0 {
258                row.sub -= 1;
259            } else if let Some(line) = self.prev_visible_line(row.line) {
260                row = RowPos::new(line, self.rows_in_line(line) - 1);
261            } else {
262                break;
263            }
264            moved -= 1;
265        }
266        (row, moved)
267    }
268
269    /// The row `n` rows after `row`, clamped to the document.
270    pub fn advance(&self, row: RowPos, n: isize) -> RowPos {
271        self.step(row, n).0
272    }
273
274    /// The text shown on `row`, without the line break. A row that ends at a
275    /// wrap point ends where the next row starts.
276    pub fn row_range(&self, row: RowPos) -> Range<ByteOffset> {
277        let start = self.doc.line_start(row.line);
278        let end = self.doc.line_end(row.line);
279        let Some(sub) = self.text_sub(row) else {
280            // Padding: empty, before or after the line.
281            let at = if row.sub < self.pad_before(row.line) {
282                start
283            } else {
284                end
285            };
286            return at..at;
287        };
288        match self.wrapped(row.line) {
289            None => start..end,
290            Some(w) => {
291                let at = |i: usize| ByteOffset(start.0 + w.starts[i]);
292                let sub = sub.min(w.starts.len() - 1);
293                at(sub)..w.starts.get(sub + 1).map_or(end, |_| at(sub + 1))
294            }
295        }
296    }
297
298    /// Whether `row` is the last row of its line's text (padding rows count
299    /// as last).
300    pub fn is_last_in_line(&self, row: RowPos) -> bool {
301        self.text_sub(row)
302            .is_none_or(|s| s + 1 >= self.text_rows(row.line))
303    }
304
305    /// The last caret position on `row`. A position at a wrap point shows
306    /// at the start of the next row, so a row that wraps ends one grapheme
307    /// before it.
308    pub fn row_end(&self, row: RowPos) -> ByteOffset {
309        let range = self.row_range(row);
310        if self.is_last_in_line(row) {
311            range.end
312        } else {
313            prev_grapheme_boundary(self.doc, range.end).max(range.start)
314        }
315    }
316
317    /// The line's visual column where `row` starts, and the cells the row
318    /// is indented by on screen (continuation rows only).
319    pub fn row_cols(&self, row: RowPos) -> (usize, usize) {
320        match (self.wrapped(row.line), self.text_sub(row)) {
321            (Some(w), Some(sub)) if sub > 0 => {
322                let sub = sub.min(w.starts.len() - 1);
323                (w.start_cols[sub], w.indent)
324            }
325            _ => (0, 0),
326        }
327    }
328
329    /// Cells the text of `row` is indented by (continuation rows only).
330    pub fn row_indent(&self, row: RowPos) -> usize {
331        self.row_cols(row).1
332    }
333
334    /// The row showing `pos`; for a position hidden by a fold, the fold's
335    /// header row.
336    pub fn row_of(&self, pos: ByteOffset) -> RowPos {
337        let line = self.doc.line_of(pos);
338        let shown = self.shown_line(line);
339        if shown != line {
340            return RowPos::new(shown, self.rows_in_line(shown) - 1);
341        }
342        let sub = match self.wrapped(line) {
343            None => 0,
344            Some(w) => {
345                let off = pos.0 - self.doc.line_start(line).0;
346                w.starts.partition_point(|&s| s <= off) - 1
347            }
348        };
349        RowPos::new(line, self.pad_before(line) + sub)
350    }
351
352    /// The on-screen column of `pos` within its row, counting a
353    /// continuation row's indent, so that Up and Down keep carets above
354    /// each other.
355    pub fn col_in_row(&self, pos: ByteOffset) -> VisualCol {
356        let col = byte_to_visual_col(self.doc, pos, self.tab_width).0;
357        let (start_col, indent) = self.row_cols(self.row_of(pos));
358        VisualCol(indent + col - start_col)
359    }
360
361    /// The position at on-screen column `col` of `row`, or the row's end if
362    /// it is shorter.
363    pub fn byte_at_col(&self, row: RowPos, col: VisualCol) -> ByteOffset {
364        if self.is_filler(row) {
365            return self.row_range(row).start;
366        }
367        let (start_col, indent) = self.row_cols(row);
368        let target = if start_col > 0 || indent > 0 {
369            start_col + col.0.saturating_sub(indent)
370        } else {
371            col.0
372        };
373        let at = visual_col_to_byte(self.doc, row.line, VisualCol(target), self.tab_width);
374        at.clamp(self.row_range(row).start, self.row_end(row))
375    }
376
377    /// A scroll position as a number of visible lines from the top, for the
378    /// scrollbar: the visible lines above `row`'s line, plus the part of that
379    /// line scrolled past.
380    pub fn offset_of(&self, row: RowPos, frac: f32) -> f64 {
381        let rows = self.rows_in_line(row.line).max(1) as f64;
382        self.visible_index(self.shown_line(row.line)) as f64 + (row.sub as f64 + frac as f64) / rows
383    }
384
385    /// The inverse of [`offset_of`](Self::offset_of): the top row and fraction
386    /// for a scrollbar offset.
387    pub fn pos_at_offset(&self, offset: f64) -> (RowPos, f32) {
388        let max = self.visible_line_count().saturating_sub(1) as f64;
389        let offset = offset.clamp(0.0, max + 0.999_999);
390        let index = offset.floor();
391        let line = self
392            .folds
393            .visible_line(index as usize, self.doc.line_count());
394        let rows = self.rows_in_line(line).max(1) as f64;
395        let within = (offset - index) * rows;
396        let sub = (within.floor() as usize).min(self.rows_in_line(line).max(1) - 1);
397        (RowPos::new(line, sub), (within - sub as f64) as f32)
398    }
399
400    /// Scrolls a (top row, fraction) position by `rows`, which may be
401    /// fractional. Hitting the first row leaves no fraction; the caller
402    /// clamps the bottom with [`max_top`](Self::max_top).
403    pub fn scroll_by(&self, row: RowPos, frac: f32, rows: f64) -> (RowPos, f32) {
404        let total = frac as f64 + rows;
405        let whole = total.floor();
406        let (to, moved) = self.step(row, whole as isize);
407        if moved as f64 != whole {
408            // Stopped at the first or last row.
409            (to, 0.0)
410        } else {
411            (to, (total - whole) as f32)
412        }
413    }
414
415    /// The lowest top row that still fills a viewport of `rows_full` rows
416    /// (the first row when the document is shorter).
417    pub fn max_top(&self, rows_full: usize) -> RowPos {
418        self.advance(self.last_row(), -(rows_full.saturating_sub(1) as isize))
419    }
420
421    /// Rows from `top` (inclusive) down, at most `limit`.
422    pub fn rows_from(&self, top: RowPos, limit: usize) -> impl Iterator<Item = RowPos> + '_ {
423        let last = self.last_row();
424        std::iter::successors(Some(top), move |&r| (r < last).then(|| self.advance(r, 1)))
425            .take(limit)
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432    use crate::editor::Editor;
433
434    fn with_map(text: &str, wrap: Option<usize>, f: impl FnOnce(DisplayMap)) {
435        let mut ed = Editor::new();
436        let buffer = ed.add_buffer(text, Default::default());
437        let view = ed.add_view(buffer).unwrap();
438        ed.view_mut(view).unwrap().wrap_cols = wrap;
439        let v = ed.view(view).unwrap();
440        f(DisplayMap::new(ed.buffer(buffer).unwrap(), v, 4));
441    }
442
443    #[test]
444    fn identity_rows_are_lines() {
445        with_map("a\nbb\n\tc", None, |m| {
446            assert_eq!(m.last_row(), RowPos::new(2, 0));
447            assert_eq!(m.row_of(ByteOffset(3)), RowPos::new(1, 0));
448            assert_eq!(m.row_range(RowPos::new(1, 0)), ByteOffset(2)..ByteOffset(4));
449            assert_eq!(m.step(RowPos::new(1, 0), 5), (RowPos::new(2, 0), 1));
450            assert_eq!(m.step(RowPos::new(1, 0), -5), (RowPos::new(0, 0), -1));
451            assert_eq!(m.col_in_row(ByteOffset(6)), VisualCol(4));
452            assert_eq!(
453                m.byte_at_col(RowPos::new(2, 0), VisualCol(2)),
454                ByteOffset(5)
455            );
456            let rows: Vec<_> = m.rows_from(RowPos::new(1, 0), 10).collect();
457            assert_eq!(rows, vec![RowPos::new(1, 0), RowPos::new(2, 0)]);
458        });
459    }
460
461    #[test]
462    fn scrolling_clamps_and_round_trips_offsets() {
463        with_map(&"x\n".repeat(99), None, |m| {
464            assert_eq!(m.max_top(10), RowPos::new(90, 0));
465            let (row, frac) = m.scroll_by(RowPos::new(5, 0), 0.25, 2.5);
466            assert_eq!((row, frac), (RowPos::new(7, 0), 0.75));
467            assert_eq!(
468                m.scroll_by(RowPos::new(1, 0), 0.5, -3.0),
469                (RowPos::new(0, 0), 0.0)
470            );
471            assert_eq!(m.offset_of(row, frac), 7.75);
472            assert_eq!(m.pos_at_offset(7.75), (row, frac));
473            assert_eq!(m.pos_at_offset(-1.0), (RowPos::new(0, 0), 0.0));
474        });
475    }
476
477    #[test]
478    fn wrapped_rows_step_and_map_positions() {
479        // "  aaaa bbbb cccc" at width 10: "  aaaa " / "bbbb " / "cccc",
480        // the continuation rows indented by 2.
481        with_map("x\n  aaaa bbbb cccc\ny", Some(10), |m| {
482            assert_eq!(m.rows_in_line(1), 3);
483            let line = 2; // start of line 1
484            assert_eq!(
485                m.row_range(RowPos::new(1, 1)),
486                ByteOffset(line + 7)..ByteOffset(line + 12)
487            );
488            assert_eq!(
489                m.row_of(ByteOffset(line + 7)),
490                RowPos::new(1, 1),
491                "wrap point"
492            );
493            assert_eq!(m.row_end(RowPos::new(1, 1)), ByteOffset(line + 11));
494            assert_eq!(m.row_end(RowPos::new(1, 2)), ByteOffset(line + 16));
495            assert_eq!(m.row_indent(RowPos::new(1, 2)), 2);
496            // The second "b" (line column 8) is at column 3 of its row,
497            // counting the indent.
498            assert_eq!(m.col_in_row(ByteOffset(line + 8)), VisualCol(3));
499            assert_eq!(
500                m.byte_at_col(RowPos::new(1, 2), VisualCol(3)),
501                ByteOffset(line + 13)
502            );
503            assert_eq!(m.step(RowPos::new(0, 0), 3), (RowPos::new(1, 2), 3));
504            assert_eq!(m.step(RowPos::new(2, 0), -2), (RowPos::new(1, 1), -2));
505            assert_eq!(m.last_row(), RowPos::new(2, 0));
506            assert_eq!(m.max_top(3), RowPos::new(1, 1));
507            assert_eq!(m.clamp_row(RowPos::new(1, 9)), RowPos::new(1, 2));
508        });
509    }
510}