1use crate::encoding::PositionEncoding;
4use lsp_types::Position;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct LineIndex {
33 line_starts: Vec<usize>,
34 len: usize,
35}
36
37impl LineIndex {
38 pub fn new(text: &str) -> Self {
40 let mut line_starts = vec![0usize];
41 for (i, byte) in text.bytes().enumerate() {
42 if byte == b'\n' {
43 line_starts.push(i + 1);
44 }
45 }
46 Self {
47 line_starts,
48 len: text.len(),
49 }
50 }
51
52 pub fn line_count(&self) -> usize {
54 self.line_starts.len()
55 }
56
57 pub fn line_start(&self, line: u32) -> Option<usize> {
59 self.line_starts.get(line as usize).copied()
60 }
61
62 pub fn line_end(&self, text: &str, line: u32) -> Option<usize> {
65 let start = self.line_start(line)?;
66 let next = self
67 .line_starts
68 .get(line as usize + 1)
69 .copied()
70 .unwrap_or(self.len);
71 let mut end = next;
72 if end > start && text.as_bytes()[end - 1] == b'\n' {
73 end -= 1;
74 }
75 Some(end)
76 }
77
78 pub fn offset(
84 &self,
85 text: &str,
86 position: Position,
87 encoding: PositionEncoding,
88 ) -> Option<usize> {
89 let line = position.line.min(self.line_count() as u32 - 1);
90 let start = self.line_start(line)?;
91 let end = self.line_end(text, line)?;
92 let line_text = text
93 .get(start..end)
94 .unwrap_or_else(|| text.get(start..).unwrap_or(""));
95 Some(start + encoding.offset_of_units(line_text, position.character))
96 }
97
98 pub fn position(&self, text: &str, offset: usize, encoding: PositionEncoding) -> Position {
104 let offset = offset.min(self.len);
105 let line = self
106 .line_starts
107 .partition_point(|&start| start <= offset)
108 .saturating_sub(1);
109 let start = self.line_starts[line];
110 let mut end = offset;
111 while end > start && !text.is_char_boundary(end) {
112 end -= 1;
113 }
114 let line_text = text.get(start..end).unwrap_or("");
115 Position {
116 line: line as u32,
117 character: encoding.units_of(line_text),
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 const TEXT: &str = "abc\ndefé\n\nx😀\n";
127
128 #[test]
129 fn line_structure() {
130 let index = LineIndex::new(TEXT);
131 assert_eq!(index.line_count(), 5);
132 assert_eq!(index.line_start(0), Some(0));
133 assert_eq!(index.line_start(1), Some(4));
134 assert_eq!(index.line_start(2), Some(10));
135 assert_eq!(index.line_start(3), Some(11));
136 assert_eq!(index.line_start(4), Some(17));
137 assert_eq!(index.line_start(5), None);
138 assert_eq!(index.line_end(TEXT, 0), Some(3));
139 assert_eq!(index.line_end(TEXT, 1), Some(9));
140 assert_eq!(index.line_end(TEXT, 2), Some(10));
141 assert_eq!(index.line_end(TEXT, 3), Some(16));
142 assert_eq!(index.line_end(TEXT, 4), Some(17));
143 }
144
145 #[test]
146 fn round_trip_utf16() {
147 let index = LineIndex::new(TEXT);
148 let enc = PositionEncoding::Utf16;
149
150 let pos = Position {
152 line: 3,
153 character: 1,
154 };
155 assert_eq!(index.offset(TEXT, pos, enc), Some(12));
156 assert_eq!(index.position(TEXT, 12, enc), pos);
157
158 let overshoot = Position {
161 line: 3,
162 character: 2,
163 };
164 assert_eq!(index.offset(TEXT, overshoot, enc), Some(16));
165 assert_eq!(
166 index.position(TEXT, 16, enc),
167 Position {
168 line: 3,
169 character: 3
170 }
171 );
172 }
173
174 #[test]
175 fn round_trip_utf8_and_utf32() {
176 let index = LineIndex::new(TEXT);
177
178 assert_eq!(
180 index.offset(
181 TEXT,
182 Position {
183 line: 3,
184 character: 2
185 },
186 PositionEncoding::Utf8
187 ),
188 Some(13)
189 );
190 assert_eq!(
192 index.position(TEXT, 13, PositionEncoding::Utf8),
193 Position {
194 line: 3,
195 character: 1
196 }
197 );
198
199 assert_eq!(
201 index.offset(
202 TEXT,
203 Position {
204 line: 3,
205 character: 2
206 },
207 PositionEncoding::Utf32
208 ),
209 Some(16)
210 );
211 assert_eq!(
212 index.position(TEXT, 16, PositionEncoding::Utf32),
213 Position {
214 line: 3,
215 character: 2
216 }
217 );
218 }
219
220 #[test]
221 fn out_of_range_clamps() {
222 let index = LineIndex::new(TEXT);
223 let enc = PositionEncoding::Utf16;
224
225 assert_eq!(
226 index.offset(
227 TEXT,
228 Position {
229 line: 99,
230 character: 0
231 },
232 enc
233 ),
234 Some(17)
235 );
236 assert_eq!(
237 index.offset(
238 TEXT,
239 Position {
240 line: 0,
241 character: 99
242 },
243 enc
244 ),
245 Some(3)
246 );
247 assert_eq!(
248 index.position(TEXT, 999, enc),
249 Position {
250 line: 4,
251 character: 0
252 }
253 );
254 }
255
256 #[test]
257 fn empty_text_degenerates() {
258 let index = LineIndex::new("");
259 assert_eq!(index.line_count(), 1);
260 assert_eq!(
261 index.offset(
262 "",
263 Position {
264 line: 0,
265 character: 0
266 },
267 PositionEncoding::Utf16
268 ),
269 Some(0)
270 );
271 assert_eq!(
272 index.position("", 0, PositionEncoding::Utf16),
273 Position {
274 line: 0,
275 character: 0
276 }
277 );
278 }
279}