-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: make move_vertically
aware of tabs and wide characters
#2620
Changes from all commits
966a17f
ad244a4
33e8fa7
3c17265
324a76b
274746e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,9 +109,6 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po | |
/// with left-side block-cursor positions, as this prevents the the block cursor | ||
/// from jumping to the next line. Otherwise you typically want it to be `false`, | ||
/// such as when dealing with raw anchor/head positions. | ||
/// | ||
/// TODO: this should be changed to work in terms of visual row/column, not | ||
/// graphemes. | ||
pub fn pos_at_coords(text: RopeSlice, coords: Position, limit_before_line_ending: bool) -> usize { | ||
let Position { mut row, col } = coords; | ||
if limit_before_line_ending { | ||
|
@@ -135,6 +132,43 @@ pub fn pos_at_coords(text: RopeSlice, coords: Position, limit_before_line_ending | |
line_start + col_char_offset | ||
} | ||
|
||
/// Convert visual (line, column) coordinates to a character index. | ||
/// | ||
/// If the `line` coordinate is beyond the end of the file, the EOF | ||
/// position will be returned. | ||
/// | ||
/// If the `column` coordinate is past the end of the given line, the | ||
/// line-end position (in this case, just before the line ending | ||
/// character) will be returned. | ||
pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize) -> usize { | ||
let Position { mut row, col } = coords; | ||
row = row.min(text.len_lines() - 1); | ||
let line_start = text.line_to_char(row); | ||
let line_end = line_end_char_index(&text, row); | ||
|
||
let mut col_char_offset = 0; | ||
let mut cols_remaining = col; | ||
for grapheme in RopeGraphemes::new(text.slice(line_start..line_end)) { | ||
let grapheme_width = if grapheme == "\t" { | ||
tab_width - ((col - cols_remaining) % tab_width) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic here and in the conditional looks slightly different though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah true, it is still slightly different. The other implementation keeps track of the current column, while mine keeps track of the columns remaining before we reach the target column, but I think the behaviour is still the same. |
||
} else { | ||
let grapheme = Cow::from(grapheme); | ||
grapheme_width(&grapheme) | ||
}; | ||
|
||
// If pos is in the middle of a wider grapheme (tab for example) | ||
// return the starting offset. | ||
if grapheme_width > cols_remaining { | ||
break; | ||
} | ||
Comment on lines
+161
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you include the original comment here: https://github.com/helix-editor/helix/blob/eba82250bb4403fcb2e3ade74ba7301a680bc561/helix-view/src/view.rs#L280-L281= There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, added in 3c17265. |
||
|
||
cols_remaining -= grapheme_width; | ||
col_char_offset += grapheme.chars().count(); | ||
} | ||
|
||
line_start + col_char_offset | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
|
@@ -305,4 +339,70 @@ mod test { | |
assert_eq!(pos_at_coords(slice, (0, 10).into(), true), 0); | ||
assert_eq!(pos_at_coords(slice, (10, 10).into(), true), 0); | ||
} | ||
|
||
#[test] | ||
fn test_pos_at_visual_coords() { | ||
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ"); | ||
let slice = text.slice(..); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 0).into(), 4), 0); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 5).into(), 4), 5); // position on \n | ||
assert_eq!(pos_at_visual_coords(slice, (0, 6).into(), 4), 5); // position after \n | ||
assert_eq!(pos_at_visual_coords(slice, (1, 0).into(), 4), 6); // position on w | ||
assert_eq!(pos_at_visual_coords(slice, (1, 1).into(), 4), 7); // position on o | ||
assert_eq!(pos_at_visual_coords(slice, (1, 4).into(), 4), 10); // position on d | ||
|
||
// Test with wide characters. | ||
let text = Rope::from("今日はいい\n"); | ||
let slice = text.slice(..); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 0).into(), 4), 0); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 1).into(), 4), 0); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 2).into(), 4), 1); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 3).into(), 4), 1); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 4).into(), 4), 2); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 5).into(), 4), 2); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 6).into(), 4), 3); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 7).into(), 4), 3); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 8).into(), 4), 4); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 9).into(), 4), 4); | ||
// assert_eq!(pos_at_visual_coords(slice, (0, 10).into(), 4, false), 5); | ||
// assert_eq!(pos_at_visual_coords(slice, (0, 10).into(), 4, true), 5); | ||
assert_eq!(pos_at_visual_coords(slice, (1, 0).into(), 4), 6); | ||
|
||
// Test with grapheme clusters. | ||
let text = Rope::from("a̐éö̲\r\n"); | ||
let slice = text.slice(..); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 0).into(), 4), 0); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 1).into(), 4), 2); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 2).into(), 4), 4); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 3).into(), 4), 7); // \r\n is one char here | ||
assert_eq!(pos_at_visual_coords(slice, (0, 4).into(), 4), 7); | ||
assert_eq!(pos_at_visual_coords(slice, (1, 0).into(), 4), 9); | ||
|
||
// Test with wide-character grapheme clusters. | ||
let text = Rope::from("किमपि"); | ||
// 2 - 1 - 2 codepoints | ||
// TODO: delete handling as per https://news.ycombinator.com/item?id=20058454 | ||
let slice = text.slice(..); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 0).into(), 4), 0); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 1).into(), 4), 0); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 2).into(), 4), 2); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 3).into(), 4), 3); | ||
|
||
// Test with tabs. | ||
let text = Rope::from("\tHello\n"); | ||
let slice = text.slice(..); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 0).into(), 4), 0); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 1).into(), 4), 0); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 2).into(), 4), 0); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 3).into(), 4), 0); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 4).into(), 4), 1); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 5).into(), 4), 2); | ||
|
||
// Test out of bounds. | ||
let text = Rope::new(); | ||
let slice = text.slice(..); | ||
assert_eq!(pos_at_visual_coords(slice, (10, 0).into(), 4), 0); | ||
assert_eq!(pos_at_visual_coords(slice, (0, 10).into(), 4), 0); | ||
assert_eq!(pos_at_visual_coords(slice, (10, 10).into(), 4), 0); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This TODO seems wrong, pos_at_coords is intended for line, col calculation, and we already have pos_at_visual_coords for functions that need to deal with tab width and grapheme widths. The method should be kept unchanged, instead commands should use visual_ versions where necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I've refactored things in 33e8fa7 so
pos_at_visual_coords
is a separate function. I removed thelimit_before_line_ending
parameter because I figure when we're dealing with visual coords we'll always want the behaviour corresponding to a true value for that parameter, right?Also, I noticed this comment that mentions
pos_at_visual_coords
; is it fine if I update the code there to use that function now that it exists? It looks like the code there does the same thing as my implementation (at least, it does now, I found a bug in my code with the handling of tabs that don't start at a multiple oftab_width
from looking at it).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah good catch! It should use that function, similar to how the inverse does: https://github.com/helix-editor/helix/blob/eba82250bb4403fcb2e3ade74ba7301a680bc561/helix-view/src/view.rs#L226-L232=
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, updated in 324a76b.