floem_editor_core/
soft_tab.rs

1use lapce_xi_rope::Rope;
2
3/// The direction to snap. Left is used when moving left, Right when moving right.
4/// Nearest is used for mouse selection.
5pub enum SnapDirection {
6    Left,
7    Right,
8    Nearest,
9}
10
11/// If the cursor is inside a soft tab at the start of the line, snap it to the
12/// nearest, left or right edge. This version takes an offset and returns an offset.
13pub fn snap_to_soft_tab(
14    text: &Rope,
15    offset: usize,
16    direction: SnapDirection,
17    tab_width: usize,
18) -> usize {
19    // Fine which line we're on.
20    let line = text.line_of_offset(offset);
21    // Get the offset to the start of the line.
22    let start_line_offset = text.offset_of_line(line);
23    // And the offset within the lint.
24    let offset_within_line = offset - start_line_offset;
25
26    start_line_offset
27        + snap_to_soft_tab_logic(
28            text,
29            offset_within_line,
30            start_line_offset,
31            direction,
32            tab_width,
33        )
34}
35
36/// If the cursor is inside a soft tab at the start of the line, snap it to the
37/// nearest, left or right edge. This version takes a line/column and returns a column.
38pub fn snap_to_soft_tab_line_col(
39    text: &Rope,
40    line: usize,
41    col: usize,
42    direction: SnapDirection,
43    tab_width: usize,
44) -> usize {
45    // Get the offset to the start of the line.
46    let start_line_offset = text.offset_of_line(line);
47
48    snap_to_soft_tab_logic(text, col, start_line_offset, direction, tab_width)
49}
50
51/// Internal shared logic that performs the actual snapping. It can be passed
52/// either an column or offset within the line since it is only modified when it makes no
53/// difference which is used (since they're equal for spaces).
54/// It returns the column or offset within the line (depending on what you passed in).
55fn snap_to_soft_tab_logic(
56    text: &Rope,
57    offset_or_col: usize,
58    start_line_offset: usize,
59    direction: SnapDirection,
60    tab_width: usize,
61) -> usize {
62    assert!(tab_width >= 1);
63
64    // Number of spaces, ignoring incomplete soft tabs.
65    let space_count = (count_spaces_from(text, start_line_offset) / tab_width) * tab_width;
66
67    // If we're past the soft tabs, we don't need to snap.
68    if offset_or_col >= space_count {
69        return offset_or_col;
70    }
71
72    let bias = match direction {
73        SnapDirection::Left => 0,
74        SnapDirection::Right => tab_width - 1,
75        SnapDirection::Nearest => tab_width / 2,
76    };
77
78    ((offset_or_col + bias) / tab_width) * tab_width
79}
80
81/// Count the number of spaces found after a certain offset.
82fn count_spaces_from(text: &Rope, from_offset: usize) -> usize {
83    let mut cursor = lapce_xi_rope::Cursor::new(text, from_offset);
84    let mut space_count = 0usize;
85    while let Some(next) = cursor.next_codepoint() {
86        if next != ' ' {
87            break;
88        }
89        space_count += 1;
90    }
91    space_count
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_count_spaces_from() {
100        let text = Rope::from("     abc\n   def\nghi\n");
101        assert_eq!(count_spaces_from(&text, 0), 5);
102        assert_eq!(count_spaces_from(&text, 1), 4);
103        assert_eq!(count_spaces_from(&text, 5), 0);
104        assert_eq!(count_spaces_from(&text, 6), 0);
105
106        assert_eq!(count_spaces_from(&text, 8), 0);
107        assert_eq!(count_spaces_from(&text, 9), 3);
108        assert_eq!(count_spaces_from(&text, 10), 2);
109
110        assert_eq!(count_spaces_from(&text, 16), 0);
111        assert_eq!(count_spaces_from(&text, 17), 0);
112    }
113
114    #[test]
115    fn test_snap_to_soft_tab() {
116        let text = Rope::from("          abc\n      def\n    ghi\nklm\n        opq");
117
118        let tab_width = 4;
119
120        // Input offset, and output offset for Left, Nearest and Right respectively.
121        let test_cases = [
122            (0, 0, 0, 0),
123            (1, 0, 0, 4),
124            (2, 0, 4, 4),
125            (3, 0, 4, 4),
126            (4, 4, 4, 4),
127            (5, 4, 4, 8),
128            (6, 4, 8, 8),
129            (7, 4, 8, 8),
130            (8, 8, 8, 8),
131            (9, 9, 9, 9),
132            (10, 10, 10, 10),
133            (11, 11, 11, 11),
134            (12, 12, 12, 12),
135            (13, 13, 13, 13),
136            (14, 14, 14, 14),
137            (15, 14, 14, 18),
138            (16, 14, 18, 18),
139            (17, 14, 18, 18),
140            (18, 18, 18, 18),
141            (19, 19, 19, 19),
142            (20, 20, 20, 20),
143            (21, 21, 21, 21),
144        ];
145
146        for test_case in test_cases {
147            assert_eq!(
148                snap_to_soft_tab(&text, test_case.0, SnapDirection::Left, tab_width),
149                test_case.1
150            );
151            assert_eq!(
152                snap_to_soft_tab(&text, test_case.0, SnapDirection::Nearest, tab_width),
153                test_case.2
154            );
155            assert_eq!(
156                snap_to_soft_tab(&text, test_case.0, SnapDirection::Right, tab_width),
157                test_case.3
158            );
159        }
160    }
161
162    #[test]
163    fn test_snap_to_soft_tab_line_col() {
164        let text = Rope::from("          abc\n      def\n    ghi\nklm\n        opq");
165
166        let tab_width = 4;
167
168        // Input line, column, and output column for Left, Nearest and Right respectively.
169        let test_cases = [
170            (0, 0, 0, 0, 0),
171            (0, 1, 0, 0, 4),
172            (0, 2, 0, 4, 4),
173            (0, 3, 0, 4, 4),
174            (0, 4, 4, 4, 4),
175            (0, 5, 4, 4, 8),
176            (0, 6, 4, 8, 8),
177            (0, 7, 4, 8, 8),
178            (0, 8, 8, 8, 8),
179            (0, 9, 9, 9, 9),
180            (0, 10, 10, 10, 10),
181            (0, 11, 11, 11, 11),
182            (0, 12, 12, 12, 12),
183            (0, 13, 13, 13, 13),
184            (1, 0, 0, 0, 0),
185            (1, 1, 0, 0, 4),
186            (1, 2, 0, 4, 4),
187            (1, 3, 0, 4, 4),
188            (1, 4, 4, 4, 4),
189            (1, 5, 5, 5, 5),
190            (1, 6, 6, 6, 6),
191            (1, 7, 7, 7, 7),
192            (4, 0, 0, 0, 0),
193            (4, 1, 0, 0, 4),
194            (4, 2, 0, 4, 4),
195            (4, 3, 0, 4, 4),
196            (4, 4, 4, 4, 4),
197            (4, 5, 4, 4, 8),
198            (4, 6, 4, 8, 8),
199            (4, 7, 4, 8, 8),
200            (4, 8, 8, 8, 8),
201            (4, 9, 9, 9, 9),
202        ];
203
204        for test_case in test_cases {
205            assert_eq!(
206                snap_to_soft_tab_line_col(
207                    &text,
208                    test_case.0,
209                    test_case.1,
210                    SnapDirection::Left,
211                    tab_width
212                ),
213                test_case.2
214            );
215            assert_eq!(
216                snap_to_soft_tab_line_col(
217                    &text,
218                    test_case.0,
219                    test_case.1,
220                    SnapDirection::Nearest,
221                    tab_width
222                ),
223                test_case.3
224            );
225            assert_eq!(
226                snap_to_soft_tab_line_col(
227                    &text,
228                    test_case.0,
229                    test_case.1,
230                    SnapDirection::Right,
231                    tab_width
232                ),
233                test_case.4
234            );
235        }
236    }
237}