Skip to main content

hjkl_buffer/
position.rs

1/// A `(row, col)` location inside a [`crate::Buffer`].
2///
3/// `col` is a **char index** along the row's line, not a byte offset.
4/// That's how vim users think about cursor positions ("column 4" =
5/// the 4th character) and it sidesteps the off-by-one bugs that come
6/// from mixing byte and char indices when the buffer holds
7/// non-ASCII text. The accompanying [`Position::byte_offset`] helper
8/// converts back to a byte offset when slicing the underlying
9/// `String`.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
11pub struct Position {
12    pub row: usize,
13    pub col: usize,
14}
15
16impl Position {
17    pub const fn new(row: usize, col: usize) -> Self {
18        Self { row, col }
19    }
20
21    /// Byte offset of `self.col` (a char index) into `line`. Returns
22    /// `line.len()` when `col` is at or past the end of the line —
23    /// matches `String::insert` / `replace_range` boundary semantics.
24    pub fn byte_offset(self, line: &str) -> usize {
25        line.char_indices()
26            .nth(self.col)
27            .map(|(b, _)| b)
28            .unwrap_or(line.len())
29    }
30}
31
32#[cfg(test)]
33mod tests {
34    use super::Position;
35
36    #[test]
37    fn byte_offset_ascii() {
38        assert_eq!(Position::new(0, 0).byte_offset("hello"), 0);
39        assert_eq!(Position::new(0, 3).byte_offset("hello"), 3);
40        assert_eq!(Position::new(0, 5).byte_offset("hello"), 5);
41        // Past end clamps at line length so callers can use it as an
42        // insertion point without bounds-check ceremony.
43        assert_eq!(Position::new(0, 99).byte_offset("hello"), 5);
44    }
45
46    #[test]
47    fn byte_offset_utf8() {
48        // "tablé" — 'é' is 2 bytes in UTF-8.
49        let line = "tablé";
50        assert_eq!(Position::new(0, 4).byte_offset(line), 4);
51        assert_eq!(Position::new(0, 5).byte_offset(line), 6);
52    }
53
54    #[test]
55    fn ord_is_row_major() {
56        assert!(Position::new(0, 5) < Position::new(1, 0));
57        assert!(Position::new(2, 0) > Position::new(1, 999));
58        assert!(Position::new(1, 3) < Position::new(1, 4));
59    }
60}