mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-27 05:23:24 +00:00
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:
commit
da9c0bd0a7
3 changed files with 128 additions and 13 deletions
|
@ -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) {}"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {}
|
||||
"#####,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue