mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 10:50:00 +00:00

They get ignored by the editor (rightfully so), while the live preview will wait for the new source code, blocking all further edits.
949 lines
33 KiB
Rust
949 lines
33 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use i_slint_compiler::parser::TextSize;
|
|
|
|
use crate::common;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct TextOffsetAdjustment {
|
|
pub start_offset: TextSize,
|
|
pub end_offset: TextSize,
|
|
pub new_text_length: u32,
|
|
}
|
|
|
|
impl TextOffsetAdjustment {
|
|
pub fn new(
|
|
edit: &lsp_types::TextEdit,
|
|
source_file: &i_slint_compiler::diagnostics::SourceFile,
|
|
) -> Self {
|
|
let new_text_length = edit.new_text.len() as u32;
|
|
let (start_offset, end_offset) = {
|
|
let so = source_file.offset(
|
|
edit.range.start.line as usize + 1,
|
|
edit.range.start.character as usize + 1,
|
|
);
|
|
let eo = source_file
|
|
.offset(edit.range.end.line as usize + 1, edit.range.end.character as usize + 1);
|
|
(
|
|
TextSize::new(std::cmp::min(so, eo) as u32),
|
|
TextSize::new(std::cmp::max(so, eo) as u32),
|
|
)
|
|
};
|
|
|
|
Self { start_offset, end_offset, new_text_length }
|
|
}
|
|
|
|
pub fn adjust(&self, offset: TextSize) -> TextSize {
|
|
// This is a bit simplistic... Worst case: Some unexpected element gets selected. We can live with that.
|
|
|
|
debug_assert!(self.end_offset >= self.start_offset);
|
|
let old_length = self.end_offset - self.start_offset;
|
|
|
|
if offset >= self.end_offset {
|
|
offset + TextSize::new(self.new_text_length) - old_length
|
|
} else if offset >= self.start_offset {
|
|
((u32::from(offset) as i64 + self.new_text_length as i64 - u32::from(old_length) as i64)
|
|
.clamp(
|
|
u32::from(self.start_offset) as i64,
|
|
u32::from(
|
|
self.end_offset
|
|
.min(self.start_offset + TextSize::new(self.new_text_length)),
|
|
) as i64,
|
|
) as u32)
|
|
.into()
|
|
} else {
|
|
offset
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct TextOffsetAdjustments(Vec<TextOffsetAdjustment>);
|
|
|
|
impl TextOffsetAdjustments {
|
|
pub fn add_adjustment(&mut self, adjustment: TextOffsetAdjustment) {
|
|
self.0.push(adjustment);
|
|
}
|
|
|
|
pub fn adjust(&self, input: TextSize) -> TextSize {
|
|
let input_ = i64::from(u32::from(input));
|
|
let total_adjustment = self
|
|
.0
|
|
.iter()
|
|
.fold(0_i64, |acc, a| acc + i64::from(u32::from(a.adjust(input))) - input_);
|
|
((input_ + total_adjustment) as u32).into()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.0.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum EditIteratorState<'a> {
|
|
Changes { urls: Vec<&'a lsp_types::Url>, main_index: usize, index: usize },
|
|
DocumentChanges { main_index: usize, index: usize },
|
|
Done,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct EditIterator<'a> {
|
|
workspace_edit: &'a lsp_types::WorkspaceEdit,
|
|
state: EditIteratorState<'a>,
|
|
}
|
|
|
|
impl<'a> EditIterator<'a> {
|
|
pub fn new(workspace_edit: &'a lsp_types::WorkspaceEdit) -> Self {
|
|
Self {
|
|
workspace_edit,
|
|
state: EditIteratorState::Changes {
|
|
urls: workspace_edit
|
|
.changes
|
|
.as_ref()
|
|
.map(|hm| hm.keys().collect::<Vec<_>>())
|
|
.unwrap_or_default(),
|
|
main_index: 0,
|
|
index: 0,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Iterator for EditIterator<'a> {
|
|
type Item = (lsp_types::OptionalVersionedTextDocumentIdentifier, &'a lsp_types::TextEdit);
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
match &mut self.state {
|
|
EditIteratorState::Changes { urls, main_index, index } => {
|
|
if let Some(changes) = &self.workspace_edit.changes {
|
|
if let Some(uri) = urls.get(*main_index) {
|
|
if let Some(edits) = changes.get(uri) {
|
|
if let Some(edit) = edits.get(*index) {
|
|
*index += 1;
|
|
return Some((
|
|
lsp_types::OptionalVersionedTextDocumentIdentifier {
|
|
uri: (*uri).clone(),
|
|
version: None,
|
|
},
|
|
edit,
|
|
));
|
|
} else {
|
|
*index = 0;
|
|
*main_index += 1;
|
|
return self.next();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.state = EditIteratorState::DocumentChanges { main_index: 0, index: 0 };
|
|
self.next()
|
|
}
|
|
EditIteratorState::DocumentChanges { main_index, index } => {
|
|
if let Some(lsp_types::DocumentChanges::Edits(edits)) =
|
|
&self.workspace_edit.document_changes
|
|
{
|
|
if let Some(doc_edit) = edits.get(*main_index) {
|
|
if let Some(edit) = doc_edit.edits.get(*index) {
|
|
*index += 1;
|
|
let te = match edit {
|
|
lsp_types::OneOf::Left(te) => te,
|
|
lsp_types::OneOf::Right(ate) => &ate.text_edit,
|
|
};
|
|
return Some((doc_edit.text_document.clone(), te));
|
|
} else {
|
|
*index = 0;
|
|
*main_index += 1;
|
|
return self.next();
|
|
}
|
|
}
|
|
}
|
|
|
|
self.state = EditIteratorState::Done;
|
|
None
|
|
}
|
|
EditIteratorState::Done => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct TextEditor {
|
|
source_file: i_slint_compiler::diagnostics::SourceFile,
|
|
contents: String,
|
|
original_offset_range: (usize, usize),
|
|
adjustments: TextOffsetAdjustments,
|
|
}
|
|
|
|
impl TextEditor {
|
|
pub fn new(source_file: i_slint_compiler::diagnostics::SourceFile) -> crate::Result<Self> {
|
|
let Some(contents) = source_file.source().map(|s| s.to_string()) else {
|
|
return Err(format!("Source file {:?} had no contents set", source_file.path()).into());
|
|
};
|
|
Ok(Self {
|
|
source_file,
|
|
contents,
|
|
original_offset_range: (usize::MAX, 0),
|
|
adjustments: TextOffsetAdjustments::default(),
|
|
})
|
|
}
|
|
|
|
pub fn apply(&mut self, text_edit: &lsp_types::TextEdit) -> crate::Result<()> {
|
|
let current_offset = {
|
|
let start_range = &text_edit.range.start;
|
|
let end_range = &text_edit.range.end;
|
|
let start_offset = self
|
|
.source_file
|
|
.offset(start_range.line as usize + 1, start_range.character as usize + 1);
|
|
let end_offset = self
|
|
.source_file
|
|
.offset(end_range.line as usize + 1, end_range.character as usize + 1);
|
|
(start_offset, end_offset)
|
|
};
|
|
|
|
let adjusted_offset = (
|
|
usize::from(self.adjustments.adjust((current_offset.0 as u32).into())),
|
|
usize::from(self.adjustments.adjust((current_offset.1 as u32).into())),
|
|
);
|
|
|
|
if self.contents.len() < adjusted_offset.1 {
|
|
return Err("Text edit range is out of bounds".into());
|
|
}
|
|
|
|
// Book keeping:
|
|
self.original_offset_range.0 = self.original_offset_range.0.min(current_offset.0);
|
|
self.original_offset_range.1 = self.original_offset_range.1.max(current_offset.1);
|
|
|
|
self.contents.replace_range((adjusted_offset.0)..(adjusted_offset.1), &text_edit.new_text);
|
|
|
|
self.adjustments.add_adjustment(TextOffsetAdjustment::new(text_edit, &self.source_file));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn finalize(self) -> Option<(String, TextOffsetAdjustments, (usize, usize))> {
|
|
if self.source_file.source() == Some(&self.contents) {
|
|
None
|
|
} else {
|
|
(!self.adjustments.is_empty()).then_some((
|
|
self.contents,
|
|
self.adjustments,
|
|
self.original_offset_range,
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct EditedText {
|
|
pub url: lsp_types::Url,
|
|
pub contents: String,
|
|
}
|
|
|
|
pub fn apply_workspace_edit(
|
|
document_cache: &common::DocumentCache,
|
|
workspace_edit: &lsp_types::WorkspaceEdit,
|
|
) -> common::Result<Vec<EditedText>> {
|
|
let mut processing = HashMap::new();
|
|
|
|
for (doc, edit) in EditIterator::new(workspace_edit) {
|
|
// This is ugly but necessary since the constructor might error out:-/
|
|
if !processing.contains_key(&doc.uri) {
|
|
let Some(document) = document_cache.get_document(&doc.uri) else {
|
|
continue;
|
|
};
|
|
let Some(document_node) = &document.node else {
|
|
continue;
|
|
};
|
|
let editor = TextEditor::new(document_node.source_file.clone())?;
|
|
processing.insert(doc.uri.clone(), editor);
|
|
}
|
|
|
|
processing.get_mut(&doc.uri).expect("just added if missing").apply(edit)?;
|
|
}
|
|
|
|
Ok(processing
|
|
.drain()
|
|
.filter_map(|(url, v)| {
|
|
let edit_result = v.finalize()?;
|
|
Some(EditedText { url, contents: edit_result.0 })
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_offset_adjustments() {
|
|
let mut a = TextOffsetAdjustments::default();
|
|
// same length change
|
|
a.add_adjustment(TextOffsetAdjustment {
|
|
start_offset: 10.into(),
|
|
end_offset: 20.into(),
|
|
new_text_length: 10,
|
|
});
|
|
// insert
|
|
a.add_adjustment(TextOffsetAdjustment {
|
|
start_offset: 25.into(),
|
|
end_offset: 25.into(),
|
|
new_text_length: 1,
|
|
});
|
|
// smaller replacement
|
|
a.add_adjustment(TextOffsetAdjustment {
|
|
start_offset: 30.into(),
|
|
end_offset: 40.into(),
|
|
new_text_length: 5,
|
|
});
|
|
// longer replacement
|
|
a.add_adjustment(TextOffsetAdjustment {
|
|
start_offset: 50.into(),
|
|
end_offset: 60.into(),
|
|
new_text_length: 20,
|
|
});
|
|
// deletion
|
|
a.add_adjustment(TextOffsetAdjustment {
|
|
start_offset: 70.into(),
|
|
end_offset: 80.into(),
|
|
new_text_length: 0,
|
|
});
|
|
|
|
assert_eq!(a.adjust(0.into()), 0.into());
|
|
assert_eq!(a.adjust(20.into()), 20.into());
|
|
assert_eq!(a.adjust(25.into()), 26.into());
|
|
assert_eq!(a.adjust(30.into()), 31.into());
|
|
assert_eq!(a.adjust(40.into()), 36.into());
|
|
assert_eq!(a.adjust(60.into()), 66.into());
|
|
assert_eq!(a.adjust(70.into()), 76.into());
|
|
assert_eq!(a.adjust(80.into()), 76.into());
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_offset_adjustments_reverse() {
|
|
let mut a = TextOffsetAdjustments::default();
|
|
// deletion
|
|
a.add_adjustment(TextOffsetAdjustment {
|
|
start_offset: 70.into(),
|
|
end_offset: 80.into(),
|
|
new_text_length: 0,
|
|
});
|
|
// longer replacement
|
|
a.add_adjustment(TextOffsetAdjustment {
|
|
start_offset: 50.into(),
|
|
end_offset: 60.into(),
|
|
new_text_length: 20,
|
|
});
|
|
// smaller replacement
|
|
a.add_adjustment(TextOffsetAdjustment {
|
|
start_offset: 30.into(),
|
|
end_offset: 40.into(),
|
|
new_text_length: 5,
|
|
});
|
|
// insert
|
|
a.add_adjustment(TextOffsetAdjustment {
|
|
start_offset: 25.into(),
|
|
end_offset: 25.into(),
|
|
new_text_length: 1,
|
|
});
|
|
// same length change
|
|
a.add_adjustment(TextOffsetAdjustment {
|
|
start_offset: 10.into(),
|
|
end_offset: 20.into(),
|
|
new_text_length: 10,
|
|
});
|
|
|
|
assert_eq!(a.adjust(0.into()), 0.into());
|
|
assert_eq!(a.adjust(20.into()), 20.into());
|
|
assert_eq!(a.adjust(25.into()), 26.into());
|
|
assert_eq!(a.adjust(30.into()), 31.into());
|
|
assert_eq!(a.adjust(40.into()), 36.into());
|
|
assert_eq!(a.adjust(60.into()), 66.into());
|
|
assert_eq!(a.adjust(70.into()), 76.into());
|
|
assert_eq!(a.adjust(80.into()), 76.into());
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_iterator_empty() {
|
|
let workspace_edit = lsp_types::WorkspaceEdit {
|
|
changes: None,
|
|
document_changes: None,
|
|
change_annotations: None,
|
|
};
|
|
|
|
let mut it = EditIterator::new(&workspace_edit);
|
|
assert!(it.next().is_none());
|
|
assert!(it.next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_iterator_changes_one_empty() {
|
|
let workspace_edit = lsp_types::WorkspaceEdit {
|
|
changes: Some(std::collections::HashMap::from([(
|
|
lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
|
|
vec![],
|
|
)])),
|
|
document_changes: None,
|
|
change_annotations: None,
|
|
};
|
|
|
|
let mut it = EditIterator::new(&workspace_edit);
|
|
assert!(it.next().is_none());
|
|
assert!(it.next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_iterator_changes_one_one() {
|
|
let workspace_edit = lsp_types::WorkspaceEdit {
|
|
changes: Some(std::collections::HashMap::from([(
|
|
lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
|
|
vec![lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(22, 41),
|
|
lsp_types::Position::new(41, 22),
|
|
),
|
|
new_text: "Replacement".to_string(),
|
|
}],
|
|
)])),
|
|
document_changes: None,
|
|
change_annotations: None,
|
|
};
|
|
|
|
let mut it = EditIterator::new(&workspace_edit);
|
|
let r = it.next().unwrap();
|
|
assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
|
|
assert_eq!(r.0.version, None);
|
|
assert_eq!(&r.1.new_text, "Replacement");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
|
|
assert!(it.next().is_none());
|
|
assert!(it.next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_iterator_changes_one_two() {
|
|
let workspace_edit = lsp_types::WorkspaceEdit {
|
|
changes: Some(std::collections::HashMap::from([(
|
|
lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
|
|
vec![
|
|
lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(22, 41),
|
|
lsp_types::Position::new(41, 22),
|
|
),
|
|
new_text: "Replacement".to_string(),
|
|
},
|
|
lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(43, 11),
|
|
lsp_types::Position::new(43, 12),
|
|
),
|
|
new_text: "Foo".to_string(),
|
|
},
|
|
],
|
|
)])),
|
|
document_changes: None,
|
|
change_annotations: None,
|
|
};
|
|
|
|
let mut it = EditIterator::new(&workspace_edit);
|
|
|
|
let r = it.next().unwrap();
|
|
assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
|
|
assert_eq!(r.0.version, None);
|
|
assert_eq!(&r.1.new_text, "Replacement");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
|
|
|
|
let r = it.next().unwrap();
|
|
assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
|
|
assert_eq!(r.0.version, None);
|
|
assert_eq!(&r.1.new_text, "Foo");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(43, 11));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(43, 12));
|
|
|
|
assert!(it.next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_iterator_changes_two() {
|
|
let workspace_edit = lsp_types::WorkspaceEdit {
|
|
changes: Some(std::collections::HashMap::from([
|
|
(
|
|
lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
|
|
vec![lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(22, 41),
|
|
lsp_types::Position::new(41, 22),
|
|
),
|
|
new_text: "Replacement".to_string(),
|
|
}],
|
|
),
|
|
(
|
|
lsp_types::Url::parse("file://foo/baz.slint").unwrap(),
|
|
vec![lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(43, 11),
|
|
lsp_types::Position::new(43, 12),
|
|
),
|
|
new_text: "Foo".to_string(),
|
|
}],
|
|
),
|
|
])),
|
|
document_changes: None,
|
|
change_annotations: None,
|
|
};
|
|
|
|
let mut seen1 = false;
|
|
let mut seen2 = false;
|
|
|
|
for r in EditIterator::new(&workspace_edit) {
|
|
// random order!
|
|
if r.0.uri.to_string() == "file://foo/bar.slint" {
|
|
assert!(!seen1);
|
|
assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
|
|
assert_eq!(r.0.version, None);
|
|
assert_eq!(&r.1.new_text, "Replacement");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
|
|
seen1 = true;
|
|
} else {
|
|
assert!(!seen2);
|
|
assert_eq!(&r.0.uri.to_string(), "file://foo/baz.slint");
|
|
assert_eq!(r.0.version, None);
|
|
assert_eq!(&r.1.new_text, "Foo");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(43, 11));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(43, 12));
|
|
seen2 = true;
|
|
}
|
|
}
|
|
assert!(seen1 && seen2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_iterator_document_changes_empty() {
|
|
let workspace_edit = lsp_types::WorkspaceEdit {
|
|
changes: None,
|
|
document_changes: Some(lsp_types::DocumentChanges::Edits(vec![])),
|
|
change_annotations: None,
|
|
};
|
|
|
|
let mut it = EditIterator::new(&workspace_edit);
|
|
assert!(it.next().is_none());
|
|
assert!(it.next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_iterator_document_changes_operations() {
|
|
let workspace_edit = lsp_types::WorkspaceEdit {
|
|
changes: None,
|
|
document_changes: Some(lsp_types::DocumentChanges::Operations(vec![])),
|
|
change_annotations: None,
|
|
};
|
|
|
|
let mut it = EditIterator::new(&workspace_edit);
|
|
assert!(it.next().is_none());
|
|
assert!(it.next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_iterator_document_changes_one_empty() {
|
|
let workspace_edit = lsp_types::WorkspaceEdit {
|
|
changes: None,
|
|
document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
|
|
lsp_types::TextDocumentEdit {
|
|
edits: vec![],
|
|
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
|
|
uri: lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
|
|
version: Some(99),
|
|
},
|
|
},
|
|
])),
|
|
change_annotations: None,
|
|
};
|
|
|
|
let mut it = EditIterator::new(&workspace_edit);
|
|
assert!(it.next().is_none());
|
|
assert!(it.next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_iterator_document_changes_one_one() {
|
|
let workspace_edit = lsp_types::WorkspaceEdit {
|
|
changes: None,
|
|
document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
|
|
lsp_types::TextDocumentEdit {
|
|
edits: vec![lsp_types::OneOf::Left(lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(22, 41),
|
|
lsp_types::Position::new(41, 22),
|
|
),
|
|
new_text: "Replacement".to_string(),
|
|
})],
|
|
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
|
|
uri: lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
|
|
version: Some(99),
|
|
},
|
|
},
|
|
])),
|
|
change_annotations: None,
|
|
};
|
|
|
|
let mut it = EditIterator::new(&workspace_edit);
|
|
let r = it.next().unwrap();
|
|
assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
|
|
assert_eq!(r.0.version, Some(99));
|
|
assert_eq!(&r.1.new_text, "Replacement");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
|
|
assert!(it.next().is_none());
|
|
assert!(it.next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_iterator_document_changes_one_two() {
|
|
let workspace_edit = lsp_types::WorkspaceEdit {
|
|
changes: None,
|
|
document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
|
|
lsp_types::TextDocumentEdit {
|
|
edits: vec![
|
|
lsp_types::OneOf::Left(lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(22, 41),
|
|
lsp_types::Position::new(41, 22),
|
|
),
|
|
new_text: "Replacement".to_string(),
|
|
}),
|
|
lsp_types::OneOf::Right(lsp_types::AnnotatedTextEdit {
|
|
text_edit: lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(43, 11),
|
|
lsp_types::Position::new(43, 12),
|
|
),
|
|
new_text: "Foo".to_string(),
|
|
},
|
|
annotation_id: "CID".to_string(),
|
|
}),
|
|
],
|
|
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
|
|
uri: lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
|
|
version: Some(99),
|
|
},
|
|
},
|
|
])),
|
|
change_annotations: None,
|
|
};
|
|
|
|
let mut it = EditIterator::new(&workspace_edit);
|
|
let r = it.next().unwrap();
|
|
assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
|
|
assert_eq!(r.0.version, Some(99));
|
|
assert_eq!(&r.1.new_text, "Replacement");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
|
|
|
|
let r = it.next().unwrap();
|
|
assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
|
|
assert_eq!(r.0.version, Some(99));
|
|
assert_eq!(&r.1.new_text, "Foo");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(43, 11));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(43, 12));
|
|
assert!(it.next().is_none());
|
|
assert!(it.next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_iterator_document_changes_two() {
|
|
let workspace_edit = lsp_types::WorkspaceEdit {
|
|
changes: None,
|
|
document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
|
|
lsp_types::TextDocumentEdit {
|
|
edits: vec![lsp_types::OneOf::Left(lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(22, 41),
|
|
lsp_types::Position::new(41, 22),
|
|
),
|
|
new_text: "Replacement".to_string(),
|
|
})],
|
|
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
|
|
uri: lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
|
|
version: Some(99),
|
|
},
|
|
},
|
|
lsp_types::TextDocumentEdit {
|
|
edits: vec![lsp_types::OneOf::Right(lsp_types::AnnotatedTextEdit {
|
|
text_edit: lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(43, 11),
|
|
lsp_types::Position::new(43, 12),
|
|
),
|
|
new_text: "Foo".to_string(),
|
|
},
|
|
annotation_id: "CID".to_string(),
|
|
})],
|
|
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
|
|
uri: lsp_types::Url::parse("file://foo/baz.slint").unwrap(),
|
|
version: Some(98),
|
|
},
|
|
},
|
|
])),
|
|
change_annotations: None,
|
|
};
|
|
|
|
let mut it = EditIterator::new(&workspace_edit);
|
|
let r = it.next().unwrap();
|
|
assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
|
|
assert_eq!(r.0.version, Some(99));
|
|
assert_eq!(&r.1.new_text, "Replacement");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
|
|
|
|
let r = it.next().unwrap();
|
|
assert_eq!(&r.0.uri.to_string(), "file://foo/baz.slint");
|
|
assert_eq!(r.0.version, Some(98));
|
|
assert_eq!(&r.1.new_text, "Foo");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(43, 11));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(43, 12));
|
|
assert!(it.next().is_none());
|
|
assert!(it.next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_iterator_document_mixed() {
|
|
let workspace_edit = lsp_types::WorkspaceEdit {
|
|
changes: Some(std::collections::HashMap::from([
|
|
(
|
|
lsp_types::Url::parse("file://foo/bar.slint").unwrap(),
|
|
vec![lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(22, 41),
|
|
lsp_types::Position::new(41, 22),
|
|
),
|
|
new_text: "Replacement".to_string(),
|
|
}],
|
|
),
|
|
(
|
|
lsp_types::Url::parse("file://foo/baz.slint").unwrap(),
|
|
vec![lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(43, 11),
|
|
lsp_types::Position::new(43, 12),
|
|
),
|
|
new_text: "Foo".to_string(),
|
|
}],
|
|
),
|
|
])),
|
|
document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
|
|
lsp_types::TextDocumentEdit {
|
|
edits: vec![lsp_types::OneOf::Left(lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(22, 41),
|
|
lsp_types::Position::new(41, 22),
|
|
),
|
|
new_text: "Doc Replacement".to_string(),
|
|
})],
|
|
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
|
|
uri: lsp_types::Url::parse("file://doc/bar.slint").unwrap(),
|
|
version: Some(99),
|
|
},
|
|
},
|
|
lsp_types::TextDocumentEdit {
|
|
edits: vec![lsp_types::OneOf::Right(lsp_types::AnnotatedTextEdit {
|
|
text_edit: lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(43, 11),
|
|
lsp_types::Position::new(43, 12),
|
|
),
|
|
new_text: "Doc Foo".to_string(),
|
|
},
|
|
annotation_id: "CID".to_string(),
|
|
})],
|
|
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
|
|
uri: lsp_types::Url::parse("file://doc/baz.slint").unwrap(),
|
|
version: Some(98),
|
|
},
|
|
},
|
|
])),
|
|
change_annotations: None,
|
|
};
|
|
|
|
let mut seen = [false; 4];
|
|
|
|
for r in EditIterator::new(&workspace_edit) {
|
|
// random order!
|
|
if r.0.uri.to_string() == "file://foo/bar.slint" {
|
|
assert!(!seen[0]);
|
|
assert!(!seen[2]);
|
|
assert!(!seen[3]);
|
|
assert_eq!(&r.0.uri.to_string(), "file://foo/bar.slint");
|
|
assert_eq!(r.0.version, None);
|
|
assert_eq!(&r.1.new_text, "Replacement");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
|
|
seen[0] = true;
|
|
} else if r.0.uri.to_string() == "file://foo/baz.slint" {
|
|
assert!(!seen[1]);
|
|
assert!(!seen[2]);
|
|
assert!(!seen[3]);
|
|
assert_eq!(&r.0.uri.to_string(), "file://foo/baz.slint");
|
|
assert_eq!(r.0.version, None);
|
|
assert_eq!(&r.1.new_text, "Foo");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(43, 11));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(43, 12));
|
|
seen[1] = true;
|
|
} else if r.0.uri.to_string() == "file://doc/bar.slint" {
|
|
assert!(seen[0]);
|
|
assert!(seen[1]);
|
|
assert!(!seen[2]);
|
|
assert!(!seen[3]);
|
|
assert_eq!(&r.0.uri.to_string(), "file://doc/bar.slint");
|
|
assert_eq!(r.0.version, Some(99));
|
|
assert_eq!(&r.1.new_text, "Doc Replacement");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(22, 41));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(41, 22));
|
|
seen[2] = true;
|
|
} else {
|
|
assert!(seen[0]);
|
|
assert!(seen[1]);
|
|
assert!(seen[2]);
|
|
assert!(!seen[3]);
|
|
assert_eq!(&r.0.uri.to_string(), "file://doc/baz.slint");
|
|
assert_eq!(r.0.version, Some(98));
|
|
assert_eq!(&r.1.new_text, "Doc Foo");
|
|
assert_eq!(&r.1.range.start, &lsp_types::Position::new(43, 11));
|
|
assert_eq!(&r.1.range.end, &lsp_types::Position::new(43, 12));
|
|
seen[3] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_texteditor_no_content_in_source_file() {
|
|
use i_slint_compiler::diagnostics::SourceFileInner;
|
|
|
|
let source_file = SourceFileInner::from_path_only(std::path::PathBuf::from("/tmp/foo.slint"));
|
|
|
|
assert!(TextEditor::new(source_file).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_texteditor_edit_out_of_range() {
|
|
use i_slint_compiler::diagnostics::SourceFileInner;
|
|
|
|
let source_file = std::rc::Rc::new(SourceFileInner::new(
|
|
std::path::PathBuf::from("/tmp/foo.slint"),
|
|
r#""#.to_string(),
|
|
));
|
|
|
|
let mut editor = TextEditor::new(source_file.clone()).unwrap();
|
|
|
|
let edit = lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(1, 2),
|
|
lsp_types::Position::new(1, 3),
|
|
),
|
|
new_text: "Foobar".to_string(),
|
|
};
|
|
assert!(editor.apply(&edit).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_texteditor_delete_everything() {
|
|
use i_slint_compiler::diagnostics::SourceFileInner;
|
|
|
|
let source_file = std::rc::Rc::new(SourceFileInner::new(
|
|
std::path::PathBuf::from("/tmp/foo.slint"),
|
|
r#"abc
|
|
def
|
|
geh"#
|
|
.to_string(),
|
|
));
|
|
|
|
let mut editor = TextEditor::new(source_file.clone()).unwrap();
|
|
|
|
let edit = lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(0, 0),
|
|
lsp_types::Position::new(2, 3),
|
|
),
|
|
new_text: "".to_string(),
|
|
};
|
|
assert!(editor.apply(&edit).is_ok());
|
|
|
|
let result = editor.finalize().unwrap();
|
|
assert!(result.0.is_empty());
|
|
assert_eq!(result.1.adjust(42.into()), 31.into());
|
|
assert_eq!(result.2 .0, 0);
|
|
assert_eq!(result.2 .1, 3 * 3 + 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_texteditor_replace() {
|
|
use i_slint_compiler::diagnostics::SourceFileInner;
|
|
|
|
let source_file = std::rc::Rc::new(SourceFileInner::new(
|
|
std::path::PathBuf::from("/tmp/foo.slint"),
|
|
r#"abc
|
|
def
|
|
geh"#
|
|
.to_string(),
|
|
));
|
|
|
|
let mut editor = TextEditor::new(source_file.clone()).unwrap();
|
|
|
|
let edit = lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(1, 0),
|
|
lsp_types::Position::new(1, 3),
|
|
),
|
|
new_text: "REPLACEMENT".to_string(),
|
|
};
|
|
assert!(editor.apply(&edit).is_ok());
|
|
|
|
let result = editor.finalize().unwrap();
|
|
assert_eq!(
|
|
&result.0,
|
|
r#"abc
|
|
REPLACEMENT
|
|
geh"#
|
|
);
|
|
assert_eq!(result.1.adjust(42.into()), 50.into());
|
|
assert_eq!(result.2 .0, 3 + 1);
|
|
assert_eq!(result.2 .1, 3 + 1 + 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_texteditor_2step_replace_all() {
|
|
use i_slint_compiler::diagnostics::SourceFileInner;
|
|
|
|
let source_file = std::rc::Rc::new(SourceFileInner::new(
|
|
std::path::PathBuf::from("/tmp/foo.slint"),
|
|
r#"abc
|
|
def
|
|
geh"#
|
|
.to_string(),
|
|
));
|
|
|
|
let mut editor = TextEditor::new(source_file.clone()).unwrap();
|
|
|
|
let edit = lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(0, 0),
|
|
lsp_types::Position::new(2, 3),
|
|
),
|
|
new_text: "".to_string(),
|
|
};
|
|
assert!(editor.apply(&edit).is_ok());
|
|
let edit = lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(0, 0),
|
|
lsp_types::Position::new(0, 0),
|
|
),
|
|
new_text: "REPLACEMENT".to_string(),
|
|
};
|
|
assert!(editor.apply(&edit).is_ok());
|
|
|
|
let result = editor.finalize().unwrap();
|
|
assert_eq!(&result.0, "REPLACEMENT");
|
|
assert_eq!(result.1.adjust(42.into()), 42.into());
|
|
assert_eq!(result.2 .0, 0);
|
|
assert_eq!(result.2 .1, 3 * 3 + 2);
|
|
}
|