VSCode: add "Remove element" code action

This commit is contained in:
J-P Nurmi 2023-09-07 13:29:14 +02:00 committed by Olivier Goffart
parent 46bd17c02d
commit 8284674d4d
3 changed files with 199 additions and 25 deletions

View file

@ -26,7 +26,7 @@ All notable changes to this project are documented in this file.
### LSP ### LSP
- Fixed "Show Preview" command without component selected (#3412) - Fixed "Show Preview" command without component selected (#3412)
- Added "Wrap in element" code action - Added "Wrap in element" and "Remove element" code actions
## [1.2.0] - 2023-09-04 ## [1.2.0] - 2023-09-04

View file

@ -762,6 +762,9 @@ impl SyntaxToken {
})?; })?;
Some(SyntaxToken { token, source_file: self.source_file.clone() }) Some(SyntaxToken { token, source_file: self.source_file.clone() })
} }
pub fn text(&self) -> &str {
self.token.text()
}
} }
impl std::fmt::Display for SyntaxToken { impl std::fmt::Display for SyntaxToken {

View file

@ -1,7 +1,7 @@
// Copyright © SixtyFPS GmbH <info@slint.dev> // Copyright © SixtyFPS GmbH <info@slint.dev>
// 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
// cSpell: ignore descr rfind // cSpell: ignore descr rfind unindented
mod completion; mod completion;
mod goto; mod goto;
@ -801,22 +801,24 @@ fn get_code_actions(
let node = token.parent(); let node = token.parent();
let uri = Url::from_file_path(token.source_file.path()).ok()?; let uri = Url::from_file_path(token.source_file.path()).ok()?;
let mut result = vec![]; let mut result = vec![];
let component = syntax_nodes::Component::new(node.clone())
.or_else(|| {
syntax_nodes::DeclaredIdentifier::new(node.clone())
.and_then(|n| n.parent())
.and_then(syntax_nodes::Component::new)
})
.or_else(|| {
syntax_nodes::QualifiedName::new(node.clone())
.and_then(|n| n.parent())
.and_then(syntax_nodes::Element::new)
.and_then(|n| n.parent())
.and_then(syntax_nodes::Component::new)
});
#[cfg(feature = "preview-lense")] #[cfg(feature = "preview-lense")]
{ {
let component = syntax_nodes::Component::new(node.clone()) if let Some(component) = &component {
.or_else(|| {
syntax_nodes::DeclaredIdentifier::new(node.clone())
.and_then(|n| n.parent())
.and_then(syntax_nodes::Component::new)
})
.or_else(|| {
syntax_nodes::QualifiedName::new(node.clone())
.and_then(|n| n.parent())
.and_then(syntax_nodes::Element::new)
.and_then(|n| n.parent())
.and_then(syntax_nodes::Component::new)
});
if let Some(component) = component {
if let Some(component_name) = if let Some(component_name) =
i_slint_compiler::parser::identifier_text(&component.DeclaredIdentifier()) i_slint_compiler::parser::identifier_text(&component.DeclaredIdentifier())
{ {
@ -857,7 +859,7 @@ fn get_code_actions(
.text() .text()
.to_string() .to_string()
.lines() .lines()
.map(|line| format!(" {}", line)) .map(|line| if line.is_empty() { line.to_string() } else { format!(" {}", line) })
.collect::<Vec<String>>(); .collect::<Vec<String>>();
let edits = vec![TextEdit::new( let edits = vec![TextEdit::new(
lsp_types::Range::new(r.start, r.end), lsp_types::Range::new(r.start, r.end),
@ -871,11 +873,80 @@ fn get_code_actions(
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(std::iter::once((uri, edits)).collect()), changes: Some(std::iter::once((uri.clone(), edits)).collect()),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
})); }));
// Collect all normal, repeated, and conditional sub-elements and any
// whitespace in between for substituting the parent element with its
// sub-elements, dropping its own properties, callbacks etc.
fn is_sub_element(kind: SyntaxKind) -> bool {
match kind {
SyntaxKind::SubElement => true,
SyntaxKind::RepeatedElement => true,
SyntaxKind::ConditionalElement => true,
_ => return false,
}
}
let sub_elements = node
.parent()
.unwrap()
.children_with_tokens()
.skip_while(|n| !is_sub_element(n.kind()))
.filter(|n| match n {
NodeOrToken::Node(_) => is_sub_element(n.kind()),
NodeOrToken::Token(t) => {
t.kind() == SyntaxKind::Whitespace
&& t.next_sibling_or_token().map_or(false, |n| is_sub_element(n.kind()))
}
})
.collect::<Vec<_>>();
if match component {
// A top-level component element can only be removed if it contains
// exactly one sub-element (without any condition or assignment)
// that can substitute the component element.
Some(_) => {
sub_elements.len() == 1
&& sub_elements
.first()
.and_then(|n| n.as_node().unwrap().first_child_or_token().map(|n| n.kind()))
== Some(SyntaxKind::Element)
}
// Any other element can be removed in favor of one or more sub-elements.
None => sub_elements.iter().any(|n| n.kind() == SyntaxKind::SubElement),
} {
let unindented_lines = sub_elements
.iter()
.map(|n| match n {
NodeOrToken::Node(n) => n
.text()
.to_string()
.lines()
.map(|line| line.strip_prefix(" ").unwrap_or(line).to_string())
.collect::<Vec<_>>()
.join("\n"),
NodeOrToken::Token(t) => {
t.text().strip_suffix(" ").unwrap_or(t.text()).to_string()
}
})
.collect::<Vec<String>>();
let edits = vec![TextEdit::new(
lsp_types::Range::new(r.start, r.end),
unindented_lines.concat(),
)];
result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
title: "Remove element".into(),
kind: Some(lsp_types::CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit {
changes: Some(std::iter::once((uri, edits)).collect()),
..Default::default()
}),
..Default::default()
}));
}
} }
(!result.is_empty()).then_some(result) (!result.is_empty()).then_some(result)
@ -1355,10 +1426,39 @@ component Demo {
#[test] #[test]
fn test_code_actions() { fn test_code_actions() {
let (mut dc, url, _) = complex_document_cache(); let (mut dc, url, _) = loaded_document_cache(
r#"import { Button, VerticalBox , LineEdit, HorizontalBox} from "std-widgets.slint";
export component TestWindow inherits Window {
VerticalBox {
alignment: start;
Text {
text: "Hello World!";
font-size: 20px;
}
input := LineEdit {
placeholder-text: "Enter your name";
}
if (true): HorizontalBox {
alignment: end;
Button { text: "Cancel"; }
Button {
text: "OK";
primary: true;
}
}
}
}"#
.into(),
);
let mut capabilities = ClientCapabilities::default(); let mut capabilities = ClientCapabilities::default();
let text_literal = lsp_types::Range::new(Position::new(33, 22), Position::new(33, 33)); let text_literal = lsp_types::Range::new(Position::new(7, 18), Position::new(7, 32));
assert_eq!( assert_eq!(
token_descr(&mut dc, &url, &text_literal.start) token_descr(&mut dc, &url, &text_literal.start)
.and_then(|(token, _)| get_code_actions(&mut dc, token, &capabilities)), .and_then(|(token, _)| get_code_actions(&mut dc, token, &capabilities)),
@ -1387,7 +1487,7 @@ component Demo {
}),]) }),])
); );
let text_element = lsp_types::Range::new(Position::new(32, 12), Position::new(35, 13)); let text_element = lsp_types::Range::new(Position::new(6, 8), Position::new(9, 9));
for offset in 0..=4 { for offset in 0..=4 {
let pos = Position::new(text_element.start.line, text_element.start.character + offset); let pos = Position::new(text_element.start.line, text_element.start.character + offset);
@ -1418,10 +1518,10 @@ component Demo {
vec![TextEdit::new( vec![TextEdit::new(
text_element, text_element,
r#"${0:element} { r#"${0:element} {
Text { Text {
text: "Duration:"; text: "Hello World!";
vertical-alignment: center; font-size: 20px;
} }
}"# }"#
.into() .into()
)] )]
@ -1434,5 +1534,76 @@ component Demo {
}),]) }),])
); );
} }
let horizontal_box = lsp_types::Range::new(Position::new(15, 19), Position::new(24, 9));
capabilities.experimental = None;
assert_eq!(
token_descr(&mut dc, &url, &horizontal_box.start)
.and_then(|(token, _)| get_code_actions(&mut dc, token, &capabilities)),
None
);
capabilities.experimental = Some(serde_json::json!({"snippetTextEdit": true}));
assert_eq!(
token_descr(&mut dc, &url, &horizontal_box.start)
.and_then(|(token, _)| get_code_actions(&mut dc, token, &capabilities)),
Some(vec![
CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
title: "Wrap in element".into(),
kind: Some(lsp_types::CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit {
changes: Some(
std::iter::once((
url.clone(),
vec![TextEdit::new(
horizontal_box,
r#"${0:element} {
HorizontalBox {
alignment: end;
Button { text: "Cancel"; }
Button {
text: "OK";
primary: true;
}
}
}"#
.into()
)]
))
.collect()
),
..Default::default()
}),
..Default::default()
}),
CodeActionOrCommand::CodeAction(lsp_types::CodeAction {
title: "Remove element".into(),
kind: Some(lsp_types::CodeActionKind::REFACTOR),
edit: Some(WorkspaceEdit {
changes: Some(
std::iter::once((
url.clone(),
vec![TextEdit::new(
horizontal_box,
r#"Button { text: "Cancel"; }
Button {
text: "OK";
primary: true;
}"#
.into()
)]
))
.collect()
),
..Default::default()
}),
..Default::default()
})
])
);
} }
} }