Skip to main content

ui/
code_long.rs

1// code_long -- what ui.code_editor (code_edit.rs) keeps of the long lines it shows: checkpoints
2// of their columns and their tokens, so that a pass looks at the part of a line in view and an
3// edit works again only near it
4//
5//   Longs::col / byte / floor     columns (code_doc.rs: col_from, byte_from, cell_floor) from
6//                                 the checkpoint before the place
7//   Longs::tokens                 the tokens of a long line (highlight.rs) and its end state
8//
9// A line of COL_LONG bytes or more gets checkpoints (byte, column) at cluster boundaries about
10// every COL_STEP bytes and keeps its tokens with places outside every construct about every
11// TOK_STEP bytes.  The entries follow the document's change journal (lines that moved keep
12// theirs); a line changed in place keeps its text of before, and the next look at it compares:
13// the columns are walked again from the checkpoint before the change to where they meet the
14// old ones again (moved along after that, a tab evens out a shift of the columns), the tokens
15// are scanned again from a place TOK_REACH bytes before the change (rules look ahead) to the
16// first place after it where the scan is where the old scan was.  A construct that looks
17// further ahead than TOK_REACH (Markdown emphasis waiting for its closer) is seen again when the
18// whole line is scanned: a change of its start state, another grammar.
19
20use alloc::rc::Rc;
21use alloc::string::String;
22use alloc::vec::Vec;
23use core::cell::RefCell;
24
25use crate::code_doc::*;
26use crate::highlight::*;
27
28/// a line this long (bytes) gets checkpoints of its columns and keeps its tokens
29pub(crate) const COL_LONG: usize = 4096;
30// the bytes between two column checkpoints, between two token places
31const COL_STEP: usize = 1024;
32const TOK_STEP: usize = 1024;
33// how far before a change a scan of the tokens starts again
34const TOK_REACH: usize = 4096;
35// long lines kept (more than a pass looks at), the bytes they take at most
36const LONG_KEEP: usize = 256;
37const LONG_BYTES: usize = 256 << 20;
38
39// the checkpoints of a long line: (byte, column) at cluster boundaries about every COL_STEP
40// bytes, (0, 0) first
41fn col_marks(t: &str, tw: usize) -> Vec<(usize, usize)> {
42    let mut out = alloc::vec![(0, 0)];
43    col_walk(t, 0, 0, tw, &mut out, &mut |_, _| None);
44    out
45}
46
47// walk t's clusters from boundary i at column col on, a checkpoint into out about every
48// COL_STEP bytes; meet(a, b) sees the boundaries a..b of each step (an ASCII run: every byte, a
49// cluster: a alone, b = a + 1), Some(d) stops at boundary a + d -> (where it stopped, the
50// column there)
51fn col_walk(t: &str, i: usize, col: usize, tw: usize, out: &mut Vec<(usize, usize)>, meet: &mut dyn FnMut(usize, usize) -> Option<usize>) -> Option<(usize, usize)> {
52    let tw = tw.clamp(1, 16);
53    let b = t.as_bytes();
54    let (mut i, mut col) = (i, col);
55    let mut next = out.last().map_or(0, |m| m.0) + COL_STEP;
56    while i < t.len() {
57        let k = ascii_run(b, i, t.len());
58        let (n, w) = if k > 0 { (i + k, k) } else { cell(t, i, col, tw) };
59        if let Some(d) = meet(i, if k > 0 { n } else { i + 1 }) {
60            return Some((i + d, col + d));
61        }
62        // (every byte inside an ASCII run is a boundary)
63        while k > 0 && next < n {
64            out.push((next, col + (next - i)));
65            next += COL_STEP;
66        }
67        (i, col) = (n, col + w);
68        if i >= next && i < t.len() {
69            out.push((i, col));
70            next = i + COL_STEP;
71        }
72    }
73    None
74}
75
76// the checkpoints of line `new` after a change of `old` (marks: its checkpoints; p bytes at the
77// start and s at the end alike): kept before the change, walked again to the first old one past
78// it the walk meets, the rest moved along -- by the shift of the columns there, after the next
79// tab by the shift that tab leaves (a multiple of the tab width, the same to the end)
80fn col_marks_edit(old: &str, marks: &[(usize, usize)], new: &str, p: usize, s: usize, tw: usize) -> Vec<(usize, usize)> {
81    let tw = tw.clamp(1, 16);
82    let delta = new.len() as isize - old.len() as isize;
83    // (a character or cluster at the change may join what is around it)
84    let k = marks.partition_point(|m| m.0 + 16 < p).max(1);
85    let mut out = marks[..k].to_vec();
86    let (b0, c0) = out[k - 1];
87    // (the old checkpoints past the change, where they are in new)
88    let from = new.len() - s + 16;
89    let at = |j: usize| (marks[j].0 as isize + delta) as usize;
90    let mut j = marks.partition_point(|m| (m.0 as isize + delta) < from as isize);
91    let met = col_walk(new, b0, c0, tw, &mut out, &mut |a, b| {
92        while j < marks.len() && at(j) < a.max(from) {
93            j += 1;
94        }
95        (j < marks.len() && at(j) < b).then(|| at(j) - a)
96    });
97    let Some((y, cy)) = met else { return out };
98    out.retain(|m| m.0 < y);
99    let d = cy as isize - marks[j].1 as isize;
100    // the first tab from there: after it the shift it leaves
101    let tab = new[y..].find('\t').map(|t| y + t);
102    let d1 = tab.map_or(d, |t| {
103        let a = col_from(old, marks[j].0, marks[j].1, (t as isize - delta) as usize, tw) as isize;
104        let (wn, wo) = (tw as isize - (a + d).rem_euclid(tw as isize), tw as isize - a.rem_euclid(tw as isize));
105        d + wn - wo
106    });
107    for m in &marks[j..] {
108        let x = (m.0 as isize + delta) as usize;
109        let sh = if tab.is_some_and(|t| x > t) { d1 } else { d };
110        out.push((x, (m.1 as isize + sh) as usize));
111    }
112    out
113}
114
115// the tokens of a long line: from start state st with highlighter h; the state at its end;
116// places outside every construct about every TOK_STEP bytes (byte, the tokens before it, the
117// end of the last of them then: a later token of its kind may have joined it)
118struct Toks {
119    h: Highlighter,
120    st: u32,
121    end: u32,
122    toks: Rc<Vec<Token>>,
123    marks: Vec<(usize, usize, usize)>,
124}
125
126impl Toks {
127    // scan a whole line
128    fn scan(h: Highlighter, t: &str, st: u32) -> Toks {
129        let mut toks = Vec::new();
130        let mut marks = alloc::vec![(0, 0, 0)];
131        let (_, end) = h.line_part(t, st, 0, &mut toks, &mut |i, out| {
132            if i >= marks.last().map_or(0, |m| m.0) + TOK_STEP {
133                marks.push((i, out.len(), out.last().map_or(0, |x| x.end)));
134            }
135            false
136        });
137        Toks { h, st, end: end.unwrap_or(0), toks: Rc::new(toks), marks }
138    }
139
140    // the line changed from old to new (p bytes at the start and s at the end alike): scan
141    // again from a place TOK_REACH before the change to where the scan meets the old one
142    fn edit(&mut self, old: &str, new: &str, p: usize, s: usize) {
143        let delta = new.len() as isize - old.len() as isize;
144        let k = self.marks.partition_point(|m| m.0 + TOK_REACH <= p).max(1) - 1;
145        let (r, rk, rend) = self.marks[k];
146        let toks = Rc::make_mut(&mut self.toks);
147        // the token before r as it was there (a token from r on may join it again)
148        let head = rk.saturating_sub(1);
149        let mut mid: Vec<Token> = toks[head..rk].iter().map(|x| Token { end: rend, ..*x }).collect();
150        let base = head;
151        // the scans may meet past the change (the character before the place too) and past the
152        // first non-blank of both lines (rules at the line start)
153        let bol = |t: &str| t.len() - t.trim_start_matches([' ', '\t']).len();
154        let from = (new.len() - s + 4).max(bol(new) + 1).max((bol(old) as isize + delta + 1).max(0) as usize);
155        let om = &self.marks;
156        let mut j = om.partition_point(|m| (m.0 as isize + delta) < from as isize);
157        let mut marks: Vec<(usize, usize, usize)> = self.marks[..=k].to_vec();
158        let mut met = None;
159        let (_, end) = self.h.line_part(new, self.st, r, &mut mid, &mut |i, out| {
160            if i >= from {
161                while j < om.len() && (om[j].0 as isize + delta) < i as isize {
162                    j += 1;
163                }
164                if let Some(&(_, ok, oend)) = om.get(j).filter(|m| (m.0 as isize + delta) as usize == i) {
165                    // the old scan was here too, with the same token before it: the same from here
166                    let same = match (out.last(), ok.checked_sub(1).map(|q| toks[q])) {
167                        (Some(a), Some(b)) => a.kind == b.kind && a.end as isize == oend as isize + delta,
168                        (None, None) => true,
169                        _ => false,
170                    };
171                    if same {
172                        met = Some(j);
173                        return true;
174                    }
175                }
176            }
177            if i > r && i >= marks.last().map_or(0, |m| m.0) + TOK_STEP {
178                marks.push((i, base + out.len(), out.last().map_or(0, |x| x.end)));
179            }
180            false
181        });
182        match met {
183            Some(j) => {
184                let (_, ok, _) = self.marks[j];
185                // the token before the meeting place grew as the old one did
186                if let (Some(a), Some(q)) = (mid.last_mut(), ok.checked_sub(1)) {
187                    a.end = (toks[q].end as isize + delta) as usize;
188                }
189                let nk = base + mid.len();
190                toks.splice(head..ok, mid);
191                for x in toks[nk..].iter_mut() {
192                    x.start = (x.start as isize + delta) as usize;
193                    x.end = (x.end as isize + delta) as usize;
194                }
195                let sh = nk as isize - ok as isize;
196                for m in &self.marks[j..] {
197                    // (no token before it: the end stays 0)
198                    let e = if m.1 > 0 { (m.2 as isize + delta) as usize } else { 0 };
199                    marks.push(((m.0 as isize + delta) as usize, (m.1 as isize + sh) as usize, e));
200                }
201            }
202            None => {
203                toks.truncate(head);
204                toks.extend(mid);
205                self.end = end.unwrap_or(0);
206            }
207        }
208        self.marks = marks;
209    }
210}
211
212// what is kept of one long line; text: the line these are of (checked: it is the line's at the
213// cache's revision)
214struct LongLine {
215    line: usize,
216    text: String,
217    checked: bool,
218    marks: Option<Vec<(usize, usize)>>,
219    toks: Option<Toks>,
220}
221
222impl LongLine {
223    // the bytes it takes (about)
224    fn size(&self) -> usize {
225        let t = self.toks.as_ref().map_or(0, |k| k.toks.len() * core::mem::size_of::<Token>() + k.marks.len() * 24);
226        self.text.len() + self.marks.as_ref().map_or(0, |m| m.len() * 16) + t
227    }
228}
229
230#[derive(Default)]
231struct Long {
232    uid: u32,
233    rev: u64,
234    tw: usize,
235    lines: Vec<LongLine>,
236}
237
238/// the long lines of a code editor (see the module header)
239#[derive(Default)]
240pub(crate) struct Longs(RefCell<Long>);
241
242// the bytes a and b have alike at the start, and then at the end (not into the start)
243fn alike(a: &str, b: &str) -> (usize, usize) {
244    let (x, y) = (a.as_bytes(), b.as_bytes());
245    let m = x.len().min(y.len());
246    // (blocks first: slices compare fast)
247    let mut p = 0;
248    while p + 64 <= m && x[p..p + 64] == y[p..p + 64] {
249        p += 64;
250    }
251    while p < m && x[p] == y[p] {
252        p += 1;
253    }
254    let mut s = 0;
255    while s + 64 <= m - p && x[x.len() - s - 64..x.len() - s] == y[y.len() - s - 64..y.len() - s] {
256        s += 64;
257    }
258    while s < m - p && x[x.len() - s - 1] == y[y.len() - s - 1] {
259        s += 1;
260    }
261    (p, s)
262}
263
264/// (tests) line `text` changed by `edits` one after the other (bytes a..b of the line then
265/// replaced by s): the column checkpoints and the tokens a code editor keeps of a long line,
266/// brought along through each change, against a fresh look at the line -> what differs (empty:
267/// nothing)
268pub fn code_long_check(text: &str, h: &Highlighter, tw: u8, edits: &[(usize, usize, &str)]) -> Vec<String> {
269    let tw = tw.clamp(1, 16) as usize;
270    let mut t = String::from(text);
271    let mut marks = col_marks(&t, tw);
272    let mut tk = Toks::scan(*h, &t, 0);
273    let mut out = Vec::new();
274    // (a place inside a character: its start)
275    let floor = |t: &str, mut i: usize| {
276        i = i.min(t.len());
277        while !t.is_char_boundary(i) {
278            i -= 1;
279        }
280        i
281    };
282    for (k, &(a, b, s)) in edits.iter().enumerate() {
283        let a = floor(&t, a);
284        let b = floor(&t, b).max(a);
285        let n = alloc::format!("{}{}{}", &t[..a], s, &t[b..]);
286        let (p, q) = alike(&t, &n);
287        marks = col_marks_edit(&t, &marks, &n, p, q, tw);
288        tk.edit(&t, &n, p, q);
289        t = n;
290        // every checkpoint a cluster boundary with its column, in order
291        let bounds = text::segment::graphemes(&t);
292        if let Some(m) = marks.iter().find(|m| bounds.binary_search(&m.0).is_err() || col_in(&t, m.0, tw) != m.1) {
293            out.push(alloc::format!("edit {k}: checkpoint {m:?}: column {}", col_in(&t, m.0, tw)));
294        }
295        if marks.windows(2).any(|w| w[0].0 >= w[1].0) {
296            out.push(alloc::format!("edit {k}: checkpoints out of order"));
297        }
298        // the tokens and end state of a whole scan; every place kept one that scan passes
299        let mut seen = Vec::new();
300        let mut fresh = Vec::new();
301        let (_, end) = h.line_part(&t, 0, 0, &mut fresh, &mut |i, o| {
302            seen.push((i, o.len(), o.last().map_or(0, |x| x.end)));
303            false
304        });
305        if *tk.toks != fresh || tk.end != end.unwrap_or(0) {
306            let at = tk.toks.iter().zip(&fresh).position(|(x, y)| x != y).unwrap_or(fresh.len().min(tk.toks.len()));
307            out.push(alloc::format!("edit {k}: tokens differ from {at}: {:?} / {:?}", tk.toks.get(at), fresh.get(at)));
308        }
309        if let Some(m) = tk.marks.iter().skip(1).find(|m| seen.binary_search(m).is_err()) {
310            out.push(alloc::format!("edit {k}: token place {m:?} not one of a whole scan"));
311        }
312    }
313    out
314}
315
316impl Longs {
317    // f with the entry of a long line, brought up to the document's revision
318    fn with<R>(&self, doc: &CodeDoc, line: usize, tw: usize, f: impl FnOnce(&mut LongLine, &str) -> R) -> R {
319        let mut s = self.0.borrow_mut();
320        let same = s.uid == doc.uid() && s.tw == tw;
321        if !same || s.rev != doc.revision() {
322            match doc.changes_since(s.rev).filter(|_| same) {
323                Some(cs) => {
324                    for c in cs {
325                        // lines changed in place keep their entries (compared at the next look)
326                        s.lines.retain_mut(|e| {
327                            if e.line >= c.first + c.old {
328                                e.line = e.line - c.old + c.new;
329                                true
330                            } else if e.line >= c.first && c.old == c.new {
331                                e.checked = false;
332                                true
333                            } else {
334                                e.line < c.first
335                            }
336                        });
337                    }
338                }
339                None => s.lines.clear(),
340            }
341            (s.uid, s.rev, s.tw) = (doc.uid(), doc.revision(), tw);
342        }
343        let t = doc.line(line);
344        let k = match s.lines.iter().position(|e| e.line == line) {
345            Some(k) => k,
346            None => {
347                // (the oldest go)
348                let mut bytes: usize = s.lines.iter().map(|e| e.size()).sum::<usize>() + t.len();
349                let mut k = 0;
350                while k < s.lines.len() && (s.lines.len() - k >= LONG_KEEP || bytes > LONG_BYTES) {
351                    bytes -= s.lines[k].size();
352                    k += 1;
353                }
354                s.lines.drain(..k);
355                s.lines.push(LongLine { line, text: String::from(t), checked: true, marks: None, toks: None });
356                s.lines.len() - 1
357            }
358        };
359        let e = &mut s.lines[k];
360        if !e.checked {
361            e.checked = true;
362            if e.text != t {
363                let (p, q) = alike(&e.text, t);
364                if let Some(m) = e.marks.take() {
365                    e.marks = Some(col_marks_edit(&e.text, &m, t, p, q, tw));
366                }
367                if let Some(tk) = e.toks.as_mut() {
368                    tk.edit(&e.text, t, p, q);
369                }
370                e.text.clear();
371                e.text.push_str(t);
372            }
373        }
374        f(e, t)
375    }
376
377    // the checkpoint of a long line at or before byte x (by_col: column x)
378    fn mark(&self, doc: &CodeDoc, line: usize, tw: usize, x: usize, by_col: bool) -> (usize, usize) {
379        if doc.line(line).len() < COL_LONG {
380            return (0, 0);
381        }
382        self.with(doc, line, tw, |e, t| {
383            let m = e.marks.get_or_insert_with(|| col_marks(t, tw));
384            m[m.partition_point(|p| if by_col { p.1 <= x } else { p.0 <= x }).saturating_sub(1)]
385        })
386    }
387
388    /// the column of byte on a line
389    pub(crate) fn col(&self, doc: &CodeDoc, line: usize, byte: usize, tw: usize) -> usize {
390        let (b, c) = self.mark(doc, line, tw, byte, false);
391        col_from(doc.line(line), b, c, byte, tw)
392    }
393
394    /// the cluster boundary nearest to column col
395    pub(crate) fn byte(&self, doc: &CodeDoc, line: usize, col: usize, tw: usize) -> usize {
396        let (b, c) = self.mark(doc, line, tw, col, true);
397        byte_from(doc.line(line), b, c, col, tw)
398    }
399
400    /// the cluster boundary at or before column col and its column
401    pub(crate) fn floor(&self, doc: &CodeDoc, line: usize, col: usize, tw: usize) -> (usize, usize) {
402        let (b, c) = self.mark(doc, line, tw, col, true);
403        cell_floor(doc.line(line), b, c, col, tw)
404    }
405
406    /// the tokens of a long line from start state st -> (the state at its end, the tokens)
407    pub(crate) fn tokens(&self, doc: &CodeDoc, line: usize, tw: usize, h: &Highlighter, st: u32) -> (u32, Rc<Vec<Token>>) {
408        self.with(doc, line, tw, |e, t| {
409            if !e.toks.as_ref().is_some_and(|k| k.st == st && core::ptr::eq(k.h.grammar(), h.grammar())) {
410                e.toks = Some(Toks::scan(*h, t, st));
411            }
412            match &e.toks {
413                Some(k) => (k.end, k.toks.clone()),
414                None => (0, Rc::default()),
415            }
416        })
417    }
418}