logicaffeine_lsp/
line_index.rs1use tower_lsp::lsp_types::Position;
2
3#[derive(Debug, Clone)]
9pub struct LineIndex {
10 line_starts: Vec<usize>,
12 source: String,
14}
15
16impl LineIndex {
17 pub fn new(source: &str) -> Self {
18 let mut line_starts = vec![0];
19 for (i, b) in source.bytes().enumerate() {
20 if b == b'\n' {
21 line_starts.push(i + 1);
22 }
23 }
24 LineIndex {
25 line_starts,
26 source: source.to_string(),
27 }
28 }
29
30 pub fn position(&self, byte_offset: usize) -> Position {
34 let byte_offset = byte_offset.min(self.source.len());
35
36 let line = self
37 .line_starts
38 .partition_point(|&start| start <= byte_offset)
39 .saturating_sub(1);
40
41 let line_start = self.line_starts[line];
42 let line_text = &self.source[line_start..byte_offset];
43 let character = line_text.encode_utf16().count() as u32;
44
45 Position {
46 line: line as u32,
47 character,
48 }
49 }
50
51 pub fn line_start_offset(&self, line: usize) -> usize {
54 self.line_starts
55 .get(line)
56 .copied()
57 .unwrap_or(self.source.len())
58 }
59
60 pub fn utf16_length(&self, byte_start: usize, byte_end: usize) -> u32 {
62 let start = byte_start.min(self.source.len());
63 let end = byte_end.min(self.source.len());
64 if start >= end {
65 return 0;
66 }
67 self.source[start..end].encode_utf16().count() as u32
68 }
69
70 pub fn offset(&self, position: Position) -> usize {
72 let line = position.line as usize;
73 if line >= self.line_starts.len() {
74 return self.source.len();
75 }
76
77 let line_start = self.line_starts[line];
78 let line_end = self
79 .line_starts
80 .get(line + 1)
81 .copied()
82 .unwrap_or(self.source.len());
83
84 let line_text = &self.source[line_start..line_end];
85 let mut utf16_offset = 0u32;
86 let target = position.character;
87
88 for (byte_idx, ch) in line_text.char_indices() {
89 if utf16_offset >= target {
90 return line_start + byte_idx;
91 }
92 utf16_offset += ch.len_utf16() as u32;
93 }
94
95 line_end
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn single_line() {
105 let idx = LineIndex::new("hello world");
106 assert_eq!(idx.position(0), Position { line: 0, character: 0 });
107 assert_eq!(idx.position(5), Position { line: 0, character: 5 });
108 assert_eq!(idx.position(11), Position { line: 0, character: 11 });
109 }
110
111 #[test]
112 fn multi_line() {
113 let idx = LineIndex::new("abc\ndef\nghi");
114 assert_eq!(idx.position(0), Position { line: 0, character: 0 });
115 assert_eq!(idx.position(3), Position { line: 0, character: 3 });
116 assert_eq!(idx.position(4), Position { line: 1, character: 0 });
117 assert_eq!(idx.position(7), Position { line: 1, character: 3 });
118 assert_eq!(idx.position(8), Position { line: 2, character: 0 });
119 }
120
121 #[test]
122 fn roundtrip() {
123 let src = "Let x be 5.\nSet x to 10.\nShow x.\n";
124 let idx = LineIndex::new(src);
125 for offset in 0..src.len() {
126 let pos = idx.position(offset);
127 let back = idx.offset(pos);
128 assert_eq!(back, offset, "roundtrip failed at offset {offset}");
129 }
130 }
131
132 #[test]
133 fn offset_from_position() {
134 let idx = LineIndex::new("abc\ndef\nghi");
135 assert_eq!(idx.offset(Position { line: 0, character: 0 }), 0);
136 assert_eq!(idx.offset(Position { line: 1, character: 0 }), 4);
137 assert_eq!(idx.offset(Position { line: 2, character: 2 }), 10);
138 }
139
140 #[test]
141 fn empty_source() {
142 let idx = LineIndex::new("");
143 assert_eq!(idx.position(0), Position { line: 0, character: 0 });
144 assert_eq!(idx.offset(Position { line: 0, character: 0 }), 0);
145 }
146
147 #[test]
148 fn out_of_bounds_offset() {
149 let idx = LineIndex::new("abc");
150 let pos = idx.position(100);
151 assert_eq!(pos, Position { line: 0, character: 3 });
152 }
153
154 #[test]
155 fn out_of_bounds_position() {
156 let idx = LineIndex::new("abc");
157 let offset = idx.offset(Position { line: 5, character: 0 });
158 assert_eq!(offset, 3);
159 }
160
161 #[test]
162 fn line_start_offset_returns_correct_values() {
163 let idx = LineIndex::new("abc\ndef\nghi");
164 assert_eq!(idx.line_start_offset(0), 0);
165 assert_eq!(idx.line_start_offset(1), 4);
166 assert_eq!(idx.line_start_offset(2), 8);
167 }
168
169 #[test]
170 fn line_start_offset_out_of_bounds() {
171 let idx = LineIndex::new("abc\ndef");
172 assert_eq!(idx.line_start_offset(99), 7);
174 }
175
176 #[test]
177 fn windows_line_endings() {
178 let src = "abc\r\ndef\r\nghi";
179 let idx = LineIndex::new(src);
180 let pos = idx.position(5);
182 assert_eq!(pos, Position { line: 1, character: 0 });
183 let back = idx.offset(pos);
184 assert_eq!(back, 5);
185 }
186
187 #[test]
188 fn multibyte_utf8_roundtrip() {
189 let src = "café\nworld";
191 let idx = LineIndex::new(src);
192 let pos_e_accent = idx.position(3);
194 assert_eq!(pos_e_accent.line, 0);
195 assert_eq!(pos_e_accent.character, 3, "UTF-16 offset of 'é' should be 3");
196 let pos_world = idx.position(6);
197 assert_eq!(pos_world, Position { line: 1, character: 0 });
198 let back = idx.offset(pos_world);
199 assert_eq!(back, 6);
200 }
201}