on master help sign in
Ensure coords in screen depends on char width (#885)

The issue affected files with lots of tabs at the start as well. Fix #840

commit3eb829e2330fed5ad1c095f8bba44f62361b4943 authorIvan Tham <pickfire@riseup.net> dateWed 3 Nov 2021, 11:02 (5 years ago) parentee889aa
5 files changed+87−18
helix-core/src/lib.rs+1−1 helix-core/src/movement.rs+4 helix-core/src/position.rs+73−8 helix-term/src/ui/completion.rs+4−6 helix-view/src/view.rs+5−3
helix-core/src/lib.rs+1−1
@@ -194,7 +194,7 @@ pub use tendril::StrTendril as Tendril;
194194pub use {regex, tree_sitter};
195195
196196pub use graphemes::RopeGraphemes;
197−pub use position::{coords_at_pos, pos_at_coords, Position};
197+pub use position::{coords_at_pos, pos_at_coords, visual_coords_at_pos, Position};
198198pub use selection::{Range, Selection};
199199pub use smallvec::SmallVec;
200200pub use syntax::Syntax;
helix-core/src/movement.rs+4
@@ -53,6 +53,10 @@ pub fn move_vertically(
5353 let pos = range.cursor(slice);
5454
5555 // Compute the current position's 2d coordinates.
56+ // TODO: switch this to use `visual_coords_at_pos` rather than
57+ // `coords_at_pos` as this will cause a jerky movement when the visual
58+ // position does not match, like moving from a line with tabs/CJK to
59+ // a line without
5660 let Position { row, col } = coords_at_pos(slice, pos);
5761 let horiz = range.horiz.unwrap_or(col as u32);
5862
helix-core/src/position.rs+73−8
@@ -2,6 +2,7 @@ use crate::{
22 chars::char_is_line_ending,
33 graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
44 line_ending::line_end_char_index,
5+ unicode::width::UnicodeWidthChar,
56 RopeSlice,
67};
78
@@ -54,11 +55,8 @@ impl From<Position> for tree_sitter::Point {
5455}
5556/// Convert a character index to (line, column) coordinates.
5657///
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.
6260pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
6361 let line = text.char_to_line(pos);
6462
@@ -69,6 +67,28 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
6967 Position::new(line, col)
7068}
7169
70+/// Convert a character index to (line, column) coordinates visually.
71+///
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);
77+
78+ let line_start = text.line_to_char(line);
79+ let pos = ensure_grapheme_boundary_prev(text, pos);
80+ let col = text
81+ .slice(line_start..pos)
82+ .chars()
83+ .flat_map(|c| match c {
84+ '\t' => Some(tab_width),
85+ c => UnicodeWidthChar::width(c),
86+ })
87+ .sum();
88+
89+ Position::new(line, col)
90+}
91+
7292/// Convert (line, column) coordinates to a character index.
7393///
7494/// If the `line` coordinate is beyond the end of the file, the EOF
@@ -130,7 +150,6 @@ mod test {
130150 assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
131151
132152 // Test with wide characters.
133− // TODO: account for character width.
134153 let text = Rope::from("今日はいい\n");
135154 let slice = text.slice(..);
136155 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -151,7 +170,6 @@ mod test {
151170 assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
152171
153172 // Test with wide-character grapheme clusters.
154− // TODO: account for character width.
155173 let text = Rope::from("किमपि\n");
156174 let slice = text.slice(..);
157175 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -161,7 +179,6 @@ mod test {
161179 assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
162180
163181 // Test with tabs.
164− // Todo: account for tab stops.
165182 let text = Rope::from("\tHello\n");
166183 let slice = text.slice(..);
167184 assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@@ -169,6 +186,54 @@ mod test {
169186 assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
170187 }
171188
189+ #[test]
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
198+
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());
209+
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());
218+
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());
228+
229+ // Test with tabs.
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());
235+ }
236+
172237 #[test]
173238 fn test_pos_at_coords() {
174239 let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
helix-term/src/ui/completion.rs+4−6
@@ -264,12 +264,10 @@ impl Component for Completion {
264264 .language()
265265 .and_then(|scope| scope.strip_prefix("source."))
266266 .unwrap_or("");
267− let cursor_pos = doc
268− .selection(view.id)
269− .primary()
270− .cursor(doc.text().slice(..));
271− let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
272− - view.offset.row) as u16;
267+ let text = doc.text().slice(..);
268+ let cursor_pos = doc.selection(view.id).primary().cursor(text);
269+ let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
270+ let cursor_pos = (coords.row - view.offset.row) as u16;
273271 let mut markdown_doc = match &option.documentation {
274272 Some(lsp::Documentation::String(contents))
275273 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
helix-view/src/view.rs+5−3
@@ -2,10 +2,9 @@ use std::borrow::Cow;
22
33use crate::{graphics::Rect, Document, DocumentId, ViewId};
44use helix_core::{
5− coords_at_pos,
65 graphemes::{grapheme_width, RopeGraphemes},
76 line_ending::line_end_char_index,
8− Position, RopeSlice, Selection,
7+ visual_coords_at_pos, Position, RopeSlice, Selection,
98};
109
1110type Jump = (DocumentId, Selection);
@@ -91,7 +90,10 @@ impl View {
9190 .selection(self.id)
9291 .primary()
9392 .cursor(doc.text().slice(..));
94− let Position { col, row: line } = coords_at_pos(doc.text().slice(..), cursor);
93+
94+ let Position { col, row: line } =
95+ visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width());
96+
9597 let inner_area = self.inner_area();
9698 let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
9799