Skip to main content

strop_core/buffer/
layout_cache.rs

1//! Per-buffer presentation cache. Edits keep only proven unchanged prefixes;
2//! worker results install only at their exact revision. Text remains the rope.
3use super::Buffer;
4use crate::id::{BufferRevision, DisplayColumn, LineIndex};
5use crate::layout::{
6    LayoutCheckpoint, LineLayoutIndex, PreparedLineLayout, RopeGraphemes, INLINE_LAYOUT_BYTES,
7};
8use crate::InputEdit;
9use std::cell::RefCell;
10use std::sync::Arc;
11
12struct CachedLine {
13    line: LineIndex,
14    index: Arc<LineLayoutIndex>,
15    valid: usize,
16}
17#[derive(Default)]
18pub(super) struct LineLayouts {
19    entries: RefCell<Vec<CachedLine>>,
20}
21impl Buffer {
22    pub fn prepare_line_layout(
23        &self,
24        line: LineIndex,
25        tab: usize,
26        cancelled: impl Fn() -> bool,
27    ) -> Option<PreparedLineLayout> {
28        let text = self
29            .text()
30            .byte_slice(self.line_start(line)..self.line_end(line));
31        if text.len_bytes() <= INLINE_LAYOUT_BYTES {
32            return None;
33        }
34        LineLayoutIndex::build(text, tab, cancelled).map(|index| PreparedLineLayout {
35            line,
36            index: Arc::new(index),
37        })
38    }
39
40    pub fn install_line_layouts(
41        &mut self,
42        revision: BufferRevision,
43        layouts: &[PreparedLineLayout],
44    ) -> bool {
45        if revision != self.revision() {
46            return false;
47        }
48        for layout in layouts {
49            if layout.line.get() >= self.len_lines()
50                || layout.index.bytes != self.line_end(layout.line) - self.line_start(layout.line)
51                || layout.index.checkpoints.is_empty()
52                || layout.index.checkpoints[0].byte.get() != 0
53                || layout.index.checkpoints[0].cell.get() != 0
54                || layout.index.checkpoints.windows(2).any(|points| {
55                    points[0].byte >= points[1].byte || points[0].cell > points[1].cell
56                })
57                || layout
58                    .index
59                    .checkpoints
60                    .last()
61                    .is_some_and(|point| point.byte.get() > layout.index.bytes)
62            {
63                return false;
64            }
65        }
66        let entries = self.line_layouts.entries.get_mut();
67        for layout in layouts {
68            let value = CachedLine {
69                line: layout.line,
70                index: layout.index.clone(),
71                valid: layout.index.checkpoints.len(),
72            };
73            if let Some(entry) = entries
74                .iter_mut()
75                .find(|entry| entry.line == layout.line && entry.index.tab == layout.index.tab)
76            {
77                *entry = value;
78            } else {
79                const CACHED_LINES: usize = 256;
80                if entries.len() == CACHED_LINES {
81                    entries.remove(0);
82                }
83                entries.push(value);
84            }
85        }
86        true
87    }
88
89    pub(super) fn invalidate_line_layouts(&mut self, edit: &InputEdit) {
90        let same_line = edit.start_point.0 == edit.old_end_point.0
91            && edit.start_point.0 == edit.new_end_point.0;
92        self.line_layouts.entries.get_mut().retain_mut(|entry| {
93            if entry.line.get() < edit.start_point.0 {
94                return true;
95            }
96            if !same_line {
97                return false;
98            }
99            if entry.line.get() == edit.start_point.0 {
100                entry.valid = entry.index.prefix_before(edit.start_point.1, entry.valid);
101            }
102            true
103        });
104    }
105
106    pub fn layout_checkpoint(
107        &self,
108        line: LineIndex,
109        origin: DisplayColumn,
110        tab: usize,
111    ) -> Option<LayoutCheckpoint> {
112        let entries = self.line_layouts.entries.borrow();
113        if let Some(entry) = entries
114            .iter()
115            .find(|entry| entry.line == line && entry.index.tab == tab.max(1))
116        {
117            let checkpoint = entry.index.at_cell(origin, entry.valid);
118            if entry.valid == entry.index.checkpoints.len()
119                || origin.get().saturating_sub(checkpoint.cell.get()) <= INLINE_LAYOUT_BYTES
120            {
121                return Some(checkpoint);
122            }
123        }
124        (origin.get() == 0 || self.line_end(line) - self.line_start(line) <= INLINE_LAYOUT_BYTES)
125            .then_some(LayoutCheckpoint::default())
126    }
127
128    pub fn try_cell_col_with_tab(&self, offset: usize, tab: usize) -> Option<DisplayColumn> {
129        self.column_from_layout(offset, tab, true)
130    }
131
132    pub(super) fn column_from_layout(
133        &self,
134        offset: usize,
135        tab: usize,
136        bounded: bool,
137    ) -> Option<DisplayColumn> {
138        let offset = offset.min(self.len_bytes());
139        let line = LineIndex::new(self.line_of(offset));
140        let start = self.line_start(line);
141        let text = self.text().byte_slice(start..self.line_end(line));
142        let byte = offset.saturating_sub(start).min(text.len_bytes());
143        let (point, indexed) = self
144            .line_layouts
145            .entries
146            .borrow()
147            .iter()
148            .find(|entry| entry.line == line && entry.index.tab == tab.max(1))
149            .map_or((LayoutCheckpoint::default(), false), |entry| {
150                (
151                    entry.index.at_byte(byte, entry.valid),
152                    entry.valid == entry.index.checkpoints.len(),
153                )
154            });
155        if bounded && !indexed && byte.saturating_sub(point.byte.get()) > INLINE_LAYOUT_BYTES {
156            return None;
157        }
158        let mut end = point.cell;
159        for (span, cluster) in RopeGraphemes::from_checkpoint(text, tab, point) {
160            if byte < span.byte + cluster.len() {
161                return Some(span.cell);
162            }
163            end = span.cell + span.width;
164        }
165        Some(end)
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::{id::ByteOffset, Range};
173
174    fn warm(buffer: &mut Buffer, line: usize, tab: usize) -> PreparedLineLayout {
175        let layout = buffer
176            .prepare_line_layout(LineIndex::new(line), tab, || false)
177            .unwrap();
178        assert!(buffer.install_line_layouts(buffer.revision(), std::slice::from_ref(&layout)));
179        layout
180    }
181
182    #[test]
183    fn indexed_unicode_cells_and_byte_offsets_match_the_frozen_line() {
184        let source = format!("{}\t็•Œ๐Ÿ‘ฉโ€๐Ÿ’ปe\u{301} tail", "ab".repeat(4096));
185        let mut buffer = Buffer::from_text(&source);
186        warm(&mut buffer, 0, 4);
187        let plain = Buffer::from_snapshot(buffer.snapshot());
188        for (byte, _) in source.char_indices().filter(|(byte, _)| *byte >= 8192) {
189            assert_eq!(
190                buffer.try_cell_col_with_tab(byte, 4),
191                Some(plain.cell_col_with_tab(byte, 4))
192            );
193        }
194        let origin = DisplayColumn::new(8196);
195        let checkpoint = buffer
196            .layout_checkpoint(LineIndex::new(0), origin, 4)
197            .unwrap();
198        let text = buffer.text().slice(..);
199        let indexed: Vec<_> = RopeGraphemes::from_checkpoint(text, 4, checkpoint)
200            .filter(|(span, _)| span.cell >= origin)
201            .map(|(span, text)| (span.byte, span.cell, text.into_owned()))
202            .collect();
203        let expected: Vec<_> = RopeGraphemes::new(text, 4)
204            .filter(|(span, _)| span.cell >= origin)
205            .map(|(span, text)| (span.byte, span.cell, text.into_owned()))
206            .collect();
207        assert_eq!(indexed, expected);
208    }
209
210    #[test]
211    fn edits_keep_only_proven_prefixes_and_reject_old_publications() {
212        let mut buffer = Buffer::from_text(&format!("{}\t็•Œend", "a".repeat(8192)));
213        let old_revision = buffer.revision();
214        let old = warm(&mut buffer, 0, 4);
215        buffer.edit().insert(ByteOffset::new(8196), "Z").unwrap();
216        assert!(!buffer.install_line_layouts(old_revision, &[old]));
217        let byte = buffer.len_bytes();
218        let fresh = Buffer::from_snapshot(buffer.snapshot());
219        assert_eq!(
220            buffer.try_cell_col_with_tab(byte, 4),
221            Some(fresh.cell_col_with_tab(byte, 4))
222        );
223        buffer
224            .edit()
225            .replace(Range::charwise(0usize, 1usize), "\t")
226            .unwrap();
227        // A distant changed prefix cannot silently reuse its former cells.
228        assert!(buffer.try_cell_col_with_tab(byte, 4).is_none());
229        warm(&mut buffer, 0, 4);
230        let fresh = Buffer::from_snapshot(buffer.snapshot());
231        assert_eq!(
232            buffer.try_cell_col_with_tab(byte, 4),
233            Some(fresh.cell_col_with_tab(byte, 4))
234        );
235    }
236}