lsp: Factor out common code to create WorkspaceEdits

Removes some duplication and lends itself for resue in the previewer :-)

Side-effect: We now consitently use versioned text documents, so the
editor will notice when the LSP refactored outdated data.
This commit is contained in:
Tobias Hunger 2024-01-29 21:50:46 +01:00 committed by Tobias Hunger
parent b23c605ac1
commit a329a7312b
3 changed files with 145 additions and 143 deletions

View file

@ -4,10 +4,11 @@
//! Data structures common between LSP and previewer //! Data structures common between LSP and previewer
use i_slint_compiler::{ use i_slint_compiler::{
diagnostics::{SourceFile, SourceFileVersion},
object_tree::Element, object_tree::Element,
parser::{syntax_nodes, SyntaxKind}, parser::{syntax_nodes, SyntaxKind},
}; };
use lsp_types::Url; use lsp_types::{TextEdit, Url, WorkspaceEdit};
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
@ -15,6 +16,9 @@ pub type Error = Box<dyn std::error::Error>;
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
pub type UrlVersion = Option<i32>; pub type UrlVersion = Option<i32>;
#[cfg(target_arch = "wasm32")]
use crate::wasm_prelude::*;
/// Use this in nodes you want the language server and preview to /// Use this in nodes you want the language server and preview to
/// ignore a node for code analysis purposes. /// ignore a node for code analysis purposes.
pub const NODE_IGNORE_COMMENT: &str = "@lsp:ignore-node"; pub const NODE_IGNORE_COMMENT: &str = "@lsp:ignore-node";
@ -30,6 +34,34 @@ pub fn filter_ignore_nodes_in_element(
}) })
} }
pub fn create_workspace_edit(
uri: Url,
version: SourceFileVersion,
edits: Vec<TextEdit>,
) -> WorkspaceEdit {
let edits = edits
.into_iter()
.map(|te| lsp_types::OneOf::Left::<TextEdit, lsp_types::AnnotatedTextEdit>(te))
.collect();
let edit = lsp_types::TextDocumentEdit {
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier { uri, version },
edits,
};
let changes = lsp_types::DocumentChanges::Edits(vec![edit]);
WorkspaceEdit { document_changes: Some(changes), ..Default::default() }
}
pub fn create_workspace_edit_from_source_file(
source_file: &SourceFile,
edits: Vec<TextEdit>,
) -> Option<WorkspaceEdit> {
Some(create_workspace_edit(
Url::from_file_path(source_file.path()).ok()?,
source_file.version(),
edits,
))
}
/// A versioned file /// A versioned file
#[derive(Clone, serde::Deserialize, serde::Serialize)] #[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct VersionedUrl { pub struct VersionedUrl {

View file

@ -12,7 +12,10 @@ mod semantic_tokens;
#[cfg(test)] #[cfg(test)]
mod test; mod test;
use crate::common::{LspToPreviewMessage, PreviewComponent, PreviewConfig, Result, VersionedUrl}; use crate::common::{
create_workspace_edit, create_workspace_edit_from_source_file, LspToPreviewMessage,
PreviewComponent, PreviewConfig, Result, VersionedUrl,
};
use crate::language::properties::find_element_indent; use crate::language::properties::find_element_indent;
use crate::util::{lookup_current_element_type, map_node, map_range, map_token, to_lsp_diag}; use crate::util::{lookup_current_element_type, map_node, map_range, map_token, to_lsp_diag};
@ -399,6 +402,7 @@ pub fn register_request_handlers(rh: &mut RequestHandler) {
token_descr(&mut document_cache, &uri, &params.text_document_position.position) token_descr(&mut document_cache, &uri, &params.text_document_position.position)
{ {
let p = tk.parent(); let p = tk.parent();
let version = p.source_file.version();
if let Some(value) = find_element_id_for_highlight(&tk, &tk.parent()) { if let Some(value) = find_element_id_for_highlight(&tk, &tk.parent()) {
let edits = value let edits = value
.into_iter() .into_iter()
@ -407,10 +411,7 @@ pub fn register_request_handlers(rh: &mut RequestHandler) {
new_text: params.new_name.clone(), new_text: params.new_name.clone(),
}) })
.collect(); .collect();
return Ok(Some(WorkspaceEdit { return Ok(Some(create_workspace_edit(uri, version, edits)));
changes: Some(std::iter::once((uri, edits)).collect()),
..Default::default()
}));
} }
}; };
Err("This symbol cannot be renamed. (Only element id can be renamed at the moment)".into()) Err("This symbol cannot be renamed. (Only element id can be renamed at the moment)".into())
@ -891,10 +892,7 @@ fn get_code_actions(
]; ];
result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
title: "Wrap in `@tr()`".into(), title: "Wrap in `@tr()`".into(),
edit: Some(WorkspaceEdit { edit: create_workspace_edit_from_source_file(&token.source_file, edits),
changes: Some(std::iter::once((uri, edits)).collect()),
..Default::default()
}),
..Default::default() ..Default::default()
})); }));
} else if token.kind() == SyntaxKind::Identifier } else if token.kind() == SyntaxKind::Identifier
@ -921,10 +919,10 @@ fn get_code_actions(
result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
title: format!("Add import from \"{file}\""), title: format!("Add import from \"{file}\""),
kind: Some(lsp_types::CodeActionKind::QUICKFIX), kind: Some(lsp_types::CodeActionKind::QUICKFIX),
edit: Some(WorkspaceEdit { edit: create_workspace_edit_from_source_file(
changes: Some(std::iter::once((uri.clone(), vec![edit])).collect()), &token.source_file,
..Default::default() vec![edit],
}), ),
..Default::default() ..Default::default()
})) }))
}, },
@ -956,10 +954,7 @@ fn get_code_actions(
result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
title: "Wrap in element".into(), title: "Wrap in element".into(),
kind: Some(lsp_types::CodeActionKind::REFACTOR), kind: Some(lsp_types::CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit { edit: create_workspace_edit_from_source_file(&token.source_file, edits),
changes: Some(std::iter::once((uri.clone(), edits)).collect()),
..Default::default()
}),
..Default::default() ..Default::default()
})); }));
@ -1023,10 +1018,7 @@ fn get_code_actions(
result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
title: "Remove element".into(), title: "Remove element".into(),
kind: Some(lsp_types::CodeActionKind::REFACTOR), kind: Some(lsp_types::CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit { edit: create_workspace_edit_from_source_file(&token.source_file, edits),
changes: Some(std::iter::once((uri.clone(), edits)).collect()),
..Default::default()
}),
..Default::default() ..Default::default()
})); }));
} }
@ -1049,10 +1041,7 @@ fn get_code_actions(
result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
title: "Repeat element".into(), title: "Repeat element".into(),
kind: Some(lsp_types::CodeActionKind::REFACTOR), kind: Some(lsp_types::CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit { edit: create_workspace_edit_from_source_file(&token.source_file, edits),
changes: Some(std::iter::once((uri.clone(), edits)).collect()),
..Default::default()
}),
..Default::default() ..Default::default()
})); }));
@ -1063,10 +1052,7 @@ fn get_code_actions(
result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
title: "Make conditional".into(), title: "Make conditional".into(),
kind: Some(lsp_types::CodeActionKind::REFACTOR), kind: Some(lsp_types::CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit { edit: create_workspace_edit_from_source_file(&token.source_file, edits),
changes: Some(std::iter::once((uri.clone(), edits)).collect()),
..Default::default()
}),
..Default::default() ..Default::default()
})); }));
} }
@ -1120,7 +1106,7 @@ fn get_document_symbols(
// DocumentSymbol doesn't implement default and some field depends on features or are deprecated // DocumentSymbol doesn't implement default and some field depends on features or are deprecated
let ds: DocumentSymbol = serde_json::from_value( let ds: DocumentSymbol = serde_json::from_value(
serde_json::json!({ "name" : "", "kind": 255, "range" : lsp_types::Range::default(), "selectionRange": lsp_types::Range::default() }) serde_json::json!({ "name" : "", "kind": 255, "range" : lsp_types::Range::default(), "selectionRange" : lsp_types::Range::default() })
) )
.unwrap(); .unwrap();
@ -1721,26 +1707,28 @@ export component TestWindow inherits Window {
Some(vec![CodeActionOrCommand::CodeAction(lsp_types::CodeAction { Some(vec![CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
title: "Wrap in `@tr()`".into(), title: "Wrap in `@tr()`".into(),
edit: Some(WorkspaceEdit { edit: Some(WorkspaceEdit {
changes: Some( document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
std::iter::once(( lsp_types::TextDocumentEdit {
url.clone(), text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
vec![ version: Some(42),
TextEdit::new( uri: url.clone(),
},
edits: vec![
lsp_types::OneOf::Left(TextEdit::new(
lsp_types::Range::new(text_literal.start, text_literal.start), lsp_types::Range::new(text_literal.start, text_literal.start),
"@tr(".into() "@tr(".into()
), )),
TextEdit::new( lsp_types::OneOf::Left(TextEdit::new(
lsp_types::Range::new(text_literal.end, text_literal.end), lsp_types::Range::new(text_literal.end, text_literal.end),
")".into() ")".into()
) )),
] ],
)) }
.collect() ])),
),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
}),]) })]),
); );
let text_element = lsp_types::Range::new(Position::new(6, 8), Position::new(9, 9)); let text_element = lsp_types::Range::new(Position::new(6, 8), Position::new(9, 9));
@ -1769,10 +1757,14 @@ export component TestWindow inherits Window {
title: "Wrap in element".into(), title: "Wrap in element".into(),
kind: Some(lsp_types::CodeActionKind::REFACTOR), kind: Some(lsp_types::CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit { edit: Some(WorkspaceEdit {
changes: Some( document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
std::iter::once(( lsp_types::TextDocumentEdit {
url.clone(), text_document:
vec![TextEdit::new( lsp_types::OptionalVersionedTextDocumentIdentifier {
version: Some(42),
uri: url.clone(),
},
edits: vec![lsp_types::OneOf::Left(TextEdit::new(
text_element, text_element,
r#"${0:element} { r#"${0:element} {
Text { Text {
@ -1781,10 +1773,9 @@ export component TestWindow inherits Window {
} }
}"# }"#
.into() .into()
)] ))],
)) },
.collect() ])),
),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
@ -1793,19 +1784,22 @@ export component TestWindow inherits Window {
title: "Repeat element".into(), title: "Repeat element".into(),
kind: Some(lsp_types::CodeActionKind::REFACTOR), kind: Some(lsp_types::CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit { edit: Some(WorkspaceEdit {
changes: Some( document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
std::iter::once(( lsp_types::TextDocumentEdit {
url.clone(), text_document:
vec![TextEdit::new( lsp_types::OptionalVersionedTextDocumentIdentifier {
version: Some(42),
uri: url.clone(),
},
edits: vec![lsp_types::OneOf::Left(TextEdit::new(
lsp_types::Range::new( lsp_types::Range::new(
text_element.start, text_element.start,
text_element.start text_element.start
), ),
r#"for ${1:name}[index] in ${0:model} : "#.into() r#"for ${1:name}[index] in ${0:model} : "#.into()
)] ))],
)) }
.collect() ])),
),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
@ -1814,19 +1808,22 @@ export component TestWindow inherits Window {
title: "Make conditional".into(), title: "Make conditional".into(),
kind: Some(lsp_types::CodeActionKind::REFACTOR), kind: Some(lsp_types::CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit { edit: Some(WorkspaceEdit {
changes: Some( document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
std::iter::once(( lsp_types::TextDocumentEdit {
url.clone(), text_document:
vec![TextEdit::new( lsp_types::OptionalVersionedTextDocumentIdentifier {
version: Some(42),
uri: url.clone(),
},
edits: vec![lsp_types::OneOf::Left(TextEdit::new(
lsp_types::Range::new( lsp_types::Range::new(
text_element.start, text_element.start,
text_element.start text_element.start
), ),
r#"if ${0:condition} : "#.into() r#"if ${0:condition} : "#.into()
)] ))],
)) }
.collect() ])),
),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
@ -1853,10 +1850,13 @@ export component TestWindow inherits Window {
title: "Wrap in element".into(), title: "Wrap in element".into(),
kind: Some(lsp_types::CodeActionKind::REFACTOR), kind: Some(lsp_types::CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit { edit: Some(WorkspaceEdit {
changes: Some( document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
std::iter::once(( lsp_types::TextDocumentEdit {
url.clone(), text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
vec![TextEdit::new( version: Some(42),
uri: url.clone(),
},
edits: vec![lsp_types::OneOf::Left(TextEdit::new(
horizontal_box, horizontal_box,
r#"${0:element} { r#"${0:element} {
HorizontalBox { HorizontalBox {
@ -1871,10 +1871,9 @@ export component TestWindow inherits Window {
} }
}"# }"#
.into() .into()
)] ))]
)) }
.collect() ])),
),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
@ -1883,10 +1882,13 @@ export component TestWindow inherits Window {
title: "Remove element".into(), title: "Remove element".into(),
kind: Some(lsp_types::CodeActionKind::REFACTOR), kind: Some(lsp_types::CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit { edit: Some(WorkspaceEdit {
changes: Some( document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
std::iter::once(( lsp_types::TextDocumentEdit {
url.clone(), text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
vec![TextEdit::new( version: Some(42),
uri: url.clone(),
},
edits: vec![lsp_types::OneOf::Left(TextEdit::new(
horizontal_box, horizontal_box,
r#"Button { text: "Cancel"; } r#"Button { text: "Cancel"; }
@ -1895,14 +1897,13 @@ export component TestWindow inherits Window {
primary: true; primary: true;
}"# }"#
.into() .into()
)] ))]
)) }
.collect() ])),
),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
}) }),
]) ])
); );
@ -1919,20 +1920,22 @@ export component TestWindow inherits Window {
title: "Add import from \"std-widgets.slint\"".into(), title: "Add import from \"std-widgets.slint\"".into(),
kind: Some(lsp_types::CodeActionKind::QUICKFIX), kind: Some(lsp_types::CodeActionKind::QUICKFIX),
edit: Some(WorkspaceEdit { edit: Some(WorkspaceEdit {
changes: Some( document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
std::iter::once(( lsp_types::TextDocumentEdit {
url.clone(), text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
vec![TextEdit::new( version: Some(42),
uri: url.clone(),
},
edits: vec![lsp_types::OneOf::Left(TextEdit::new(
lsp_types::Range::new(import_pos, import_pos), lsp_types::Range::new(import_pos, import_pos),
", LineEdit".into() ", LineEdit".into()
)] ))]
)) }
.collect() ])),
),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
})]) }),])
); );
} }
} }

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use super::DocumentCache; use super::DocumentCache;
use crate::common::{Error, Result}; use crate::common::{create_workspace_edit, Error, Result};
use crate::util::{ use crate::util::{
map_node, map_node_and_url, map_position, map_range, to_lsp_diag, with_property_lookup_ctx, map_node, map_node_and_url, map_position, map_range, to_lsp_diag, with_property_lookup_ctx,
ExpressionContextInfo, ExpressionContextInfo,
@ -466,18 +466,7 @@ fn create_workspace_edit_for_set_binding_on_existing_property(
property.defined_at.as_ref().map(|defined_at| { property.defined_at.as_ref().map(|defined_at| {
let edit = let edit =
lsp_types::TextEdit { range: defined_at.expression_range, new_text: new_expression }; lsp_types::TextEdit { range: defined_at.expression_range, new_text: new_expression };
let edits = vec![lsp_types::OneOf::Left(edit)]; create_workspace_edit(uri.clone(), version, vec![edit])
let text_document_edits = vec![lsp_types::TextDocumentEdit {
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier::new(
uri.clone(),
version.unwrap_or(i32::MIN),
),
edits,
}];
lsp_types::WorkspaceEdit {
document_changes: Some(lsp_types::DocumentChanges::Edits(text_document_edits)),
..Default::default()
}
}) })
} }
@ -580,18 +569,7 @@ fn create_workspace_edit_for_set_binding_on_known_property(
} }
}, },
}; };
let edits = vec![lsp_types::OneOf::Left(edit)]; create_workspace_edit(uri.clone(), version, vec![edit])
let text_document_edits = vec![lsp_types::TextDocumentEdit {
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier::new(
uri.clone(),
version.unwrap_or(i32::MIN),
),
edits,
}];
lsp_types::WorkspaceEdit {
document_changes: Some(lsp_types::DocumentChanges::Edits(text_document_edits)),
..Default::default()
}
}, },
) )
} }
@ -605,18 +583,18 @@ fn set_binding_on_known_property(
new_expression: &str, new_expression: &str,
diag: &mut BuildDiagnostics, diag: &mut BuildDiagnostics,
) -> Result<(SetBindingResponse, Option<lsp_types::WorkspaceEdit>)> { ) -> Result<(SetBindingResponse, Option<lsp_types::WorkspaceEdit>)> {
let workspace_edit = (!diag.has_error()) let workspace_edit = if diag.has_error() {
.then(|| { None
create_workspace_edit_for_set_binding_on_known_property( } else {
uri, create_workspace_edit_for_set_binding_on_known_property(
document_cache.document_version(uri), uri,
element, document_cache.document_version(uri),
properties, element,
property_name, properties,
new_expression, property_name,
) new_expression,
}) )
.flatten(); };
Ok(( Ok((
SetBindingResponse { diagnostics: diag.iter().map(to_lsp_diag).collect::<Vec<_>>() }, SetBindingResponse { diagnostics: diag.iter().map(to_lsp_diag).collect::<Vec<_>>() },
@ -719,18 +697,7 @@ fn create_workspace_edit_for_remove_binding(
range: lsp_types::Range, range: lsp_types::Range,
) -> lsp_types::WorkspaceEdit { ) -> lsp_types::WorkspaceEdit {
let edit = lsp_types::TextEdit { range, new_text: String::new() }; let edit = lsp_types::TextEdit { range, new_text: String::new() };
let edits = vec![lsp_types::OneOf::Left(edit)]; create_workspace_edit(uri.clone(), version, vec![edit])
let text_document_edits = vec![lsp_types::TextDocumentEdit {
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier::new(
uri.clone(),
version.unwrap_or(i32::MIN),
),
edits,
}];
lsp_types::WorkspaceEdit {
document_changes: Some(lsp_types::DocumentChanges::Edits(text_document_edits)),
..Default::default()
}
} }
pub(crate) fn remove_binding( pub(crate) fn remove_binding(