@@ -2,6 +2,7 @@ use crate::{
2 chars::char_is_line_ending,
2 chars::char_is_line_ending,
3 graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
3 graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
4 line_ending::line_end_char_index,
4 line_ending::line_end_char_index,
5+ unicode::width::UnicodeWidthChar,
@@ -54,11 +55,8 @@ impl From<Position> for tree_sitter::Point {
55/// Convert a character index to (line, column) coordinates.
56/// Convert a character index to (line, column) coordinates.
57−/// TODO: this should be split into two methods: one for visual
58−/// row/column, and one for "objective" row/column (possibly with
59−/// the column specified in `char`s). The former would be used
60−/// for cursor movement, and the latter would be used for e.g. the
61−/// row:column display in the status line.
58+/// column in `char` count which can be used for row:column display in
59+/// status line. See [`visual_coords_at_pos`] for a visual one.
62pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
60pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
63 let line = text.char_to_line(pos);
61 let line = text.char_to_line(pos);
@@ -69,6 +67,28 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
69 Position::new(line, col)
67 Position::new(line, col)
70+/// Convert a character index to (line, column) coordinates visually.
72+/// Takes \t, double-width characters (CJK) into account as well as text
73+/// not in the document in the future.
74+/// See [`coords_at_pos`] for an "objective" one.
75+pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Position {
76+ let line = text.char_to_line(pos);
78+ let line_start = text.line_to_char(line);
79+ let pos = ensure_grapheme_boundary_prev(text, pos);
81+ .slice(line_start..pos)
83+ .flat_map(|c| match c {
84+ '\t' => Some(tab_width),
85+ c => UnicodeWidthChar::width(c),
89+ Position::new(line, col)
72/// Convert (line, column) coordinates to a character index.
92/// Convert (line, column) coordinates to a character index.
74/// If the `line` coordinate is beyond the end of the file, the EOF
94/// If the `line` coordinate is beyond the end of the file, the EOF
@@ -130,7 +150,6 @@ mod test {
130 assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
150 assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
132 // Test with wide characters.
152 // Test with wide characters.
133− // TODO: account for character width.
134 let text = Rope::from("今日はいい\n");
153 let text = Rope::from("今日はいい\n");
135 let slice = text.slice(..);
154 let slice = text.slice(..);
136 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
155 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -151,7 +170,6 @@ mod test {
151 assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
170 assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
153 // Test with wide-character grapheme clusters.
172 // Test with wide-character grapheme clusters.
154− // TODO: account for character width.
155 let text = Rope::from("किमपि\n");
173 let text = Rope::from("किमपि\n");
156 let slice = text.slice(..);
174 let slice = text.slice(..);
157 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
175 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -161,7 +179,6 @@ mod test {
161 assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
179 assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
163 // Test with tabs.
181 // Test with tabs.
164− // Todo: account for tab stops.
165 let text = Rope::from("\tHello\n");
182 let text = Rope::from("\tHello\n");
166 let slice = text.slice(..);
183 let slice = text.slice(..);
167 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
184 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -169,6 +186,54 @@ mod test {
169 assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
186 assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
190+ fn test_visual_coords_at_pos() {
191+ let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
192+ let slice = text.slice(..);
193+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
194+ assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into()); // position on \n
195+ assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into()); // position on w
196+ assert_eq!(visual_coords_at_pos(slice, 7, 8), (1, 1).into()); // position on o
197+ assert_eq!(visual_coords_at_pos(slice, 10, 8), (1, 4).into()); // position on d
199+ // Test with wide characters.
200+ let text = Rope::from("今日はいい\n");
201+ let slice = text.slice(..);
202+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
203+ assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 2).into());
204+ assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 4).into());
205+ assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 6).into());
206+ assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 8).into());
207+ assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 10).into());
208+ assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into());
210+ // Test with grapheme clusters.
211+ let text = Rope::from("a̐éö̲\r\n");
212+ let slice = text.slice(..);
213+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
214+ assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 1).into());
215+ assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 2).into());
216+ assert_eq!(visual_coords_at_pos(slice, 7, 8), (0, 3).into());
217+ assert_eq!(visual_coords_at_pos(slice, 9, 8), (1, 0).into());
219+ // Test with wide-character grapheme clusters.
220+ // TODO: account for cluster.
221+ let text = Rope::from("किमपि\n");
222+ let slice = text.slice(..);
223+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
224+ assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 2).into());
225+ assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 3).into());
226+ assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into());
227+ assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into());
230+ let text = Rope::from("\tHello\n");
231+ let slice = text.slice(..);
232+ assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
233+ assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 8).into());
234+ assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 9).into());
173 fn test_pos_at_coords() {
238 fn test_pos_at_coords() {
174 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
239 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");