1use super::*;
33
34#[derive(Clone, Debug, PartialEq)]
35pub struct Position {
36 pub byte: usize,
37 pub char: usize,
38 pub point: Point,
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct Edit<'a> {
43 pub end_char: usize,
44 pub input_edit: InputEdit,
45 pub start_char: usize,
46 pub text: &'a str,
47}
48
49pub trait RopeExt {
50 fn apply_edit(&mut self, edit: &Edit);
51 fn build_edit<'a>(
52 &self,
53 change: &'a lsp::TextDocumentContentChangeEvent,
54 ) -> Edit<'a>;
55 fn byte_to_lsp_position(&self, byte: usize) -> lsp::Position;
56 fn lsp_position_to_position(&self, position: lsp::Position) -> Position;
57}
58
59impl RopeExt for Rope {
60 fn apply_edit(&mut self, edit: &Edit) {
63 self.remove(edit.start_char..edit.end_char);
64
65 if !edit.text.is_empty() {
66 self.insert(edit.start_char, edit.text);
67 }
68 }
69
70 fn build_edit<'a>(
73 &self,
74 change: &'a lsp::TextDocumentContentChangeEvent,
75 ) -> Edit<'a> {
76 let text = change.text.as_str();
77
78 let text_end_bytes = text.len();
79
80 let range = change.range.unwrap_or_else(|| lsp::Range {
81 start: self.byte_to_lsp_position(0),
82 end: self.byte_to_lsp_position(self.len_bytes()),
83 });
84
85 let (start, old_end) = (
86 self.lsp_position_to_position(range.start),
87 self.lsp_position_to_position(range.end),
88 );
89
90 let input_edit = InputEdit {
91 new_end_byte: start.byte + text_end_bytes,
92 new_end_position: start.point.advance(text.point_delta()),
93 old_end_byte: old_end.byte,
94 old_end_position: old_end.point,
95 start_byte: start.byte,
96 start_position: start.point,
97 };
98
99 Edit {
100 end_char: old_end.char,
101 input_edit,
102 start_char: start.char,
103 text,
104 }
105 }
106
107 fn byte_to_lsp_position(&self, byte: usize) -> lsp::Position {
110 let line = self.byte_to_line(byte);
111
112 let line_char = self.line_to_char(line);
113 let line_utf16_cu = self.char_to_utf16_cu(line_char);
114
115 let char = self.byte_to_char(byte);
116 let char_utf16_cu = self.char_to_utf16_cu(char);
117
118 lsp::Position::new(
119 u32::try_from(line).expect("line index exceeds u32::MAX"),
120 u32::try_from(char_utf16_cu - line_utf16_cu)
121 .expect("character offset exceeds u32::MAX"),
122 )
123 }
124
125 fn lsp_position_to_position(&self, position: lsp::Position) -> Position {
129 let row = position.line as usize;
130
131 let row_char = self.line_to_char(row);
132 let row_byte = self.line_to_byte(row);
133
134 let col_char = self.utf16_cu_to_char(
135 self.char_to_utf16_cu(row_char) + position.character as usize,
136 );
137
138 let col_byte = self.char_to_byte(col_char);
139
140 Position {
141 byte: col_byte,
142 char: col_char,
143 point: Point::new(row, col_byte - row_byte),
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use {super::*, pretty_assertions::assert_eq, ropey::Rope};
151
152 fn change(
153 text: &str,
154 range: lsp::Range,
155 ) -> lsp::TextDocumentContentChangeEvent {
156 lsp::TextDocumentContentChangeEvent {
157 range: Some(range),
158 range_length: None,
159 text: text.into(),
160 }
161 }
162
163 #[test]
164 fn apply_insert_into_empty_document() {
165 let mut rope = Rope::from_str("");
166
167 let change = change("🧪\nnew", lsp::Range::at(0, 0, 0, 0));
168
169 let edit = rope.build_edit(&change);
170
171 assert_eq!(
172 edit,
173 Edit {
174 start_char: 0,
175 end_char: 0,
176 input_edit: InputEdit {
177 start_byte: 0,
178 old_end_byte: 0,
179 new_end_byte: "🧪\nnew".len(),
180 start_position: Point::new(0, 0),
181 old_end_position: Point::new(0, 0),
182 new_end_position: Point::new(1, 3),
183 },
184 text: "🧪\nnew",
185 }
186 );
187
188 rope.apply_edit(&edit);
189
190 assert_eq!(rope.to_string(), "🧪\nnew");
191 }
192
193 #[test]
194 fn apply_insert_edit_updates_rope_contents() {
195 let mut rope = Rope::from_str("hello world");
196
197 let change = change("rope", lsp::Range::at(0, 6, 0, 11));
198
199 let edit = rope.build_edit(&change);
200
201 assert_eq!(
202 edit,
203 Edit {
204 start_char: 6,
205 end_char: 11,
206 input_edit: InputEdit {
207 new_end_byte: 10,
208 new_end_position: Point::new(0, 10),
209 old_end_byte: 11,
210 old_end_position: Point::new(0, 11),
211 start_byte: 6,
212 start_position: Point::new(0, 6),
213 },
214 text: "rope",
215 }
216 );
217
218 rope.apply_edit(&edit);
219
220 assert_eq!(rope.to_string(), "hello rope");
221 }
222
223 #[test]
224 fn apply_insert_edit_respects_utf16_columns() {
225 let mut rope = Rope::from_str("ab");
226
227 let change = change("🧪", lsp::Range::at(0, 1, 0, 1));
228
229 let edit = rope.build_edit(&change);
230
231 assert_eq!(
232 edit,
233 Edit {
234 start_char: 1,
235 end_char: 1,
236 input_edit: InputEdit {
237 new_end_byte: 5,
238 new_end_position: Point::new(0, 5),
239 old_end_byte: 1,
240 old_end_position: Point::new(0, 1),
241 start_byte: 1,
242 start_position: Point::new(0, 1),
243 },
244 text: "🧪",
245 }
246 );
247
248 rope.apply_edit(&edit);
249
250 assert_eq!(rope.to_string(), "a🧪b");
251 }
252
253 #[test]
254 fn apply_delete_edit_respects_utf16_columns() {
255 let mut rope = Rope::from_str("a😊b");
256
257 let change = change("", lsp::Range::at(0, 1, 0, 3));
258
259 let edit = rope.build_edit(&change);
260
261 assert_eq!(
262 edit,
263 Edit {
264 start_char: 1,
265 end_char: 2,
266 input_edit: InputEdit {
267 new_end_byte: 1,
268 new_end_position: Point::new(0, 1),
269 old_end_byte: 5,
270 old_end_position: Point::new(0, 5),
271 start_byte: 1,
272 start_position: Point::new(0, 1),
273 },
274 text: "",
275 }
276 );
277
278 rope.apply_edit(&edit);
279
280 assert_eq!(rope.to_string(), "ab");
281 }
282
283 #[test]
284 fn lsp_round_trip_handles_utf16_columns() {
285 let rope = Rope::from_str("a😊b\nsecond");
286
287 let position = rope.byte_to_lsp_position(5);
288
289 assert_eq!(position, lsp::Position::new(0, 3));
290
291 assert_eq!(
292 rope.lsp_position_to_position(position),
293 Position {
294 byte: 5,
295 char: 2,
296 point: Point::new(0, 5),
297 }
298 );
299 }
300
301 #[test]
302 fn replacement_across_surrogates_is_consistent() {
303 let mut rope = Rope::from_str("foo😊bar");
304
305 let change = change("🧪", lsp::Range::at(0, 3, 0, 5));
306
307 let edit = rope.build_edit(&change);
308
309 assert_eq!(
310 edit,
311 Edit {
312 start_char: 3,
313 end_char: 4,
314 input_edit: InputEdit {
315 start_byte: 3,
316 old_end_byte: 7,
317 new_end_byte: 7,
318 start_position: Point::new(0, 3),
319 old_end_position: Point::new(0, 7),
320 new_end_position: Point::new(0, 7),
321 },
322 text: "🧪",
323 }
324 );
325
326 rope.apply_edit(&edit);
327
328 assert_eq!(rope.to_string(), "foo🧪bar");
329 }
330
331 #[test]
332 fn multiline_edit_handles_utf16_offsets() {
333 let mut rope = Rope::from_str("foo😊\nbar");
334
335 let change = change("XX", lsp::Range::at(0, 2, 1, 1));
336
337 let edit = rope.build_edit(&change);
338
339 assert_eq!(
340 edit,
341 Edit {
342 start_char: 2,
343 end_char: 6,
344 input_edit: InputEdit {
345 start_byte: 2,
346 old_end_byte: 9,
347 new_end_byte: 4,
348 start_position: Point::new(0, 2),
349 old_end_position: Point::new(1, 1),
350 new_end_position: Point::new(0, 4),
351 },
352 text: "XX",
353 }
354 );
355
356 rope.apply_edit(&edit);
357
358 assert_eq!(rope.to_string(), "foXXar");
359 }
360
361 #[test]
362 fn append_beyond_eof_updates_point() {
363 let mut rope = Rope::from_str("hi");
364
365 let change = change("🧪\nnew", lsp::Range::at(0, 2, 0, 2));
366
367 let edit = rope.build_edit(&change);
368
369 assert_eq!(
370 edit,
371 Edit {
372 start_char: 2,
373 end_char: 2,
374 input_edit: InputEdit {
375 start_byte: 2,
376 old_end_byte: 2,
377 new_end_byte: 10,
378 start_position: Point::new(0, 2),
379 old_end_position: Point::new(0, 2),
380 new_end_position: Point::new(1, 3),
381 },
382 text: "🧪\nnew",
383 }
384 );
385
386 rope.apply_edit(&edit);
387
388 assert_eq!(rope.to_string(), "hi🧪\nnew");
389 }
390
391 #[test]
392 fn replace_entire_document_via_full_range() {
393 let mut rope = Rope::from_str("foo😊bar");
394
395 let change = lsp::TextDocumentContentChangeEvent {
396 range: None,
397 range_length: None,
398 text: "🧪baz".into(),
399 };
400
401 let edit = rope.build_edit(&change);
402
403 assert_eq!(
404 edit,
405 Edit {
406 start_char: 0,
407 end_char: 7,
408 input_edit: InputEdit {
409 start_byte: 0,
410 old_end_byte: 10,
411 new_end_byte: 7,
412 start_position: Point::new(0, 0),
413 old_end_position: Point::new(0, 10),
414 new_end_position: Point::new(0, 7),
415 },
416 text: "🧪baz",
417 }
418 );
419
420 rope.apply_edit(&edit);
421
422 assert_eq!(rope.to_string(), "🧪baz");
423 }
424}