lattice_protocol/position.rs
1//! Logical positions and ranges within a buffer.
2//!
3//! Per §5.6.4: core, plugins, and the dispatcher deal exclusively in *logical*
4//! positions (line, byte). The renderer translates to visual positions when
5//! drawing. Plugins never see pixels.
6//!
7//! Byte offsets, not chars or UTF-16 code units: the rope and tree-sitter both
8//! index by byte, so the hot path never converts. Protocol peers that count
9//! differently (LSP's UTF-16 `character`) convert at their own boundary.
10
11use serde::{Deserialize, Serialize};
12
13/// A logical cursor position: zero-based line, zero-based byte offset within
14/// that line. UTF-8 byte offsets, not codepoint indices.
15///
16/// Ordering is lexicographic — by line, then byte — so `<` means "earlier in
17/// the buffer". Nothing here validates a position against a buffer: a byte
18/// past the end of the line, or inside a multi-byte character, is
19/// representable, and it is the consumer's job to clamp or reject it
20/// ([`ProtocolError::PositionOutOfBounds`](crate::ProtocolError::PositionOutOfBounds)).
21///
22/// # Examples
23///
24/// ```
25/// use lattice_protocol::Position;
26///
27/// // "héllo": `é` is two UTF-8 bytes, so the `l` after it is at byte 3.
28/// let l = Position::new(0, 3);
29/// assert!(Position::ZERO < l);
30/// assert!(Position::new(0, 99) < Position::new(1, 0)); // line wins
31/// ```
32#[derive(
33 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
34)]
35pub struct Position {
36 /// Zero-based line index.
37 pub line: u32,
38 /// Zero-based UTF-8 byte offset from the start of `line` (tree-sitter's
39 /// `Point.column` convention). A value equal to the line's byte length is
40 /// the end-of-line position.
41 pub byte: u32,
42}
43
44impl Position {
45 /// The start of the buffer: line 0, byte 0.
46 pub const ZERO: Position = Position { line: 0, byte: 0 };
47
48 /// A position at `line`, `byte` (both zero-based; `byte` in UTF-8 bytes).
49 pub const fn new(line: u32, byte: u32) -> Self {
50 Self { line, byte }
51 }
52}
53
54/// A half-open `[start, end)` range expressed as two `Position`s.
55///
56/// The vim-grammar `Range` (line ranges, marks, patterns, `:%`, `Selection`,
57/// custom) lives in `lattice-grammar`; this is the protocol-level structural
58/// range used by edits and decorations.
59///
60/// `start` is included, `end` is not, so `start == end` is a zero-width range
61/// (an insertion point). The constructors do not order or validate the
62/// endpoints; a well-formed range has `start <= end`, and consumers reject an
63/// inverted one ([`ProtocolError::InvalidRange`](crate::ProtocolError::InvalidRange)).
64///
65/// # Examples
66///
67/// ```
68/// use lattice_protocol::{Position, Range};
69///
70/// // The first three bytes of line 0 — "abc" in "abcdef".
71/// let abc = Range::new(Position::new(0, 0), Position::new(0, 3));
72/// assert!(!abc.is_empty());
73///
74/// // A range may span lines; `end` is exclusive.
75/// let two_lines = Range::new(Position::new(1, 4), Position::new(2, 0));
76/// assert_eq!(two_lines.end.line, 2);
77///
78/// // An insertion point.
79/// assert!(Range::empty(Position::new(3, 1)).is_empty());
80/// ```
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82pub struct Range {
83 /// First position inside the range (inclusive).
84 pub start: Position,
85 /// First position past the range (exclusive).
86 pub end: Position,
87}
88
89impl Range {
90 /// The range `[start, end)`. Endpoints are stored as given.
91 pub const fn new(start: Position, end: Position) -> Self {
92 Self { start, end }
93 }
94
95 /// A zero-width range at `at` — where an insert goes.
96 pub const fn empty(at: Position) -> Self {
97 Self { start: at, end: at }
98 }
99
100 /// `true` when `start == end`: the range covers no bytes.
101 pub fn is_empty(&self) -> bool {
102 self.start == self.end
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 #![allow(clippy::unwrap_used, clippy::panic)]
109 use super::*;
110
111 #[test]
112 fn position_zero_is_origin() {
113 assert_eq!(Position::ZERO, Position::new(0, 0));
114 assert_eq!(Position::ZERO.line, 0);
115 assert_eq!(Position::ZERO.byte, 0);
116 }
117
118 #[test]
119 fn position_constructor_sets_fields() {
120 let p = Position::new(7, 3);
121 assert_eq!(p.line, 7);
122 assert_eq!(p.byte, 3);
123 }
124
125 #[test]
126 fn position_orders_lexicographically_by_line_then_byte() {
127 assert!(Position::new(0, 5) < Position::new(1, 0));
128 assert!(Position::new(2, 1) < Position::new(2, 2));
129 assert_eq!(Position::new(3, 4), Position::new(3, 4));
130 }
131
132 #[test]
133 fn range_new_keeps_endpoints() {
134 let a = Position::new(1, 0);
135 let b = Position::new(2, 5);
136 let r = Range::new(a, b);
137 assert_eq!(r.start, a);
138 assert_eq!(r.end, b);
139 }
140
141 #[test]
142 fn range_empty_is_a_zero_width_at_position() {
143 let p = Position::new(4, 2);
144 let r = Range::empty(p);
145 assert_eq!(r.start, p);
146 assert_eq!(r.end, p);
147 assert!(r.is_empty());
148 }
149
150 #[test]
151 fn non_empty_range_is_not_empty() {
152 let r = Range::new(Position::new(0, 0), Position::new(0, 1));
153 assert!(!r.is_empty());
154 }
155
156 #[test]
157 fn ranges_are_serializable() {
158 let r = Range::new(Position::new(1, 2), Position::new(3, 4));
159 let json = serde_json::to_string(&r).unwrap();
160 let back: Range = serde_json::from_str(&json).unwrap();
161 assert_eq!(back, r);
162 }
163}