mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-26 18:06:26 +00:00
Some checks are pending
autofix.ci / format_fix (push) Waiting to run
autofix.ci / lint_typecheck (push) Waiting to run
CI / ffi_32bit_build (push) Blocked by required conditions
CI / docs (push) Blocked by required conditions
CI / wasm (push) Blocked by required conditions
CI / wasm_demo (push) Blocked by required conditions
CI / tree-sitter (push) Blocked by required conditions
CI / files-changed (push) Waiting to run
CI / build_and_test (--exclude bevy-example, ubuntu-22.04, 1.85) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, --exclude bevy-example, windows-2022, 1.85) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, macos-14, stable) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, windows-2022, beta) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, windows-2022, stable) (push) Blocked by required conditions
CI / build_and_test (ubuntu-22.04, nightly) (push) Blocked by required conditions
CI / node_test (macos-14) (push) Blocked by required conditions
CI / node_test (ubuntu-22.04) (push) Blocked by required conditions
CI / node_test (windows-2022) (push) Blocked by required conditions
CI / python_test (macos-14) (push) Blocked by required conditions
CI / python_test (ubuntu-22.04) (push) Blocked by required conditions
CI / python_test (windows-2022) (push) Blocked by required conditions
CI / cpp_test_driver (macos-13) (push) Blocked by required conditions
CI / cpp_test_driver (ubuntu-22.04) (push) Blocked by required conditions
CI / cpp_test_driver (windows-2022) (push) Blocked by required conditions
CI / cpp_cmake (macos-14, 1.85) (push) Blocked by required conditions
CI / cpp_cmake (ubuntu-22.04, stable) (push) Blocked by required conditions
CI / cpp_cmake (windows-2022, nightly) (push) Blocked by required conditions
CI / cpp_package_test (push) Blocked by required conditions
CI / vsce_build_test (push) Blocked by required conditions
CI / mcu (pico-st7789, thumbv6m-none-eabi) (push) Blocked by required conditions
CI / mcu (pico2-st7789, thumbv8m.main-none-eabihf) (push) Blocked by required conditions
CI / mcu (stm32h735g, thumbv7em-none-eabihf) (push) Blocked by required conditions
CI / mcu-embassy (push) Blocked by required conditions
CI / updater_test (0.3.0) (push) Blocked by required conditions
CI / fmt_test (push) Blocked by required conditions
CI / esp-idf-quick (push) Blocked by required conditions
CI / android (push) Blocked by required conditions
CI / miri (push) Blocked by required conditions
CI / test-figma-inspector (push) Blocked by required conditions
1120 lines
39 KiB
Rust
1120 lines
39 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::{TextRange, 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 adjust_range(&self, range: TextRange) -> TextRange {
|
|
TextRange::new(self.adjust(range.start()), self.adjust(range.end()))
|
|
}
|
|
|
|
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_range =
|
|
crate::util::lsp_range_to_text_range(&self.source_file, text_edit.range);
|
|
let adjusted_range = self.adjustments.adjust_range(current_range);
|
|
|
|
if self.contents.len() < adjusted_range.end().into() {
|
|
return Err("Text edit range is out of bounds".into());
|
|
}
|
|
|
|
// Book keeping:
|
|
self.original_offset_range.0 =
|
|
self.original_offset_range.0.min(current_range.start().into());
|
|
self.original_offset_range.1 = self.original_offset_range.1.max(current_range.end().into());
|
|
|
|
let r: std::ops::Range<usize> = adjusted_range.start().into()..adjusted_range.end().into();
|
|
self.contents.replace_range(r, &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,
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
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())
|
|
}
|
|
|
|
/// Given a WorkspaceEdit, return a reversed version of it (that undoes it)
|
|
/// Returns none if it cannot be reverted (eg, if it deletes files)
|
|
pub fn reversed_edit(
|
|
document_cache: &common::DocumentCache,
|
|
edit: &lsp_types::WorkspaceEdit,
|
|
) -> Option<lsp_types::WorkspaceEdit> {
|
|
struct UndoHelper {
|
|
source_file: i_slint_compiler::diagnostics::SourceFile,
|
|
/// (Range in the original, original string, new length)
|
|
edits: Vec<lsp_types::TextEdit>,
|
|
}
|
|
|
|
let mut processing = HashMap::new();
|
|
|
|
for (doc, edit) in EditIterator::new(edit) {
|
|
if !processing.contains_key(&doc.uri) {
|
|
let Some(document) = document_cache.get_document(&doc.uri) else {
|
|
return None;
|
|
};
|
|
let Some(document_node) = &document.node else {
|
|
return None;
|
|
};
|
|
let helper =
|
|
UndoHelper { source_file: document_node.source_file.clone(), edits: Vec::new() };
|
|
processing.insert(doc.uri.clone(), helper);
|
|
}
|
|
|
|
let helper = processing.get_mut(&doc.uri).unwrap();
|
|
helper.edits.push(edit.clone());
|
|
}
|
|
|
|
let mut tde = Vec::new();
|
|
for (uri, mut helper) in processing {
|
|
let source = helper.source_file.source()?;
|
|
helper.edits.sort_by_key(|e| e.range.start);
|
|
let mut adjust_line = 0i32;
|
|
let mut current_line = 0;
|
|
let mut adjust_column = 0i32;
|
|
let edits = helper
|
|
.edits
|
|
.into_iter()
|
|
.map(|e| {
|
|
let orig_range = crate::util::lsp_range_to_text_range(&helper.source_file, e.range);
|
|
let orig_string = source[orig_range].to_string();
|
|
|
|
// Count the number of \n in the original string and the replaced string
|
|
let orig_lines = orig_string.chars().filter(|c| *c == '\n').count() as i32;
|
|
let new_lines = e.new_text.chars().filter(|c| *c == '\n').count() as i32;
|
|
|
|
let adjust_pos = |pos: lsp_types::Position| {
|
|
let line = pos.line.saturating_add_signed(adjust_line);
|
|
if pos.line == current_line {
|
|
lsp_types::Position::new(
|
|
line,
|
|
pos.character.saturating_add_signed(adjust_column),
|
|
)
|
|
} else {
|
|
lsp_types::Position::new(line, pos.character)
|
|
}
|
|
};
|
|
|
|
let adjusted_start = adjust_pos(e.range.start);
|
|
|
|
let new_range = lsp_types::Range::new(
|
|
adjusted_start,
|
|
lsp_types::Position::new(
|
|
adjusted_start.line.saturating_add_signed(new_lines),
|
|
if new_lines == 0 {
|
|
adjusted_start.character + e.new_text.len() as u32
|
|
} else {
|
|
e.new_text.bytes().rev().take_while(|b| *b != b'\n').count() as u32
|
|
},
|
|
),
|
|
);
|
|
|
|
adjust_line += new_lines - orig_lines;
|
|
current_line = e.range.end.line;
|
|
adjust_column = new_range.end.character as i32 - e.range.end.character as i32;
|
|
|
|
lsp_types::OneOf::Left(lsp_types::TextEdit {
|
|
range: new_range,
|
|
new_text: orig_string,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
tde.push(lsp_types::TextDocumentEdit {
|
|
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
|
|
uri,
|
|
version: None,
|
|
},
|
|
edits,
|
|
});
|
|
}
|
|
|
|
Some(lsp_types::WorkspaceEdit {
|
|
document_changes: Some(lsp_types::DocumentChanges::Edits(tde)),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_apply_reversed_edit {
|
|
use super::*;
|
|
|
|
fn check_apply_and_reversed_edit(
|
|
document_cache: &mut common::DocumentCache,
|
|
edit: &lsp_types::WorkspaceEdit,
|
|
) {
|
|
let edits = apply_workspace_edit(document_cache, edit).unwrap();
|
|
let reversed = reversed_edit(document_cache, edit).unwrap();
|
|
|
|
let snapshot = document_cache.snapshot().unwrap();
|
|
|
|
for e in &edits {
|
|
common::poll_once(document_cache.load_url(
|
|
&e.url,
|
|
None,
|
|
e.contents.clone(),
|
|
&mut Default::default(),
|
|
));
|
|
}
|
|
|
|
let reversed_edits = apply_workspace_edit(document_cache, &reversed).unwrap();
|
|
assert_eq!(edits.len(), reversed_edits.len());
|
|
for e in reversed_edits {
|
|
let doc = snapshot.get_document(&e.url).unwrap();
|
|
assert_eq!(doc.node.as_ref().unwrap().source_file.source().unwrap(), e.contents);
|
|
}
|
|
let reversed_twice = reversed_edit(document_cache, &reversed).unwrap();
|
|
assert_eq!(edits, apply_workspace_edit(&snapshot, &reversed_twice).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_multi_line_edit() {
|
|
let url = lsp_types::Url::from_file_path(common::test::main_test_file_name()).unwrap();
|
|
let code = HashMap::from([(url.clone(), "component Foo { /*..*/ }\n".to_string())]);
|
|
let mut document_cache =
|
|
crate::common::test::compile_test_with_sources("fluent", code, true);
|
|
|
|
check_apply_and_reversed_edit(
|
|
&mut document_cache,
|
|
&lsp_types::WorkspaceEdit::new(std::collections::HashMap::from([(
|
|
url.clone(),
|
|
vec![
|
|
lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(0, 0),
|
|
lsp_types::Position::new(0, 0),
|
|
),
|
|
new_text: "import { AboutSlint } from \"std-widgets.slint\";\n".to_string(),
|
|
},
|
|
lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(0, 15),
|
|
lsp_types::Position::new(0, 23),
|
|
),
|
|
new_text: "\n AboutSlint {}\n".to_string(),
|
|
},
|
|
lsp_types::TextEdit {
|
|
range: lsp_types::Range::new(
|
|
lsp_types::Position::new(0, 24),
|
|
lsp_types::Position::new(0, 24),
|
|
),
|
|
new_text: "\n//comment\n".to_string(),
|
|
},
|
|
],
|
|
)])),
|
|
);
|
|
|
|
assert_eq!(
|
|
document_cache.get_document(&url).unwrap().node.as_ref().unwrap().source_file.source().unwrap(),
|
|
"import { AboutSlint } from \"std-widgets.slint\";\ncomponent Foo {\n AboutSlint {}\n}\n//comment\n\n"
|
|
);
|
|
}
|
|
}
|