aboutsummaryrefslogtreecommitdiff
path: root/helix-core/src/position.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-core/src/position.rs')
-rw-r--r--helix-core/src/position.rs144
1 files changed, 115 insertions, 29 deletions
diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs
index 3d114b52..611e6b76 100644
--- a/helix-core/src/position.rs
+++ b/helix-core/src/position.rs
@@ -1,6 +1,7 @@
use crate::{
chars::char_is_line_ending,
- graphemes::{nth_next_grapheme_boundary, RopeGraphemes},
+ graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
+ line_ending::line_end_char_index,
RopeSlice,
};
@@ -52,19 +53,50 @@ impl From<Position> for tree_sitter::Point {
}
}
/// Convert a character index to (line, column) coordinates.
+///
+/// TODO: this should be split into two methods: one for visual
+/// row/column, and one for "objective" row/column (possibly with
+/// the column specified in `char`s). The former would be used
+/// for cursor movement, and the latter would be used for e.g. the
+/// row:column display in the status line.
pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
let line = text.char_to_line(pos);
+
let line_start = text.line_to_char(line);
+ let pos = ensure_grapheme_boundary_prev(text, pos);
let col = RopeGraphemes::new(text.slice(line_start..pos)).count();
+
Position::new(line, col)
}
/// Convert (line, column) coordinates to a character index.
-pub fn pos_at_coords(text: RopeSlice, coords: Position) -> usize {
+///
+/// `is_1_width` specifies whether the position should be treated
+/// as a block cursor or not. This effects how line-ends are handled.
+/// `false` corresponds to properly round-tripping with `coords_at_pos()`,
+/// whereas `true` will ensure that block cursors don't jump off the
+/// end of the line.
+///
+/// TODO: this should be changed to work in terms of visual row/column, not
+/// graphemes.
+pub fn pos_at_coords(text: RopeSlice, coords: Position, is_1_width: bool) -> usize {
let Position { row, col } = coords;
let line_start = text.line_to_char(row);
- // line_start + col
- nth_next_grapheme_boundary(text, line_start, col)
+ let line_end = if is_1_width {
+ line_end_char_index(&text, row)
+ } else {
+ text.line_to_char((row + 1).min(text.len_lines()))
+ };
+
+ let mut col_char_offset = 0;
+ for (i, g) in RopeGraphemes::new(text.slice(line_start..line_end)).enumerate() {
+ if i == col {
+ break;
+ }
+ col_char_offset += g.chars().count();
+ }
+
+ line_start + col_char_offset
}
#[cfg(test)]
@@ -80,55 +112,109 @@ mod test {
#[test]
fn test_coords_at_pos() {
- // let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
- // let slice = text.slice(..);
- // assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
- // assert_eq!(coords_at_pos(slice, 5), (0, 5).into()); // position on \n
- // assert_eq!(coords_at_pos(slice, 6), (1, 0).into()); // position on w
- // assert_eq!(coords_at_pos(slice, 7), (1, 1).into()); // position on o
- // assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
-
- // test with grapheme clusters
+ let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
+ let slice = text.slice(..);
+ assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
+ assert_eq!(coords_at_pos(slice, 5), (0, 5).into()); // position on \n
+ assert_eq!(coords_at_pos(slice, 6), (1, 0).into()); // position on w
+ assert_eq!(coords_at_pos(slice, 7), (1, 1).into()); // position on o
+ assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
+
+ // Test with wide characters.
+ // TODO: account for character width.
+ let text = Rope::from("今日はいい\n");
+ let slice = text.slice(..);
+ assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
+ assert_eq!(coords_at_pos(slice, 1), (0, 1).into());
+ assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
+ assert_eq!(coords_at_pos(slice, 3), (0, 3).into());
+ assert_eq!(coords_at_pos(slice, 4), (0, 4).into());
+ assert_eq!(coords_at_pos(slice, 5), (0, 5).into());
+ assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
+
+ // Test with grapheme clusters.
let text = Rope::from("a̐éö̲\r\n");
let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
assert_eq!(coords_at_pos(slice, 2), (0, 1).into());
assert_eq!(coords_at_pos(slice, 4), (0, 2).into());
assert_eq!(coords_at_pos(slice, 7), (0, 3).into());
+ assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
- let text = Rope::from("किमपि");
+ // Test with wide-character grapheme clusters.
+ // TODO: account for character width.
+ let text = Rope::from("किमपि\n");
let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
assert_eq!(coords_at_pos(slice, 2), (0, 1).into());
assert_eq!(coords_at_pos(slice, 3), (0, 2).into());
assert_eq!(coords_at_pos(slice, 5), (0, 3).into());
+ assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
+
+ // Test with tabs.
+ // Todo: account for tab stops.
+ let text = Rope::from("\tHello\n");
+ let slice = text.slice(..);
+ assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
+ assert_eq!(coords_at_pos(slice, 1), (0, 1).into());
+ assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
}
#[test]
fn test_pos_at_coords() {
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
let slice = text.slice(..);
- assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
- assert_eq!(pos_at_coords(slice, (0, 5).into()), 5); // position on \n
- assert_eq!(pos_at_coords(slice, (1, 0).into()), 6); // position on w
- assert_eq!(pos_at_coords(slice, (1, 1).into()), 7); // position on o
- assert_eq!(pos_at_coords(slice, (1, 4).into()), 10); // position on d
+ assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
+ assert_eq!(pos_at_coords(slice, (0, 5).into(), false), 5); // position on \n
+ assert_eq!(pos_at_coords(slice, (0, 6).into(), false), 6); // position after \n
+ assert_eq!(pos_at_coords(slice, (0, 6).into(), true), 5); // position after \n
+ assert_eq!(pos_at_coords(slice, (1, 0).into(), false), 6); // position on w
+ assert_eq!(pos_at_coords(slice, (1, 1).into(), false), 7); // position on o
+ assert_eq!(pos_at_coords(slice, (1, 4).into(), false), 10); // position on d
- // test with grapheme clusters
+ // Test with wide characters.
+ // TODO: account for character width.
+ let text = Rope::from("今日はいい\n");
+ let slice = text.slice(..);
+ assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
+ assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 1);
+ assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 2);
+ assert_eq!(pos_at_coords(slice, (0, 3).into(), false), 3);
+ assert_eq!(pos_at_coords(slice, (0, 4).into(), false), 4);
+ assert_eq!(pos_at_coords(slice, (0, 5).into(), false), 5);
+ assert_eq!(pos_at_coords(slice, (0, 6).into(), false), 6);
+ assert_eq!(pos_at_coords(slice, (0, 6).into(), true), 5);
+ assert_eq!(pos_at_coords(slice, (1, 0).into(), false), 6);
+
+ // Test with grapheme clusters.
let text = Rope::from("a̐éö̲\r\n");
let slice = text.slice(..);
- assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
- assert_eq!(pos_at_coords(slice, (0, 1).into()), 2);
- assert_eq!(pos_at_coords(slice, (0, 2).into()), 4);
- assert_eq!(pos_at_coords(slice, (0, 3).into()), 7); // \r\n is one char here
- assert_eq!(pos_at_coords(slice, (0, 4).into()), 9);
+ assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
+ assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 2);
+ assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 4);
+ assert_eq!(pos_at_coords(slice, (0, 3).into(), false), 7); // \r\n is one char here
+ assert_eq!(pos_at_coords(slice, (0, 4).into(), false), 9);
+ assert_eq!(pos_at_coords(slice, (0, 4).into(), true), 7);
+ assert_eq!(pos_at_coords(slice, (1, 0).into(), false), 9);
+
+ // Test with wide-character grapheme clusters.
+ // TODO: account for character width.
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_coords(slice, (0, 0).into()), 0);
- assert_eq!(pos_at_coords(slice, (0, 1).into()), 2);
- assert_eq!(pos_at_coords(slice, (0, 2).into()), 3);
- assert_eq!(pos_at_coords(slice, (0, 3).into()), 5); // eol
+ assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
+ assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 2);
+ assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 3);
+ assert_eq!(pos_at_coords(slice, (0, 3).into(), false), 5);
+ assert_eq!(pos_at_coords(slice, (0, 3).into(), true), 5);
+
+ // Test with tabs.
+ // Todo: account for tab stops.
+ let text = Rope::from("\tHello\n");
+ let slice = text.slice(..);
+ assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
+ assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 1);
+ assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 2);
}
}