Skip to main content

typ_buffer/
position.rs

1use unicode_segmentation::UnicodeSegmentation;
2use unicode_width::UnicodeWidthStr;
3
4/// Display columns occupied by a single grapheme cluster.
5///
6/// Tabs are handled by callers that know the current column, so this reports
7/// a tab as 0 and lets them add the tab-stop padding.
8fn grapheme_width(g: &str) -> usize {
9    if g == "\t" {
10        0
11    } else {
12        // Zero-width and combining sequences report 0 here, which is correct.
13        UnicodeWidthStr::width(g)
14    }
15}
16
17/// Total display columns a string occupies, expanding tabs to `tab_width` stops.
18pub fn display_width_with_tabs(s: &str, tab_width: usize) -> usize {
19    let mut col = 0usize;
20    for g in s.graphemes(true) {
21        if g == "\t" {
22            col += tab_width - (col % tab_width);
23        } else {
24            col += grapheme_width(g);
25        }
26    }
27    col
28}
29
30/// Total display columns, using the default tab width of 4.
31pub fn display_width(s: &str) -> usize {
32    display_width_with_tabs(s, 4)
33}
34
35/// Display column at which the grapheme at `grapheme_idx` begins.
36pub fn grapheme_to_display_col(line: &str, grapheme_idx: usize, tab_width: usize) -> usize {
37    let mut col = 0usize;
38    for (i, g) in line.graphemes(true).enumerate() {
39        if i == grapheme_idx {
40            return col;
41        }
42        if g == "\t" {
43            col += tab_width - (col % tab_width);
44        } else {
45            col += grapheme_width(g);
46        }
47    }
48    col
49}
50
51/// Grapheme index containing `display_col`.
52///
53/// Clicking anywhere inside a wide grapheme selects that grapheme, so the
54/// right half of a CJK character does not land on the following one. Clicks
55/// past the end of the line clamp to the line length.
56pub fn display_to_grapheme_col(line: &str, display_col: usize, tab_width: usize) -> usize {
57    let mut col = 0usize;
58    for (i, g) in line.graphemes(true).enumerate() {
59        let w = if g == "\t" {
60            tab_width - (col % tab_width)
61        } else {
62            grapheme_width(g)
63        };
64        if display_col < col + w.max(1) {
65            return i;
66        }
67        col += w;
68    }
69    line.graphemes(true).count()
70}
71
72/// A cursor location. `col` is a grapheme index, never a byte or char offset.
73///
74/// Using grapheme indices throughout means a cursor never lands inside a
75/// multi-byte character or splits a combining sequence.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
77pub struct Position {
78    pub line: usize,
79    pub col: usize,
80}