Skip to main content

escriba_core/
position.rs

1use std::cmp::Ordering;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6/// 0-indexed line + column pair. Column is a **UTF-8 char offset** into
7/// the line (not a byte offset); buffer-side conversion to rope byte
8/// positions happens in `escriba-buffer`.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, JsonSchema)]
10pub struct Position {
11    pub line: u32,
12    pub column: u32,
13}
14
15impl Position {
16    pub const ZERO: Self = Self { line: 0, column: 0 };
17
18    #[must_use]
19    pub const fn new(line: u32, column: u32) -> Self {
20        Self { line, column }
21    }
22
23    #[must_use]
24    pub const fn line(line: u32) -> Self {
25        Self { line, column: 0 }
26    }
27
28    /// Saturating addition on column.
29    #[must_use]
30    pub const fn shift_right(self, n: u32) -> Self {
31        Self {
32            line: self.line,
33            column: self.column.saturating_add(n),
34        }
35    }
36
37    /// Saturating subtraction on column.
38    #[must_use]
39    pub const fn shift_left(self, n: u32) -> Self {
40        Self {
41            line: self.line,
42            column: self.column.saturating_sub(n),
43        }
44    }
45}
46
47impl PartialOrd for Position {
48    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
49        Some(self.cmp(other))
50    }
51}
52
53impl Ord for Position {
54    fn cmp(&self, other: &Self) -> Ordering {
55        self.line
56            .cmp(&other.line)
57            .then(self.column.cmp(&other.column))
58    }
59}
60
61impl std::fmt::Display for Position {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        write!(f, "{}:{}", self.line + 1, self.column + 1)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn ord_by_line_then_column() {
73        assert!(Position::new(0, 5) < Position::new(1, 0));
74        assert!(Position::new(3, 2) < Position::new(3, 4));
75        assert_eq!(
76            Position::new(2, 0).cmp(&Position::new(2, 0)),
77            Ordering::Equal
78        );
79    }
80
81    #[test]
82    fn shift_saturates() {
83        assert_eq!(Position::new(0, 0).shift_left(5), Position::new(0, 0));
84        assert_eq!(
85            Position::new(0, u32::MAX).shift_right(5),
86            Position::new(0, u32::MAX)
87        );
88    }
89
90    #[test]
91    fn display_is_one_indexed() {
92        assert_eq!(Position::new(0, 0).to_string(), "1:1");
93        assert_eq!(Position::new(9, 3).to_string(), "10:4");
94    }
95}