Skip to content

Commit

Permalink
Auto merge of rust-lang#14442 - DropDemBits:structured-snippet-api, r…
Browse files Browse the repository at this point in the history
…=Veykril

internal: Implement Structured API for snippets

Fixes rust-lang#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.
  • Loading branch information
bors committed Apr 5, 2023
2 parents 25124a8 + 369f477 commit da9c0bd
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 13 deletions.
30 changes: 19 additions & 11 deletions crates/ide-assists/src/handlers/introduce_named_generic.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use syntax::{
ast::{self, edit_in_place::GenericParamsOwnerEdit, make, AstNode},
ast::{self, edit_in_place::GenericParamsOwnerEdit, make, AstNode, HasGenericParams},
ted,
};

Expand All @@ -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>()?;
Expand All @@ -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);
}
}
},
)
}
Expand All @@ -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) {}"#,
);
}

Expand All @@ -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) {}"#,
);
}

Expand All @@ -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) {}"#,
);
}

Expand All @@ -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) {}"#,
);
}

Expand All @@ -95,7 +103,7 @@ fn foo<
>(bar: $0impl Bar) {}
"#,
r#"
fn foo<B: Bar
fn foo<$0B: Bar
>(bar: B) {}
"#,
);
Expand All @@ -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) {}"#,
);
}

Expand All @@ -127,7 +135,7 @@ fn foo<
fn foo<
G: Foo,
F,
H, B: Bar,
H, $0B: Bar,
>(bar: B) {}
"#,
);
Expand All @@ -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) {}"#,
);
}
}
2 changes: 1 addition & 1 deletion crates/ide-assists/src/tests/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
"#####,
)
}
Expand Down
109 changes: 108 additions & 1 deletion crates/ide-db/src/source_change.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -99,13 +99,21 @@ 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 {
immutable: SyntaxNode,
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();
Expand All @@ -131,6 +139,7 @@ impl SourceChangeBuilder {
source_change: SourceChange::default(),
trigger_signature_help: false,
mutated_tree: None,
snippet_builder: None,
}
}

Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
}

0 comments on commit da9c0bd

Please sign in to comment.