Auto merge of #14442 - DropDemBits:structured-snippet-api, r=Veykril

internal: Implement Structured API for snippets

Fixes #11638 (including moving the cursor before the generated type parameter)

Adds `add_tabstop_{before,after}` for inserting tabstop snippets before & after nodes, and `add_placeholder_snippet` for wrapping nodes inside placeholder nodes.

Currently, the snippets are inserted into the syntax tree in `SourceChange::commit` so that snippet bits won't interfere with syntax lookups before completing a `SourceChange`.

It would be preferable if snippet rendering was deferred to after so that rendering can work directly with text ranges, but have left that for a future PR (it would also make it easier to finely specify which text edits have snippets in them).

Another possible snippet variation to support would be a group of placeholders (i.e. placeholders with the same tabstop number) so that a generated item and its uses can be renamed right as it's generated, which is something that is technically supported by the current snippet hack in VSCode, though it's not clear if that's a thing that is officially supported.
This commit is contained in:
bors 2023-04-05 14:33:30 +00:00
commit da9c0bd0a7
3 changed files with 128 additions and 13 deletions

View file

@ -1,5 +1,5 @@
use syntax::{
ast::{self, edit_in_place::GenericParamsOwnerEdit, make, AstNode},
ast::{self, edit_in_place::GenericParamsOwnerEdit, make, AstNode, HasGenericParams},
ted,
};
@ -14,7 +14,7 @@ use crate::{utils::suggest_name, AssistContext, AssistId, AssistKind, Assists};
// ```
// ->
// ```
// fn foo<B: Bar>(bar: B) {}
// fn foo<$0B: Bar>(bar: B) {}
// ```
pub(crate) fn introduce_named_generic(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
let impl_trait_type = ctx.find_node_at_offset::<ast::ImplTraitType>()?;
@ -39,7 +39,15 @@ pub(crate) fn introduce_named_generic(acc: &mut Assists, ctx: &AssistContext<'_>
let new_ty = make::ty(&type_param_name).clone_for_update();
ted::replace(impl_trait_type.syntax(), new_ty.syntax());
fn_.get_or_create_generic_param_list().add_generic_param(type_param.into())
fn_.get_or_create_generic_param_list().add_generic_param(type_param.into());
if let Some(cap) = ctx.config.snippet_cap {
if let Some(generic_param) =
fn_.generic_param_list().and_then(|it| it.generic_params().last())
{
edit.add_tabstop_before(cap, generic_param);
}
}
},
)
}
@ -55,7 +63,7 @@ mod tests {
check_assist(
introduce_named_generic,
r#"fn foo<G>(bar: $0impl Bar) {}"#,
r#"fn foo<G, B: Bar>(bar: B) {}"#,
r#"fn foo<G, $0B: Bar>(bar: B) {}"#,
);
}
@ -64,7 +72,7 @@ mod tests {
check_assist(
introduce_named_generic,
r#"fn foo(bar: $0impl Bar) {}"#,
r#"fn foo<B: Bar>(bar: B) {}"#,
r#"fn foo<$0B: Bar>(bar: B) {}"#,
);
}
@ -73,7 +81,7 @@ mod tests {
check_assist(
introduce_named_generic,
r#"fn foo<G>(foo: impl Foo, bar: $0impl Bar) {}"#,
r#"fn foo<G, B: Bar>(foo: impl Foo, bar: B) {}"#,
r#"fn foo<G, $0B: Bar>(foo: impl Foo, bar: B) {}"#,
);
}
@ -82,7 +90,7 @@ mod tests {
check_assist(
introduce_named_generic,
r#"fn foo<>(bar: $0impl Bar) {}"#,
r#"fn foo<B: Bar>(bar: B) {}"#,
r#"fn foo<$0B: Bar>(bar: B) {}"#,
);
}
@ -95,7 +103,7 @@ fn foo<
>(bar: $0impl Bar) {}
"#,
r#"
fn foo<B: Bar
fn foo<$0B: Bar
>(bar: B) {}
"#,
);
@ -108,7 +116,7 @@ fn foo<B: Bar
check_assist(
introduce_named_generic,
r#"fn foo<B>(bar: $0impl Bar) {}"#,
r#"fn foo<B, B: Bar>(bar: B) {}"#,
r#"fn foo<B, $0B: Bar>(bar: B) {}"#,
);
}
@ -127,7 +135,7 @@ fn foo<
fn foo<
G: Foo,
F,
H, B: Bar,
H, $0B: Bar,
>(bar: B) {}
"#,
);
@ -138,7 +146,7 @@ fn foo<
check_assist(
introduce_named_generic,
r#"fn foo(bar: $0impl Foo + Bar) {}"#,
r#"fn foo<F: Foo + Bar>(bar: F) {}"#,
r#"fn foo<$0F: Foo + Bar>(bar: F) {}"#,
);
}
}

View file

@ -1596,7 +1596,7 @@ fn doctest_introduce_named_generic() {
fn foo(bar: $0impl Bar) {}
"#####,
r#####"
fn foo<B: Bar>(bar: B) {}
fn foo<$0B: Bar>(bar: B) {}
"#####,
)
}

View file

@ -7,7 +7,7 @@ use std::{collections::hash_map::Entry, iter, mem};
use base_db::{AnchoredPathBuf, FileId};
use stdx::{hash::NoHashHashMap, never};
use syntax::{algo, AstNode, SyntaxNode, SyntaxNodePtr, TextRange, TextSize};
use syntax::{algo, ast, ted, AstNode, SyntaxNode, SyntaxNodePtr, TextRange, TextSize};
use text_edit::{TextEdit, TextEditBuilder};
use crate::SnippetCap;
@ -99,6 +99,8 @@ pub struct SourceChangeBuilder {
/// Maps the original, immutable `SyntaxNode` to a `clone_for_update` twin.
pub mutated_tree: Option<TreeMutator>,
/// Keeps track of where to place snippets
pub snippet_builder: Option<SnippetBuilder>,
}
pub struct TreeMutator {
@ -106,6 +108,12 @@ pub struct TreeMutator {
mutable_clone: SyntaxNode,
}
#[derive(Default)]
pub struct SnippetBuilder {
/// Where to place snippets at
places: Vec<PlaceSnippet>,
}
impl TreeMutator {
pub fn new(immutable: &SyntaxNode) -> TreeMutator {
let immutable = immutable.ancestors().last().unwrap();
@ -131,6 +139,7 @@ impl SourceChangeBuilder {
source_change: SourceChange::default(),
trigger_signature_help: false,
mutated_tree: None,
snippet_builder: None,
}
}
@ -140,6 +149,17 @@ impl SourceChangeBuilder {
}
fn commit(&mut self) {
// Render snippets first so that they get bundled into the tree diff
if let Some(mut snippets) = self.snippet_builder.take() {
// Last snippet always has stop index 0
let last_stop = snippets.places.pop().unwrap();
last_stop.place(0);
for (index, stop) in snippets.places.into_iter().enumerate() {
stop.place(index + 1)
}
}
if let Some(tm) = self.mutated_tree.take() {
algo::diff(&tm.immutable, &tm.mutable_clone).into_text_edit(&mut self.edit)
}
@ -214,6 +234,30 @@ impl SourceChangeBuilder {
self.trigger_signature_help = true;
}
/// Adds a tabstop snippet to place the cursor before `node`
pub fn add_tabstop_before(&mut self, _cap: SnippetCap, node: impl AstNode) {
assert!(node.syntax().parent().is_some());
self.add_snippet(PlaceSnippet::Before(node.syntax().clone()));
}
/// Adds a tabstop snippet to place the cursor after `node`
pub fn add_tabstop_after(&mut self, _cap: SnippetCap, node: impl AstNode) {
assert!(node.syntax().parent().is_some());
self.add_snippet(PlaceSnippet::After(node.syntax().clone()));
}
/// Adds a snippet to move the cursor selected over `node`
pub fn add_placeholder_snippet(&mut self, _cap: SnippetCap, node: impl AstNode) {
assert!(node.syntax().parent().is_some());
self.add_snippet(PlaceSnippet::Over(node.syntax().clone()))
}
fn add_snippet(&mut self, snippet: PlaceSnippet) {
let snippet_builder = self.snippet_builder.get_or_insert(SnippetBuilder { places: vec![] });
snippet_builder.places.push(snippet);
self.source_change.is_snippet = true;
}
pub fn finish(mut self) -> SourceChange {
self.commit();
mem::take(&mut self.source_change)
@ -236,3 +280,66 @@ impl From<FileSystemEdit> for SourceChange {
}
}
}
enum PlaceSnippet {
/// Place a tabstop before a node
Before(SyntaxNode),
/// Place a tabstop before a node
After(SyntaxNode),
/// Place a placeholder snippet in place of the node
Over(SyntaxNode),
}
impl PlaceSnippet {
/// Places the snippet before or over a node with the given tab stop index
fn place(self, order: usize) {
// ensure the target node is still attached
match &self {
PlaceSnippet::Before(node) | PlaceSnippet::After(node) | PlaceSnippet::Over(node) => {
// node should still be in the tree, but if it isn't
// then it's okay to just ignore this place
if stdx::never!(node.parent().is_none()) {
return;
}
}
}
match self {
PlaceSnippet::Before(node) => {
ted::insert_raw(ted::Position::before(&node), Self::make_tab_stop(order));
}
PlaceSnippet::After(node) => {
ted::insert_raw(ted::Position::after(&node), Self::make_tab_stop(order));
}
PlaceSnippet::Over(node) => {
let position = ted::Position::before(&node);
node.detach();
let snippet = ast::SourceFile::parse(&format!("${{{order}:_}}"))
.syntax_node()
.clone_for_update();
let placeholder =
snippet.descendants().find_map(ast::UnderscoreExpr::cast).unwrap();
ted::replace(placeholder.syntax(), node);
ted::insert_raw(position, snippet);
}
}
}
fn make_tab_stop(order: usize) -> SyntaxNode {
let stop = ast::SourceFile::parse(&format!("stop!(${order})"))
.syntax_node()
.descendants()
.find_map(ast::TokenTree::cast)
.unwrap()
.syntax()
.clone_for_update();
stop.first_token().unwrap().detach();
stop.last_token().unwrap().detach();
stop
}
}