1use ropey::Rope;
11
12pub fn line_of(text: &Rope, pos: usize) -> usize {
14 text.char_to_line(pos.min(text.len_chars()))
15}
16
17pub fn column_of(text: &Rope, pos: usize) -> usize {
19 let pos = pos.min(text.len_chars());
20 pos - text.line_to_char(text.char_to_line(pos))
21}
22
23pub fn line_start(text: &Rope, pos: usize) -> usize {
25 text.line_to_char(line_of(text, pos))
26}
27
28pub fn line_end(text: &Rope, pos: usize) -> usize {
31 let line = line_of(text, pos);
32 let start = text.line_to_char(line);
33 let slice = text.line(line);
34 let len = slice.len_chars();
35 let visible = if len > 0 && slice.char(len - 1) == '\n' { len - 1 } else { len };
36 start + visible
37}
38
39pub fn left(text: &Rope, pos: usize) -> usize {
40 let _ = text;
41 pos.saturating_sub(1)
42}
43
44pub fn right(text: &Rope, pos: usize) -> usize {
45 (pos + 1).min(text.len_chars())
46}
47
48pub fn up(text: &Rope, pos: usize, goal: Option<usize>) -> usize {
53 let line = line_of(text, pos);
54 if line == 0 {
55 return pos;
56 }
57 to_line(text, line - 1, goal.unwrap_or_else(|| column_of(text, pos)))
58}
59
60pub fn down(text: &Rope, pos: usize, goal: Option<usize>) -> usize {
61 let line = line_of(text, pos);
62 if line + 1 >= text.len_lines() {
63 return pos;
64 }
65 to_line(text, line + 1, goal.unwrap_or_else(|| column_of(text, pos)))
66}
67
68fn to_line(text: &Rope, line: usize, column: usize) -> usize {
74 let start = text.line_to_char(line);
75 let end = line_end(text, start);
76 (start + column).min(end)
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 fn rope(s: &str) -> Rope {
84 Rope::from_str(s)
85 }
86
87 #[test]
88 fn columns_and_lines_are_reported_from_the_line_start() {
89 let t = rope("abc\ndefgh\n");
90 assert_eq!((line_of(&t, 0), column_of(&t, 0)), (0, 0));
91 assert_eq!((line_of(&t, 2), column_of(&t, 2)), (0, 2));
92 assert_eq!((line_of(&t, 4), column_of(&t, 4)), (1, 0));
93 assert_eq!((line_of(&t, 7), column_of(&t, 7)), (1, 3));
94 }
95
96 #[test]
97 fn horizontal_motion_stops_at_both_ends_of_the_document() {
98 let t = rope("ab");
99 assert_eq!(left(&t, 0), 0, "no wrapping off the front");
100 assert_eq!(right(&t, 2), 2, "no running off the end");
101 assert_eq!(right(&t, 0), 1);
102 assert_eq!(left(&t, 2), 1);
103 }
104
105 #[test]
106 fn end_lands_before_the_newline_not_after_it() {
107 let t = rope("abc\ndef\n");
108 assert_eq!(line_end(&t, 0), 3, "the end of 'abc', not the start of 'def'");
109 assert_eq!(line_start(&t, 5), 4);
110 }
111
112 #[test]
113 fn end_of_a_final_line_without_a_trailing_newline() {
114 let t = rope("abc\ndef");
115 assert_eq!(line_end(&t, 5), 7);
116 }
117
118 #[test]
119 fn end_of_an_empty_line() {
120 let t = rope("abc\n\ndef");
121 assert_eq!(line_end(&t, 4), 4, "an empty line starts and ends in the same place");
122 }
123
124 #[test]
125 fn vertical_motion_parks_at_the_first_and_last_line() {
126 let t = rope("abc\ndef");
127 assert_eq!(up(&t, 1, None), 1, "already on the first line");
128 assert_eq!(down(&t, 5, None), 5, "already on the last line");
129 }
130
131 #[test]
132 fn moving_down_keeps_the_column() {
133 let t = rope("abcdef\nghijkl\n");
134 assert_eq!(down(&t, 3, None), 10, "column 3 on the next line");
135 }
136
137 #[test]
138 fn a_short_line_clamps_the_column_without_losing_it() {
139 let t = rope("abcdefgh\nxy\nabcdefgh\n");
142 let start = 6; let goal = Some(column_of(&t, start));
145 let middle = down(&t, start, goal);
146 assert_eq!(column_of(&t, middle), 2, "clamped to the short line");
147
148 let back = down(&t, middle, goal);
149 assert_eq!(column_of(&t, back), 6, "and restored on the next long one");
150 }
151
152 #[test]
153 fn without_a_goal_the_column_is_taken_from_where_we_are() {
154 let t = rope("abcdefgh\nxy\nabcdefgh\n");
155 let middle = down(&t, 6, None);
156 let back = down(&t, middle, None);
157 assert_eq!(column_of(&t, back), 2, "no sticky column, so the clamp sticks");
158 }
159
160 #[test]
161 fn motion_is_by_char_across_multibyte_text() {
162 let t = rope("héllo");
163 assert_eq!(right(&t, 1), 2, "one press crosses 'é' once, not twice");
164 assert_eq!(column_of(&t, 5), 5);
165 }
166
167 #[test]
168 fn positions_past_the_end_are_treated_as_the_end() {
169 let t = rope("abc");
170 assert_eq!(line_of(&t, 99), 0);
171 assert_eq!(column_of(&t, 99), 3);
172 }
173}