Skip to main content

ochrepad_core/
position.rs

1//! Position newtypes and the conversions between them.
2//!
3//! This is the only module that knows about tab stops, grapheme clusters and
4//! display widths. The canonical position everywhere else is [`ByteOffset`].
5
6use serde::{Deserialize, Serialize};
7use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete, UnicodeSegmentation};
8use unicode_width::UnicodeWidthStr;
9
10use crate::text::Document;
11
12/// Byte offset into the document's UTF-8 text, always on a char boundary.
13#[derive(
14    Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
15)]
16pub struct ByteOffset(pub usize);
17
18/// Char index; only used at the boundary with Ropey 1.x.
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct CharIdx(pub usize);
21
22/// A line and a byte column within that line.
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub struct LineCol {
25    pub line: usize,
26    pub col: ByteOffset,
27}
28
29/// On-screen cell column, after tab expansion and wide-glyph widths.
30#[derive(
31    Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
32)]
33pub struct VisualCol(pub usize);
34
35pub fn byte_to_line_col(doc: &Document, pos: ByteOffset) -> LineCol {
36    let line = doc.line_of(pos);
37    LineCol {
38        line,
39        col: ByteOffset(pos.0 - doc.line_start(line).0),
40    }
41}
42
43/// Clamps the column to the end of the line. The column must land on a char
44/// boundary; out-of-range lines clamp to the end of the document.
45pub fn line_col_to_byte(doc: &Document, lc: LineCol) -> ByteOffset {
46    if lc.line >= doc.line_count() {
47        return doc.len();
48    }
49    let start = doc.line_start(lc.line);
50    ByteOffset((start.0 + lc.col.0).min(doc.line_end(lc.line).0))
51}
52
53/// The next grapheme boundary after `pos` (or the end of the document).
54pub fn next_grapheme_boundary(doc: &Document, pos: ByteOffset) -> ByteOffset {
55    let len = doc.len();
56    if pos >= len {
57        return len;
58    }
59    let (mut chunk, mut chunk_start) = doc.chunk_at(pos);
60    let mut gc = GraphemeCursor::new(pos.0, len.0, true);
61    loop {
62        match gc.next_boundary(chunk, chunk_start.0) {
63            Ok(Some(n)) => return ByteOffset(n),
64            Ok(None) => return len,
65            Err(GraphemeIncomplete::NextChunk) => {
66                chunk_start = ByteOffset(chunk_start.0 + chunk.len());
67                chunk = doc.chunk_at(chunk_start).0;
68            }
69            Err(GraphemeIncomplete::PreContext(n)) => provide_context(doc, &mut gc, n),
70            Err(_) => return step_char_forward(doc, pos),
71        }
72    }
73}
74
75/// The previous grapheme boundary before `pos` (or 0).
76pub fn prev_grapheme_boundary(doc: &Document, pos: ByteOffset) -> ByteOffset {
77    if pos.0 == 0 {
78        return pos;
79    }
80    let (mut chunk, mut chunk_start) = doc.chunk_at(pos);
81    let mut gc = GraphemeCursor::new(pos.0, doc.len().0, true);
82    loop {
83        match gc.prev_boundary(chunk, chunk_start.0) {
84            Ok(Some(n)) => return ByteOffset(n),
85            Ok(None) => return ByteOffset(0),
86            Err(GraphemeIncomplete::PrevChunk) => {
87                (chunk, chunk_start) = doc.chunk_at(ByteOffset(chunk_start.0 - 1));
88            }
89            Err(GraphemeIncomplete::PreContext(n)) => provide_context(doc, &mut gc, n),
90            Err(_) => return step_char_backward(doc, pos),
91        }
92    }
93}
94
95fn provide_context(doc: &Document, gc: &mut GraphemeCursor, n: usize) {
96    let (ctx, ctx_start) = doc.chunk_at(ByteOffset(n - 1));
97    gc.provide_context(&ctx[..n - ctx_start.0], ctx_start.0);
98}
99
100fn step_char_forward(doc: &Document, pos: ByteOffset) -> ByteOffset {
101    ByteOffset(pos.0 + doc.char_at(pos).map_or(0, char::len_utf8))
102}
103
104fn step_char_backward(doc: &Document, pos: ByteOffset) -> ByteOffset {
105    let mut p = pos.0.saturating_sub(1);
106    while p > 0 && !doc.is_char_boundary(ByteOffset(p)) {
107        p -= 1;
108    }
109    ByteOffset(p)
110}
111
112/// Display width of one grapheme starting at visual column `col`. Tabs expand
113/// to the next tab stop; every other grapheme takes at least one cell so that
114/// distinct boundaries always have distinct visual columns.
115pub fn grapheme_width(g: &str, col: usize, tab_width: usize) -> usize {
116    if g == "\t" {
117        let tw = tab_width.max(1);
118        tw - col % tw
119    } else {
120        g.width().max(1)
121    }
122}
123
124/// Width of a string of graphemes that starts at visual column `start`.
125fn advance(s: &str, start: usize, tab_width: usize) -> usize {
126    s.graphemes(true)
127        .fold(start, |col, g| col + grapheme_width(g, col, tab_width))
128}
129
130pub fn byte_to_visual_col(doc: &Document, pos: ByteOffset, tab_width: usize) -> VisualCol {
131    let line_start = doc.line_start(doc.line_of(pos));
132    VisualCol(advance(&doc.slice(line_start..pos), 0, tab_width))
133}
134
135/// The last grapheme boundary on `line` whose visual column is `<= col`, or the
136/// line end if the line is shorter (no virtual space).
137pub fn visual_col_to_byte(
138    doc: &Document,
139    line: usize,
140    col: VisualCol,
141    tab_width: usize,
142) -> ByteOffset {
143    let start = doc.line_start(line);
144    let text = doc.line_text(line);
145    let mut c = 0;
146    for (i, g) in text.grapheme_indices(true) {
147        let w = grapheme_width(g, c, tab_width);
148        if c + w > col.0 {
149            return ByteOffset(start.0 + i);
150        }
151        c += w;
152    }
153    ByteOffset(start.0 + text.len())
154}
155
156/// The first grapheme boundary on `line` whose visual column is `>= col`, or
157/// the line end. Used for the left edge of a rectangle so that a tab or wide
158/// glyph straddling the edge stays outside it.
159pub fn visual_col_to_byte_ceil(
160    doc: &Document,
161    line: usize,
162    col: VisualCol,
163    tab_width: usize,
164) -> ByteOffset {
165    let start = doc.line_start(line);
166    let text = doc.line_text(line);
167    let mut c = 0;
168    for (i, g) in text.grapheme_indices(true) {
169        if c >= col.0 {
170            return ByteOffset(start.0 + i);
171        }
172        c += grapheme_width(g, c, tab_width);
173    }
174    ByteOffset(start.0 + text.len())
175}
176
177/// Width of the whole line in visual columns.
178pub fn line_visual_width(doc: &Document, line: usize, tab_width: usize) -> VisualCol {
179    VisualCol(advance(&doc.line_text(line), 0, tab_width))
180}
181
182/// (byte, visual column, width in cells) of each grapheme of one line's
183/// `text`.
184pub fn cells(text: &str, tab_width: usize) -> Vec<(usize, usize, usize)> {
185    let mut col = 0;
186    let mut out = Vec::with_capacity(text.len());
187    if text.is_ascii() {
188        // Every byte is a grapheme (a line holds no CR LF pair).
189        let tw = tab_width.max(1);
190        for (i, b) in text.bytes().enumerate() {
191            let w = if b == b'\t' { tw - col % tw } else { 1 };
192            out.push((i, col, w));
193            col += w;
194        }
195    } else {
196        for (i, g) in text.grapheme_indices(true) {
197            let w = grapheme_width(g, col, tab_width);
198            out.push((i, col, w));
199            col += w;
200        }
201    }
202    out
203}
204
205/// The narrowest wrap width, in cells.
206pub const MIN_WRAP_WIDTH: usize = 10;
207
208/// How one line wraps into display rows.
209#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct Wrapped {
211    /// Byte offset (within the line) where each row starts; the first is 0.
212    pub starts: Vec<usize>,
213    /// The visual column (of the line) where each row starts.
214    pub start_cols: Vec<usize>,
215    /// Cells that rows after the first are indented by.
216    pub indent: usize,
217}
218
219/// Wraps `text` (one line, without its line break) to rows of `width`
220/// cells. Rows break after a run of whitespace when there is one in the
221/// row, otherwise before the grapheme that does not fit; a grapheme is never
222/// split, and one wider than a row gets a row of its own. Tab stops count
223/// from the start of the line, so wrapping never changes a grapheme's
224/// visual column. Rows after the first are indented like the line, by at
225/// most half the width.
226pub fn wrap_line(text: &str, width: usize, tab_width: usize) -> Wrapped {
227    let width = width.max(MIN_WRAP_WIDTH);
228    // Most lines fit; for plain ASCII that is a length check.
229    if text.len() <= width && text.bytes().all(|b| b.is_ascii() && b != b'\t') {
230        let lead = text.len() - text.trim_start_matches(' ').len();
231        return Wrapped {
232            starts: vec![0],
233            start_cols: vec![0],
234            indent: lead.min(width / 2),
235        };
236    }
237    let lead = text.len() - text.trim_start_matches([' ', '\t']).len();
238    let indent = advance(&text[..lead], 0, tab_width).min(width / 2);
239    let mut out = Wrapped {
240        starts: vec![0],
241        start_cols: vec![0],
242        indent,
243    };
244    // (byte, column, width, is whitespace) of each grapheme.
245    let mut col = 0;
246    let gs: Vec<(usize, usize, usize, bool)> = if text.is_ascii() {
247        // Most lines: every byte is a grapheme one cell wide (a line holds
248        // no `\n`, so there is no CR LF pair), except tabs.
249        text.bytes()
250            .enumerate()
251            .map(|(i, b)| {
252                let w = if b == b'\t' {
253                    let tw = tab_width.max(1);
254                    tw - col % tw
255                } else {
256                    1
257                };
258                col += w;
259                (i, col - w, w, b == b' ' || b == b'\t')
260            })
261            .collect()
262    } else {
263        text.grapheme_indices(true)
264            .map(|(i, g)| {
265                let w = grapheme_width(g, col, tab_width);
266                col += w;
267                (i, col - w, w, g == " " || g == "\t")
268            })
269            .collect()
270    };
271    // Never break right after the leading indentation.
272    let lead_graphemes = gs.iter().take_while(|g| g.3).count();
273    let (mut row_start, mut cap) = (0, width);
274    // The last place this row could break: just after whitespace.
275    let mut candidate = None;
276    let mut j = 0;
277    while j < gs.len() {
278        let (_, col, w, space) = gs[j];
279        if j > row_start.max(lead_graphemes) && gs[j - 1].3 && !space {
280            candidate = Some(j);
281        }
282        let row_col = gs[row_start].1;
283        if col + w - row_col > cap && j > row_start {
284            let at = candidate.unwrap_or(j);
285            out.starts.push(gs[at].0);
286            out.start_cols.push(gs[at].1);
287            row_start = at;
288            cap = width - indent;
289            candidate = None;
290            // Look at the new row from its start.
291            j = at;
292            continue;
293        }
294        j += 1;
295    }
296    out
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    /// The rows of `text` wrapped at `width` (tab width 4).
304    fn rows(text: &str, width: usize) -> Vec<&str> {
305        let w = wrap_line(text, width, 4);
306        let mut ends = w.starts[1..].to_vec();
307        ends.push(text.len());
308        w.starts
309            .iter()
310            .zip(ends)
311            .map(|(&a, b)| &text[a..b])
312            .collect()
313    }
314
315    #[test]
316    fn wrapping_breaks_after_whitespace_or_inside_long_words() {
317        assert_eq!(rows("short", 10), vec!["short"]);
318        assert_eq!(rows("aaaa bbbb cccc", 10), vec!["aaaa bbbb ", "cccc"]);
319        assert_eq!(
320            rows("abcdefghijklmnopqrstuvwxy", 10),
321            vec!["abcdefghij", "klmnopqrst", "uvwxy"]
322        );
323        // Indented by 4: continuation rows hold 6 cells; the indentation
324        // itself is never a row on its own.
325        assert_eq!(
326            rows("    xxxxxxxxxxxxxxxx", 10),
327            vec!["    xxxxxx", "xxxxxx", "xxxx"]
328        );
329        assert_eq!(wrap_line("    x x", 10, 4).indent, 4);
330        // Wide glyphs are not split; a tab keeps its line column.
331        assert_eq!(rows("漢字漢字漢字", 10), vec!["漢字漢字漢", "字"]);
332        assert_eq!(rows("ab\tcdefghijk", 10), vec!["ab\t", "cdefghijk"]);
333        assert_eq!(wrap_line("ab\tcdefghijk", 10, 4).start_cols, vec![0, 4]);
334        // Combining marks and emoji stay whole.
335        let marks = "e\u{301}e\u{301}e\u{301}👨\u{200d}👩\u{200d}👧xxxxxxx";
336        assert_eq!(
337            rows(marks, 10),
338            vec!["e\u{301}e\u{301}e\u{301}👨\u{200d}👩\u{200d}👧xxxxx", "xx"]
339        );
340        // Very narrow widths are raised to the minimum.
341        assert_eq!(rows("aaaaaaaaaaaa", 3), vec!["aaaaaaaaaa", "aa"]);
342    }
343
344    fn boundaries(text: &str) -> Vec<usize> {
345        let doc = Document::new(text);
346        let mut out = vec![0];
347        let mut p = ByteOffset(0);
348        while p < doc.len() {
349            p = next_grapheme_boundary(&doc, p);
350            out.push(p.0);
351        }
352        let mut back = vec![doc.len().0];
353        let mut p = doc.len();
354        while p.0 > 0 {
355            p = prev_grapheme_boundary(&doc, p);
356            back.push(p.0);
357        }
358        back.reverse();
359        assert_eq!(out, back, "forward and backward stepping disagree");
360        out
361    }
362
363    #[test]
364    fn grapheme_stepping() {
365        // e + combining acute is one grapheme.
366        assert_eq!(boundaries("e\u{301}x"), vec![0, 3, 4]);
367        // Family emoji (ZWJ sequence) is one grapheme of 18 bytes.
368        assert_eq!(boundaries("👨‍👩‍👧!"), vec![0, 18, 19]);
369        // Skin-tone modifier joins the base emoji.
370        assert_eq!(boundaries("👍🏽"), vec![0, 8]);
371        assert_eq!(boundaries("漢字"), vec![0, 3, 6]);
372        assert_eq!(boundaries("a\nb"), vec![0, 1, 2, 3]);
373        assert_eq!(boundaries(""), vec![0]);
374    }
375
376    #[test]
377    fn grapheme_stepping_across_rope_chunks() {
378        // Long enough for Ropey to split into many chunks; combining marks and
379        // ZWJ sequences then straddle chunk boundaries somewhere.
380        let unit = "ab e\u{301}👨‍👩‍👧漢\t";
381        let text = unit.repeat(500);
382        let b = boundaries(&text);
383        let expected: Vec<usize> = std::iter::once(0)
384            .chain(text.grapheme_indices(true).map(|(i, g)| i + g.len()))
385            .collect();
386        assert_eq!(b, expected);
387    }
388
389    #[test]
390    fn visual_columns() {
391        let doc = Document::new("\ta\t漢e\u{301}👨‍👩‍👧x\nnext");
392        let cols: Vec<usize> = [0, 1, 2, 3, 6, 9, 27, 28]
393            .iter()
394            .map(|&b| byte_to_visual_col(&doc, ByteOffset(b), 4).0)
395            .collect();
396        // tab->4, a->5, tab->8, 漢 (2 cells)->10, é->11, family (2 cells)->13, x->14
397        assert_eq!(cols, vec![0, 4, 5, 8, 10, 11, 13, 14]);
398        assert_eq!(line_visual_width(&doc, 0, 4), VisualCol(14));
399        assert_eq!(byte_to_visual_col(&doc, ByteOffset(29), 4), VisualCol(0));
400    }
401
402    #[test]
403    fn visual_col_to_byte_snaps_left_inside_wide_glyphs_and_tabs() {
404        let doc = Document::new("\ta漢b");
405        let at = |c| visual_col_to_byte(&doc, 0, VisualCol(c), 4).0;
406        assert_eq!(at(0), 0);
407        assert_eq!(at(2), 0); // inside the tab
408        assert_eq!(at(4), 1);
409        assert_eq!(at(5), 2);
410        assert_eq!(at(6), 2); // inside 漢
411        assert_eq!(at(7), 5);
412        assert_eq!(at(99), 6); // clamps to line end
413    }
414
415    #[test]
416    fn line_col_round_trip() {
417        let doc = Document::new("ab\ncdé\n");
418        for b in [0, 1, 2, 3, 4, 5, 7, 8] {
419            let lc = byte_to_line_col(&doc, ByteOffset(b));
420            assert_eq!(line_col_to_byte(&doc, lc), ByteOffset(b));
421        }
422        assert_eq!(
423            byte_to_line_col(&doc, ByteOffset(5)),
424            LineCol {
425                line: 1,
426                col: ByteOffset(2)
427            }
428        );
429    }
430}