Skip to content

Commit da9c0bd

Browse files
committed
Auto merge of rust-lang#14442 - DropDemBits:structured-snippet-api, r=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.
2 parents 25124a8 + 369f477 commit da9c0bd

File tree

3 files changed

+128
-13
lines changed

3 files changed

+128
-13
lines changed

crates/ide-assists/src/handlers/introduce_named_generic.rs

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use syntax::{
2-
ast::{self, edit_in_place::GenericParamsOwnerEdit, make, AstNode},
2+
ast::{self, edit_in_place::GenericParamsOwnerEdit, make, AstNode, HasGenericParams},
33
ted,
44
};
55

@@ -14,7 +14,7 @@ use crate::{utils::suggest_name, AssistContext, AssistId, AssistKind, Assists};
1414
// ```
1515
// ->
1616
// ```
17-
// fn foo<B: Bar>(bar: B) {}
17+
// fn foo<$0B: Bar>(bar: B) {}
1818
// ```
1919
pub(crate) fn introduce_named_generic(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
2020
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<'_>
3939
let new_ty = make::ty(&type_param_name).clone_for_update();
4040

4141
ted::replace(impl_trait_type.syntax(), new_ty.syntax());
42-
fn_.get_or_create_generic_param_list().add_generic_param(type_param.into())
42+
fn_.get_or_create_generic_param_list().add_generic_param(type_param.into());
43+
44+
if let Some(cap) = ctx.config.snippet_cap {
45+
if let Some(generic_param) =
46+
fn_.generic_param_list().and_then(|it| it.generic_params().last())
47+
{
48+
edit.add_tabstop_before(cap, generic_param);
49+
}
50+
}
4351
},
4452
)
4553
}
@@ -55,7 +63,7 @@ mod tests {
5563
check_assist(
5664
introduce_named_generic,
5765
r#"fn foo<G>(bar: $0impl Bar) {}"#,
58-
r#"fn foo<G, B: Bar>(bar: B) {}"#,
66+
r#"fn foo<G, $0B: Bar>(bar: B) {}"#,
5967
);
6068
}
6169

@@ -64,7 +72,7 @@ mod tests {
6472
check_assist(
6573
introduce_named_generic,
6674
r#"fn foo(bar: $0impl Bar) {}"#,
67-
r#"fn foo<B: Bar>(bar: B) {}"#,
75+
r#"fn foo<$0B: Bar>(bar: B) {}"#,
6876
);
6977
}
7078

@@ -73,7 +81,7 @@ mod tests {
7381
check_assist(
7482
introduce_named_generic,
7583
r#"fn foo<G>(foo: impl Foo, bar: $0impl Bar) {}"#,
76-
r#"fn foo<G, B: Bar>(foo: impl Foo, bar: B) {}"#,
84+
r#"fn foo<G, $0B: Bar>(foo: impl Foo, bar: B) {}"#,
7785
);
7886
}
7987

@@ -82,7 +90,7 @@ mod tests {
8290
check_assist(
8391
introduce_named_generic,
8492
r#"fn foo<>(bar: $0impl Bar) {}"#,
85-
r#"fn foo<B: Bar>(bar: B) {}"#,
93+
r#"fn foo<$0B: Bar>(bar: B) {}"#,
8694
);
8795
}
8896

@@ -95,7 +103,7 @@ fn foo<
95103
>(bar: $0impl Bar) {}
96104
"#,
97105
r#"
98-
fn foo<B: Bar
106+
fn foo<$0B: Bar
99107
>(bar: B) {}
100108
"#,
101109
);
@@ -108,7 +116,7 @@ fn foo<B: Bar
108116
check_assist(
109117
introduce_named_generic,
110118
r#"fn foo<B>(bar: $0impl Bar) {}"#,
111-
r#"fn foo<B, B: Bar>(bar: B) {}"#,
119+
r#"fn foo<B, $0B: Bar>(bar: B) {}"#,
112120
);
113121
}
114122

@@ -127,7 +135,7 @@ fn foo<
127135
fn foo<
128136
G: Foo,
129137
F,
130-
H, B: Bar,
138+
H, $0B: Bar,
131139
>(bar: B) {}
132140
"#,
133141
);
@@ -138,7 +146,7 @@ fn foo<
138146
check_assist(
139147
introduce_named_generic,
140148
r#"fn foo(bar: $0impl Foo + Bar) {}"#,
141-
r#"fn foo<F: Foo + Bar>(bar: F) {}"#,
149+
r#"fn foo<$0F: Foo + Bar>(bar: F) {}"#,
142150
);
143151
}
144152
}

crates/ide-assists/src/tests/generated.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1596,7 +1596,7 @@ fn doctest_introduce_named_generic() {
15961596
fn foo(bar: $0impl Bar) {}
15971597
"#####,
15981598
r#####"
1599-
fn foo<B: Bar>(bar: B) {}
1599+
fn foo<$0B: Bar>(bar: B) {}
16001600
"#####,
16011601
)
16021602
}

crates/ide-db/src/source_change.rs

+108-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::{collections::hash_map::Entry, iter, mem};
77

88
use base_db::{AnchoredPathBuf, FileId};
99
use stdx::{hash::NoHashHashMap, never};
10-
use syntax::{algo, AstNode, SyntaxNode, SyntaxNodePtr, TextRange, TextSize};
10+
use syntax::{algo, ast, ted, AstNode, SyntaxNode, SyntaxNodePtr, TextRange, TextSize};
1111
use text_edit::{TextEdit, TextEditBuilder};
1212

1313
use crate::SnippetCap;
@@ -99,13 +99,21 @@ pub struct SourceChangeBuilder {
9999

100100
/// Maps the original, immutable `SyntaxNode` to a `clone_for_update` twin.
101101
pub mutated_tree: Option<TreeMutator>,
102+
/// Keeps track of where to place snippets
103+
pub snippet_builder: Option<SnippetBuilder>,
102104
}
103105

104106
pub struct TreeMutator {
105107
immutable: SyntaxNode,
106108
mutable_clone: SyntaxNode,
107109
}
108110

111+
#[derive(Default)]
112+
pub struct SnippetBuilder {
113+
/// Where to place snippets at
114+
places: Vec<PlaceSnippet>,
115+
}
116+
109117
impl TreeMutator {
110118
pub fn new(immutable: &SyntaxNode) -> TreeMutator {
111119
let immutable = immutable.ancestors().last().unwrap();
@@ -131,6 +139,7 @@ impl SourceChangeBuilder {
131139
source_change: SourceChange::default(),
132140
trigger_signature_help: false,
133141
mutated_tree: None,
142+
snippet_builder: None,
134143
}
135144
}
136145

@@ -140,6 +149,17 @@ impl SourceChangeBuilder {
140149
}
141150

142151
fn commit(&mut self) {
152+
// Render snippets first so that they get bundled into the tree diff
153+
if let Some(mut snippets) = self.snippet_builder.take() {
154+
// Last snippet always has stop index 0
155+
let last_stop = snippets.places.pop().unwrap();
156+
last_stop.place(0);
157+
158+
for (index, stop) in snippets.places.into_iter().enumerate() {
159+
stop.place(index + 1)
160+
}
161+
}
162+
143163
if let Some(tm) = self.mutated_tree.take() {
144164
algo::diff(&tm.immutable, &tm.mutable_clone).into_text_edit(&mut self.edit)
145165
}
@@ -214,6 +234,30 @@ impl SourceChangeBuilder {
214234
self.trigger_signature_help = true;
215235
}
216236

237+
/// Adds a tabstop snippet to place the cursor before `node`
238+
pub fn add_tabstop_before(&mut self, _cap: SnippetCap, node: impl AstNode) {
239+
assert!(node.syntax().parent().is_some());
240+
self.add_snippet(PlaceSnippet::Before(node.syntax().clone()));
241+
}
242+
243+
/// Adds a tabstop snippet to place the cursor after `node`
244+
pub fn add_tabstop_after(&mut self, _cap: SnippetCap, node: impl AstNode) {
245+
assert!(node.syntax().parent().is_some());
246+
self.add_snippet(PlaceSnippet::After(node.syntax().clone()));
247+
}
248+
249+
/// Adds a snippet to move the cursor selected over `node`
250+
pub fn add_placeholder_snippet(&mut self, _cap: SnippetCap, node: impl AstNode) {
251+
assert!(node.syntax().parent().is_some());
252+
self.add_snippet(PlaceSnippet::Over(node.syntax().clone()))
253+
}
254+
255+
fn add_snippet(&mut self, snippet: PlaceSnippet) {
256+
let snippet_builder = self.snippet_builder.get_or_insert(SnippetBuilder { places: vec![] });
257+
snippet_builder.places.push(snippet);
258+
self.source_change.is_snippet = true;
259+
}
260+
217261
pub fn finish(mut self) -> SourceChange {
218262
self.commit();
219263
mem::take(&mut self.source_change)
@@ -236,3 +280,66 @@ impl From<FileSystemEdit> for SourceChange {
236280
}
237281
}
238282
}
283+
284+
enum PlaceSnippet {
285+
/// Place a tabstop before a node
286+
Before(SyntaxNode),
287+
/// Place a tabstop before a node
288+
After(SyntaxNode),
289+
/// Place a placeholder snippet in place of the node
290+
Over(SyntaxNode),
291+
}
292+
293+
impl PlaceSnippet {
294+
/// Places the snippet before or over a node with the given tab stop index
295+
fn place(self, order: usize) {
296+
// ensure the target node is still attached
297+
match &self {
298+
PlaceSnippet::Before(node) | PlaceSnippet::After(node) | PlaceSnippet::Over(node) => {
299+
// node should still be in the tree, but if it isn't
300+
// then it's okay to just ignore this place
301+
if stdx::never!(node.parent().is_none()) {
302+
return;
303+
}
304+
}
305+
}
306+
307+
match self {
308+
PlaceSnippet::Before(node) => {
309+
ted::insert_raw(ted::Position::before(&node), Self::make_tab_stop(order));
310+
}
311+
PlaceSnippet::After(node) => {
312+
ted::insert_raw(ted::Position::after(&node), Self::make_tab_stop(order));
313+
}
314+
PlaceSnippet::Over(node) => {
315+
let position = ted::Position::before(&node);
316+
node.detach();
317+
318+
let snippet = ast::SourceFile::parse(&format!("${{{order}:_}}"))
319+
.syntax_node()
320+
.clone_for_update();
321+
322+
let placeholder =
323+
snippet.descendants().find_map(ast::UnderscoreExpr::cast).unwrap();
324+
ted::replace(placeholder.syntax(), node);
325+
326+
ted::insert_raw(position, snippet);
327+
}
328+
}
329+
}
330+
331+
fn make_tab_stop(order: usize) -> SyntaxNode {
332+
let stop = ast::SourceFile::parse(&format!("stop!(${order})"))
333+
.syntax_node()
334+
.descendants()
335+
.find_map(ast::TokenTree::cast)
336+
.unwrap()
337+
.syntax()
338+
.clone_for_update();
339+
340+
stop.first_token().unwrap().detach();
341+
stop.last_token().unwrap().detach();
342+
343+
stop
344+
}
345+
}

0 commit comments

Comments
 (0)