1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use std::collections::HashMap;
5
6use i_slint_compiler::parser::TextSize;
7
8use crate::common;
9
10#[derive(Clone, Debug)]
11pub struct TextOffsetAdjustment {
12 pub start_offset: TextSize,
13 pub end_offset: TextSize,
14 pub new_text_length: u32,
15}
16
17impl TextOffsetAdjustment {
18 pub fn new(
19 edit: &lsp_types::TextEdit,
20 source_file: &i_slint_compiler::diagnostics::SourceFile,
21 ) -> Self {
22 let new_text_length = edit.new_text.len() as u32;
23 let (start_offset, end_offset) = {
24 let so = source_file.offset(
25 edit.range.start.line as usize + 1,
26 edit.range.start.character as usize + 1,
27 );
28 let eo = source_file
29 .offset(edit.range.end.line as usize + 1, edit.range.end.character as usize + 1);
30 (
31 TextSize::new(std::cmp::min(so, eo) as u32),
32 TextSize::new(std::cmp::max(so, eo) as u32),
33 )
34 };
35
36 Self { start_offset, end_offset, new_text_length }
37 }
38
39 pub fn adjust(&self, offset: TextSize) -> TextSize {
40 // This is a bit simplistic... Worst case: Some unexpected element gets selected. We can live with that.
41
42 debug_assert!(self.end_offset >= self.start_offset);
43 let old_length = self.end_offset - self.start_offset;
44
45 if offset >= self.end_offset {
46 offset + TextSize::new(self.new_text_length) - old_length
47 } else if offset >= self.start_offset {
48 ((u32::from(offset) as i64 + self.new_text_length as i64 - u32::from(old_length) as i64)
49 .clamp(
50 u32::from(self.start_offset) as i64,
51 u32::from(
52 self.end_offset
53 .min(self.start_offset + TextSize::new(self.new_text_length)),
54 ) as i64,
55 ) as u32)
56 .into()
57 } else {
58 offset
59 }
60 }
61}
62
63#[derive(Clone, Default)]
64pub struct TextOffsetAdjustments(Vec<TextOffsetAdjustment>);
65
66impl TextOffsetAdjustments {
67 pub fn add_adjustment(&mut self, adjustment: TextOffsetAdjustment) {
68 self.0.push(adjustment);
69 }
70
71 pub fn adjust(&self, input: TextSize) -> TextSize {
72 let input_: i64 = i64::from(u32::from(input));
73 let total_adjustment: i64 = self
74 .0
75 .iter()
76 .fold(init:0_i64, |acc: i64, a: &TextOffsetAdjustment| acc + i64::from(u32::from(a.adjust(offset:input))) - input_);
77 ((input_ + total_adjustment) as u32).into()
78 }
79
80 pub fn is_empty(&self) -> bool {
81 self.0.is_empty()
82 }
83}
84
85#[derive(Clone)]
86enum EditIteratorState<'a> {
87 Changes { urls: Vec<&'a lsp_types::Url>, main_index: usize, index: usize },
88 DocumentChanges { main_index: usize, index: usize },
89 Done,
90}
91
92#[derive(Clone)]
93pub struct EditIterator<'a> {
94 workspace_edit: &'a lsp_types::WorkspaceEdit,
95 state: EditIteratorState<'a>,
96}
97
98impl<'a> EditIterator<'a> {
99 pub fn new(workspace_edit: &'a lsp_types::WorkspaceEdit) -> Self {
100 Self {
101 workspace_edit,
102 state: EditIteratorState::Changes {
103 urls: workspace_editOption>
104 .changes
105 .as_ref()
106 .map(|hm: &HashMap>| hm.keys().collect::<Vec<_>>())
107 .unwrap_or_default(),
108 main_index: 0,
109 index: 0,
110 },
111 }
112 }
113}
114
115impl<'a> Iterator for EditIterator<'a> {
116 type Item = (lsp_types::OptionalVersionedTextDocumentIdentifier, &'a lsp_types::TextEdit);
117
118 fn next(&mut self) -> Option<Self::Item> {
119 match &mut self.state {
120 EditIteratorState::Changes { urls, main_index, index } => {
121 if let Some(changes) = &self.workspace_edit.changes {
122 if let Some(uri) = urls.get(*main_index) {
123 if let Some(edits) = changes.get(uri) {
124 if let Some(edit) = edits.get(*index) {
125 *index += 1;
126 return Some((
127 lsp_types::OptionalVersionedTextDocumentIdentifier {
128 uri: (*uri).clone(),
129 version: None,
130 },
131 edit,
132 ));
133 } else {
134 *index = 0;
135 *main_index += 1;
136 return self.next();
137 }
138 }
139 }
140 }
141
142 self.state = EditIteratorState::DocumentChanges { main_index: 0, index: 0 };
143 self.next()
144 }
145 EditIteratorState::DocumentChanges { main_index, index } => {
146 if let Some(lsp_types::DocumentChanges::Edits(edits)) =
147 &self.workspace_edit.document_changes
148 {
149 if let Some(doc_edit) = edits.get(*main_index) {
150 if let Some(edit) = doc_edit.edits.get(*index) {
151 *index += 1;
152 let te = match edit {
153 lsp_types::OneOf::Left(te) => te,
154 lsp_types::OneOf::Right(ate) => &ate.text_edit,
155 };
156 return Some((doc_edit.text_document.clone(), te));
157 } else {
158 *index = 0;
159 *main_index += 1;
160 return self.next();
161 }
162 }
163 }
164
165 self.state = EditIteratorState::Done;
166 None
167 }
168 EditIteratorState::Done => None,
169 }
170 }
171}
172
173#[derive(Clone)]
174pub struct TextEditor {
175 source_file: i_slint_compiler::diagnostics::SourceFile,
176 contents: String,
177 original_offset_range: (usize, usize),
178 adjustments: TextOffsetAdjustments,
179}
180
181impl TextEditor {
182 pub fn new(source_file: i_slint_compiler::diagnostics::SourceFile) -> crate::Result<Self> {
183 let Some(contents) = source_file.source().map(|s| s.to_string()) else {
184 return Err(format!("Source file {:?} had no contents set", source_file.path()).into());
185 };
186 Ok(Self {
187 source_file,
188 contents,
189 original_offset_range: (usize::MAX, 0),
190 adjustments: TextOffsetAdjustments::default(),
191 })
192 }
193
194 pub fn apply(&mut self, text_edit: &lsp_types::TextEdit) -> crate::Result<()> {
195 let current_offset = {
196 let start_range = &text_edit.range.start;
197 let end_range = &text_edit.range.end;
198 let start_offset = self
199 .source_file
200 .offset(start_range.line as usize + 1, start_range.character as usize + 1);
201 let end_offset = self
202 .source_file
203 .offset(end_range.line as usize + 1, end_range.character as usize + 1);
204 (start_offset, end_offset)
205 };
206
207 let adjusted_offset = (
208 usize::from(self.adjustments.adjust((current_offset.0 as u32).into())),
209 usize::from(self.adjustments.adjust((current_offset.1 as u32).into())),
210 );
211
212 if self.contents.len() < adjusted_offset.1 {
213 return Err("Text edit range is out of bounds".into());
214 }
215
216 // Book keeping:
217 self.original_offset_range.0 = self.original_offset_range.0.min(current_offset.0);
218 self.original_offset_range.1 = self.original_offset_range.1.max(current_offset.1);
219
220 self.contents.replace_range((adjusted_offset.0)..(adjusted_offset.1), &text_edit.new_text);
221
222 self.adjustments.add_adjustment(TextOffsetAdjustment::new(text_edit, &self.source_file));
223
224 Ok(())
225 }
226
227 pub fn finalize(self) -> Option<(String, TextOffsetAdjustments, (usize, usize))> {
228 if self.source_file.source() == Some(&self.contents) {
229 None
230 } else {
231 (!self.adjustments.is_empty()).then_some((
232 self.contents,
233 self.adjustments,
234 self.original_offset_range,
235 ))
236 }
237 }
238}
239
240pub struct EditedText {
241 pub url: lsp_types::Url,
242 pub contents: String,
243}
244
245pub fn apply_workspace_edit(
246 document_cache: &common::DocumentCache,
247 workspace_edit: &lsp_types::WorkspaceEdit,
248) -> common::Result<Vec<EditedText>> {
249 let mut processing = HashMap::new();
250
251 for (doc, edit) in EditIterator::new(workspace_edit) {
252 // This is ugly but necessary since the constructor might error out:-/
253 if !processing.contains_key(&doc.uri) {
254 let Some(document) = document_cache.get_document(&doc.uri) else {
255 continue;
256 };
257 let Some(document_node) = &document.node else {
258 continue;
259 };
260 let editor = TextEditor::new(document_node.source_file.clone())?;
261 processing.insert(doc.uri.clone(), editor);
262 }
263
264 processing.get_mut(&doc.uri).expect("just added if missing").apply(edit)?;
265 }
266
267 Ok(processing
268 .drain()
269 .filter_map(|(url, v)| {
270 let edit_result = v.finalize()?;
271 Some(EditedText { url, contents: edit_result.0 })
272 })
273 .collect())
274}
275
276#[test]
277fn test_text_offset_adjustments() {
278 let mut a = TextOffsetAdjustments::default();
279 // same length change
280 a.add_adjustment(TextOffsetAdjustment {
281 start_offset: 10.into(),
282 end_offset: 20.into(),
283 new_text_length: 10,
284 });
285 // insert
286 a.add_adjustment(TextOffsetAdjustment {
287 start_offset: 25.into(),
288 end_offset: 25.into(),
289 new_text_length: 1,
290 });
291 // smaller replacement
292 a.add_adjustment(TextOffsetAdjustment {
293 start_offset: 30.into(),
294 end_offset: 40.into(),
295 new_text_length: 5,
296 });
297 // longer replacement
298 a.add_adjustment(TextOffsetAdjustment {
299 start_offset: 50.into(),
300 end_offset: 60.into(),
301 new_text_length: 20,
302 });
303 // deletion
304 a.add_adjustment(TextOffsetAdjustment {
305 start_offset: 70.into(),
306 end_offset: 80.into(),
307 new_text_length: 0,
308 });
309
310 assert_eq!(a.adjust(0.into()), 0.into());
311 assert_eq!(a.adjust(20.into()), 20.into());
312 assert_eq!(a.adjust(25.into()), 26.into());
313 assert_eq!(a.adjust(30.into()), 31.into());
314 assert_eq!(a.adjust(40.into()), 36.into());
315 assert_eq!(a.adjust(60.into()), 66.into());
316 assert_eq!(a.adjust(70.into()), 76.into());
317 assert_eq!(a.adjust(80.into()), 76.into());
318}
319
320#[test]
321fn test_text_offset_adjustments_reverse() {
322 let mut a = TextOffsetAdjustments::default();
323 // deletion
324 a.add_adjustment(TextOffsetAdjustment {
325 start_offset: 70.into(),
326 end_offset: 80.into(),
327 new_text_length: 0,
328 });
329 // longer replacement
330 a.add_adjustment(TextOffsetAdjustment {
331 start_offset: 50.into(),
332 end_offset: 60.into(),
333 new_text_length: 20,
334 });
335 // smaller replacement
336 a.add_adjustment(TextOffsetAdjustment {
337 start_offset: 30.into(),
338 end_offset: 40.into(),
339 new_text_length: 5,
340 });
341 // insert
342 a.add_adjustment(TextOffsetAdjustment {
343 start_offset: 25.into(),
344 end_offset: 25.into(),
345 new_text_length: 1,
346 });
347 // same length change
348 a.add_adjustment(TextOffsetAdjustment {
349 start_offset: 10.into(),
350 end_offset: 20.into(),
351 new_text_length: 10,
352 });
353
354 assert_eq!(a.adjust(0.into()), 0.into());
355 assert_eq!(a.adjust(20.into()), 20.into());
356 assert_eq!(a.adjust(25.into()), 26.into());
357 assert_eq!(a.adjust(30.into()), 31.into());
358 assert_eq!(a.adjust(40.into()), 36.into());
359 assert_eq!(a.adjust(60.into()), 66.into());
360 assert_eq!(a.adjust(70.into()), 76.into());
361 assert_eq!(a.adjust(80.into()), 76.into());
362}
363
364#[test]
365fn test_edit_iterator_empty() {
366 let workspace_edit = lsp_types::WorkspaceEdit {
367 changes: None,
368 document_changes: None,
369 change_annotations: None,
370 };
371
372 let mut it = EditIterator::new(&workspace_edit);
373 assert!(it.next().is_none());
374 assert!(it.next().is_none());
375}
376
377#[test]
378fn test_edit_iterator_changes_one_empty() {
379 let workspace_edit = lsp_types::WorkspaceEdit {
380 changes: Some(std::collections::HashMap::from([(
381 lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
382 vec![],
383 )])),
384 document_changes: None,
385 change_annotations: None,
386 };
387
388 let mut it = EditIterator::new(&workspace_edit);
389 assert!(it.next().is_none());
390 assert!(it.next().is_none());
391}
392
393#[test]
394fn test_edit_iterator_changes_one_one() {
395 let workspace_edit = lsp_types::WorkspaceEdit {
396 changes: Some(std::collections::HashMap::from([(
397 lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
398 vec![lsp_types::TextEdit {
399 range: lsp_types::Range::new(
400 lsp_types::Position::new(22, 41),
401 lsp_types::Position::new(41, 22),
402 ),
403 new_text: "Replacement".to_string(),
404 }],
405 )])),
406 document_changes: None,
407 change_annotations: None,
408 };
409
410 let mut it = EditIterator::new(&workspace_edit);
411 let r = it.next().unwrap();
412 assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
413 assert_eq!(r.0.version, None);
414 assert_eq!(&r.1.new_text, "Replacement");
415 assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
416 assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
417 assert!(it.next().is_none());
418 assert!(it.next().is_none());
419}
420
421#[test]
422fn test_edit_iterator_changes_one_two() {
423 let workspace_edit = lsp_types::WorkspaceEdit {
424 changes: Some(std::collections::HashMap::from([(
425 lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
426 vec![
427 lsp_types::TextEdit {
428 range: lsp_types::Range::new(
429 lsp_types::Position::new(22, 41),
430 lsp_types::Position::new(41, 22),
431 ),
432 new_text: "Replacement".to_string(),
433 },
434 lsp_types::TextEdit {
435 range: lsp_types::Range::new(
436 lsp_types::Position::new(43, 11),
437 lsp_types::Position::new(43, 12),
438 ),
439 new_text: "Foo".to_string(),
440 },
441 ],
442 )])),
443 document_changes: None,
444 change_annotations: None,
445 };
446
447 let mut it = EditIterator::new(&workspace_edit);
448
449 let r = it.next().unwrap();
450 assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
451 assert_eq!(r.0.version, None);
452 assert_eq!(&r.1.new_text, "Replacement");
453 assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
454 assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
455
456 let r = it.next().unwrap();
457 assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
458 assert_eq!(r.0.version, None);
459 assert_eq!(&r.1.new_text, "Foo");
460 assert_eq!(&r.1.range.start, &lsp_types::Position::new(43, 11));
461 assert_eq!(&r.1.range.end, &lsp_types::Position::new(43, 12));
462
463 assert!(it.next().is_none());
464}
465
466#[test]
467fn test_edit_iterator_changes_two() {
468 let workspace_edit = lsp_types::WorkspaceEdit {
469 changes: Some(std::collections::HashMap::from([
470 (
471 lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
472 vec![lsp_types::TextEdit {
473 range: lsp_types::Range::new(
474 lsp_types::Position::new(22, 41),
475 lsp_types::Position::new(41, 22),
476 ),
477 new_text: "Replacement".to_string(),
478 }],
479 ),
480 (
481 lsp_types::Url::parse("file://foo/baz.slint").unwrap(),
482 vec![lsp_types::TextEdit {
483 range: lsp_types::Range::new(
484 lsp_types::Position::new(43, 11),
485 lsp_types::Position::new(43, 12),
486 ),
487 new_text: "Foo".to_string(),
488 }],
489 ),
490 ])),
491 document_changes: None,
492 change_annotations: None,
493 };
494
495 let mut seen1 = false;
496 let mut seen2 = false;
497
498 for r in EditIterator::new(&workspace_edit) {
499 // random order!
500 if r.0.uri.to_string() == "file://foo/bar.slint" {
501 assert!(!seen1);
502 assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
503 assert_eq!(r.0.version, None);
504 assert_eq!(&r.1.new_text, "Replacement");
505 assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
506 assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
507 seen1 = true;
508 } else {
509 assert!(!seen2);
510 assert_eq!(&r.0.uri.to_string(), "file://foo/baz.slint");
511 assert_eq!(r.0.version, None);
512 assert_eq!(&r.1.new_text, "Foo");
513 assert_eq!(&r.1.range.start, &lsp_types::Position::new(43, 11));
514 assert_eq!(&r.1.range.end, &lsp_types::Position::new(43, 12));
515 seen2 = true;
516 }
517 }
518 assert!(seen1 && seen2);
519}
520
521#[test]
522fn test_edit_iterator_document_changes_empty() {
523 let workspace_edit = lsp_types::WorkspaceEdit {
524 changes: None,
525 document_changes: Some(lsp_types::DocumentChanges::Edits(vec![])),
526 change_annotations: None,
527 };
528
529 let mut it = EditIterator::new(&workspace_edit);
530 assert!(it.next().is_none());
531 assert!(it.next().is_none());
532}
533
534#[test]
535fn test_edit_iterator_document_changes_operations() {
536 let workspace_edit = lsp_types::WorkspaceEdit {
537 changes: None,
538 document_changes: Some(lsp_types::DocumentChanges::Operations(vec![])),
539 change_annotations: None,
540 };
541
542 let mut it = EditIterator::new(&workspace_edit);
543 assert!(it.next().is_none());
544 assert!(it.next().is_none());
545}
546
547#[test]
548fn test_edit_iterator_document_changes_one_empty() {
549 let workspace_edit = lsp_types::WorkspaceEdit {
550 changes: None,
551 document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
552 lsp_types::TextDocumentEdit {
553 edits: vec![],
554 text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
555 uri: lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
556 version: Some(99),
557 },
558 },
559 ])),
560 change_annotations: None,
561 };
562
563 let mut it = EditIterator::new(&workspace_edit);
564 assert!(it.next().is_none());
565 assert!(it.next().is_none());
566}
567
568#[test]
569fn test_edit_iterator_document_changes_one_one() {
570 let workspace_edit = lsp_types::WorkspaceEdit {
571 changes: None,
572 document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
573 lsp_types::TextDocumentEdit {
574 edits: vec![lsp_types::OneOf::Left(lsp_types::TextEdit {
575 range: lsp_types::Range::new(
576 lsp_types::Position::new(22, 41),
577 lsp_types::Position::new(41, 22),
578 ),
579 new_text: "Replacement".to_string(),
580 })],
581 text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
582 uri: lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
583 version: Some(99),
584 },
585 },
586 ])),
587 change_annotations: None,
588 };
589
590 let mut it = EditIterator::new(&workspace_edit);
591 let r = it.next().unwrap();
592 assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
593 assert_eq!(r.0.version, Some(99));
594 assert_eq!(&r.1.new_text, "Replacement");
595 assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
596 assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
597 assert!(it.next().is_none());
598 assert!(it.next().is_none());
599}
600
601#[test]
602fn test_edit_iterator_document_changes_one_two() {
603 let workspace_edit = lsp_types::WorkspaceEdit {
604 changes: None,
605 document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
606 lsp_types::TextDocumentEdit {
607 edits: vec![
608 lsp_types::OneOf::Left(lsp_types::TextEdit {
609 range: lsp_types::Range::new(
610 lsp_types::Position::new(22, 41),
611 lsp_types::Position::new(41, 22),
612 ),
613 new_text: "Replacement".to_string(),
614 }),
615 lsp_types::OneOf::Right(lsp_types::AnnotatedTextEdit {
616 text_edit: lsp_types::TextEdit {
617 range: lsp_types::Range::new(
618 lsp_types::Position::new(43, 11),
619 lsp_types::Position::new(43, 12),
620 ),
621 new_text: "Foo".to_string(),
622 },
623 annotation_id: "CID".to_string(),
624 }),
625 ],
626 text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
627 uri: lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
628 version: Some(99),
629 },
630 },
631 ])),
632 change_annotations: None,
633 };
634
635 let mut it = EditIterator::new(&workspace_edit);
636 let r = it.next().unwrap();
637 assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
638 assert_eq!(r.0.version, Some(99));
639 assert_eq!(&r.1.new_text, "Replacement");
640 assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
641 assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
642
643 let r = it.next().unwrap();
644 assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
645 assert_eq!(r.0.version, Some(99));
646 assert_eq!(&r.1.new_text, "Foo");
647 assert_eq!(&r.1.range.start, &lsp_types::Position::new(43, 11));
648 assert_eq!(&r.1.range.end, &lsp_types::Position::new(43, 12));
649 assert!(it.next().is_none());
650 assert!(it.next().is_none());
651}
652
653#[test]
654fn test_edit_iterator_document_changes_two() {
655 let workspace_edit = lsp_types::WorkspaceEdit {
656 changes: None,
657 document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
658 lsp_types::TextDocumentEdit {
659 edits: vec![lsp_types::OneOf::Left(lsp_types::TextEdit {
660 range: lsp_types::Range::new(
661 lsp_types::Position::new(22, 41),
662 lsp_types::Position::new(41, 22),
663 ),
664 new_text: "Replacement".to_string(),
665 })],
666 text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
667 uri: lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
668 version: Some(99),
669 },
670 },
671 lsp_types::TextDocumentEdit {
672 edits: vec![lsp_types::OneOf::Right(lsp_types::AnnotatedTextEdit {
673 text_edit: lsp_types::TextEdit {
674 range: lsp_types::Range::new(
675 lsp_types::Position::new(43, 11),
676 lsp_types::Position::new(43, 12),
677 ),
678 new_text: "Foo".to_string(),
679 },
680 annotation_id: "CID".to_string(),
681 })],
682 text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
683 uri: lsp_types::Url::parse("file://foo/baz.slint").unwrap(),
684 version: Some(98),
685 },
686 },
687 ])),
688 change_annotations: None,
689 };
690
691 let mut it = EditIterator::new(&workspace_edit);
692 let r = it.next().unwrap();
693 assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
694 assert_eq!(r.0.version, Some(99));
695 assert_eq!(&r.1.new_text, "Replacement");
696 assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
697 assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
698
699 let r = it.next().unwrap();
700 assert_eq!(&r.0.uri.to_string(), "file://foo/baz.slint");
701 assert_eq!(r.0.version, Some(98));
702 assert_eq!(&r.1.new_text, "Foo");
703 assert_eq!(&r.1.range.start, &lsp_types::Position::new(43, 11));
704 assert_eq!(&r.1.range.end, &lsp_types::Position::new(43, 12));
705 assert!(it.next().is_none());
706 assert!(it.next().is_none());
707}
708
709#[test]
710fn test_edit_iterator_document_mixed() {
711 let workspace_edit = lsp_types::WorkspaceEdit {
712 changes: Some(std::collections::HashMap::from([
713 (
714 lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
715 vec![lsp_types::TextEdit {
716 range: lsp_types::Range::new(
717 lsp_types::Position::new(22, 41),
718 lsp_types::Position::new(41, 22),
719 ),
720 new_text: "Replacement".to_string(),
721 }],
722 ),
723 (
724 lsp_types::Url::parse("file://foo/baz.slint").unwrap(),
725 vec![lsp_types::TextEdit {
726 range: lsp_types::Range::new(
727 lsp_types::Position::new(43, 11),
728 lsp_types::Position::new(43, 12),
729 ),
730 new_text: "Foo".to_string(),
731 }],
732 ),
733 ])),
734 document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
735 lsp_types::TextDocumentEdit {
736 edits: vec![lsp_types::OneOf::Left(lsp_types::TextEdit {
737 range: lsp_types::Range::new(
738 lsp_types::Position::new(22, 41),
739 lsp_types::Position::new(41, 22),
740 ),
741 new_text: "Doc Replacement".to_string(),
742 })],
743 text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
744 uri: lsp_types::Url::parse("file://doc/bar.slint").unwrap(),
745 version: Some(99),
746 },
747 },
748 lsp_types::TextDocumentEdit {
749 edits: vec![lsp_types::OneOf::Right(lsp_types::AnnotatedTextEdit {
750 text_edit: lsp_types::TextEdit {
751 range: lsp_types::Range::new(
752 lsp_types::Position::new(43, 11),
753 lsp_types::Position::new(43, 12),
754 ),
755 new_text: "Doc Foo".to_string(),
756 },
757 annotation_id: "CID".to_string(),
758 })],
759 text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
760 uri: lsp_types::Url::parse("file://doc/baz.slint").unwrap(),
761 version: Some(98),
762 },
763 },
764 ])),
765 change_annotations: None,
766 };
767
768 let mut seen = [false; 4];
769
770 for r in EditIterator::new(&workspace_edit) {
771 // random order!
772 if r.0.uri.to_string() == "file://foo/bar.slint" {
773 assert!(!seen[0]);
774 assert!(!seen[2]);
775 assert!(!seen[3]);
776 assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
777 assert_eq!(r.0.version, None);
778 assert_eq!(&r.1.new_text, "Replacement");
779 assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
780 assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
781 seen[0] = true;
782 } else if r.0.uri.to_string() == "file://foo/baz.slint" {
783 assert!(!seen[1]);
784 assert!(!seen[2]);
785 assert!(!seen[3]);
786 assert_eq!(&r.0.uri.to_string(), "file://foo/baz.slint");
787 assert_eq!(r.0.version, None);
788 assert_eq!(&r.1.new_text, "Foo");
789 assert_eq!(&r.1.range.start, &lsp_types::Position::new(43, 11));
790 assert_eq!(&r.1.range.end, &lsp_types::Position::new(43, 12));
791 seen[1] = true;
792 } else if r.0.uri.to_string() == "file://doc/bar.slint" {
793 assert!(seen[0]);
794 assert!(seen[1]);
795 assert!(!seen[2]);
796 assert!(!seen[3]);
797 assert_eq!(&r.0.uri.to_string(), "file://doc/bar.slint");
798 assert_eq!(r.0.version, Some(99));
799 assert_eq!(&r.1.new_text, "Doc Replacement");
800 assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
801 assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
802 seen[2] = true;
803 } else {
804 assert!(seen[0]);
805 assert!(seen[1]);
806 assert!(seen[2]);
807 assert!(!seen[3]);
808 assert_eq!(&r.0.uri.to_string(), "file://doc/baz.slint");
809 assert_eq!(r.0.version, Some(98));
810 assert_eq!(&r.1.new_text, "Doc Foo");
811 assert_eq!(&r.1.range.start, &lsp_types::Position::new(43, 11));
812 assert_eq!(&r.1.range.end, &lsp_types::Position::new(43, 12));
813 seen[3] = true;
814 }
815 }
816}
817
818#[test]
819fn test_texteditor_no_content_in_source_file() {
820 use i_slint_compiler::diagnostics::SourceFileInner;
821
822 let source_file = SourceFileInner::from_path_only(std::path::PathBuf::from("/tmp/foo.slint"));
823
824 assert!(TextEditor::new(source_file).is_err());
825}
826
827#[test]
828fn test_texteditor_edit_out_of_range() {
829 use i_slint_compiler::diagnostics::SourceFileInner;
830
831 let source_file = std::rc::Rc::new(SourceFileInner::new(
832 std::path::PathBuf::from("/tmp/foo.slint"),
833 r#""#.to_string(),
834 ));
835
836 let mut editor = TextEditor::new(source_file.clone()).unwrap();
837
838 let edit = lsp_types::TextEdit {
839 range: lsp_types::Range::new(
840 lsp_types::Position::new(1, 2),
841 lsp_types::Position::new(1, 3),
842 ),
843 new_text: "Foobar".to_string(),
844 };
845 assert!(editor.apply(&edit).is_err());
846}
847
848#[test]
849fn test_texteditor_delete_everything() {
850 use i_slint_compiler::diagnostics::SourceFileInner;
851
852 let source_file = std::rc::Rc::new(SourceFileInner::new(
853 std::path::PathBuf::from("/tmp/foo.slint"),
854 r#"abc
855def
856geh"#
857 .to_string(),
858 ));
859
860 let mut editor = TextEditor::new(source_file.clone()).unwrap();
861
862 let edit = lsp_types::TextEdit {
863 range: lsp_types::Range::new(
864 lsp_types::Position::new(0, 0),
865 lsp_types::Position::new(2, 3),
866 ),
867 new_text: "".to_string(),
868 };
869 assert!(editor.apply(&edit).is_ok());
870
871 let result = editor.finalize().unwrap();
872 assert!(result.0.is_empty());
873 assert_eq!(result.1.adjust(42.into()), 31.into());
874 assert_eq!(result.2 .0, 0);
875 assert_eq!(result.2 .1, 3 * 3 + 2);
876}
877
878#[test]
879fn test_texteditor_replace() {
880 use i_slint_compiler::diagnostics::SourceFileInner;
881
882 let source_file = std::rc::Rc::new(SourceFileInner::new(
883 std::path::PathBuf::from("/tmp/foo.slint"),
884 r#"abc
885def
886geh"#
887 .to_string(),
888 ));
889
890 let mut editor = TextEditor::new(source_file.clone()).unwrap();
891
892 let edit = lsp_types::TextEdit {
893 range: lsp_types::Range::new(
894 lsp_types::Position::new(1, 0),
895 lsp_types::Position::new(1, 3),
896 ),
897 new_text: "REPLACEMENT".to_string(),
898 };
899 assert!(editor.apply(&edit).is_ok());
900
901 let result = editor.finalize().unwrap();
902 assert_eq!(
903 &result.0,
904 r#"abc
905REPLACEMENT
906geh"#
907 );
908 assert_eq!(result.1.adjust(42.into()), 50.into());
909 assert_eq!(result.2 .0, 3 + 1);
910 assert_eq!(result.2 .1, 3 + 1 + 3);
911}
912
913#[test]
914fn test_texteditor_2step_replace_all() {
915 use i_slint_compiler::diagnostics::SourceFileInner;
916
917 let source_file = std::rc::Rc::new(SourceFileInner::new(
918 std::path::PathBuf::from("/tmp/foo.slint"),
919 r#"abc
920def
921geh"#
922 .to_string(),
923 ));
924
925 let mut editor = TextEditor::new(source_file.clone()).unwrap();
926
927 let edit = lsp_types::TextEdit {
928 range: lsp_types::Range::new(
929 lsp_types::Position::new(0, 0),
930 lsp_types::Position::new(2, 3),
931 ),
932 new_text: "".to_string(),
933 };
934 assert!(editor.apply(&edit).is_ok());
935 let edit = lsp_types::TextEdit {
936 range: lsp_types::Range::new(
937 lsp_types::Position::new(0, 0),
938 lsp_types::Position::new(0, 0),
939 ),
940 new_text: "REPLACEMENT".to_string(),
941 };
942 assert!(editor.apply(&edit).is_ok());
943
944 let result = editor.finalize().unwrap();
945 assert_eq!(&result.0, "REPLACEMENT");
946 assert_eq!(result.1.adjust(42.into()), 42.into());
947 assert_eq!(result.2 .0, 0);
948 assert_eq!(result.2 .1, 3 * 3 + 2);
949}
950