Skip to content

Commit 77cbf4a

Browse files
committed
Bring the implementation closer to VSCode snippet definitions
1 parent 2b17da6 commit 77cbf4a

File tree

9 files changed

+160
-179
lines changed

9 files changed

+160
-179
lines changed

crates/ide/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ pub use ide_assists::{
9898
Assist, AssistConfig, AssistId, AssistKind, AssistResolveStrategy, SingleResolve,
9999
};
100100
pub use ide_completion::{
101-
CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit,
102-
PostfixSnippet, PostfixSnippetScope, Snippet, SnippetScope,
101+
CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit, Snippet,
102+
SnippetScope,
103103
};
104104
pub use ide_db::{
105105
base_db::{

crates/ide_completion/src/completions/postfix.rs

+24-21
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub(crate) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
5656

5757
let postfix_snippet = build_postfix_snippet_builder(ctx, cap, &dot_receiver);
5858

59-
if !ctx.config.postfix_snippets.is_empty() {
59+
if !ctx.config.snippets.is_empty() {
6060
add_custom_postfix_completions(acc, ctx, &postfix_snippet, &receiver_text);
6161
}
6262

@@ -230,21 +230,23 @@ fn add_custom_postfix_completions(
230230
) -> Option<()> {
231231
let import_scope =
232232
ImportScope::find_insert_use_container_with_macros(&ctx.token.parent()?, &ctx.sema)?;
233-
ctx.config.postfix_snippets.iter().for_each(|snippet| {
234-
let imports = match snippet.imports(ctx, &import_scope) {
235-
Some(imports) => imports,
236-
None => return,
237-
};
238-
let mut builder = postfix_snippet(
239-
&snippet.label,
240-
snippet.description.as_deref().unwrap_or_default(),
241-
&format!("{}", snippet.snippet(&receiver_text)),
242-
);
243-
for import in imports.into_iter() {
244-
builder.add_import(import);
245-
}
246-
builder.add_to(acc);
247-
});
233+
ctx.config.postfix_snippets().filter(|(_, snip)| snip.is_expr()).for_each(
234+
|(trigger, snippet)| {
235+
let imports = match snippet.imports(ctx, &import_scope) {
236+
Some(imports) => imports,
237+
None => return,
238+
};
239+
let mut builder = postfix_snippet(
240+
trigger,
241+
snippet.description.as_deref().unwrap_or_default(),
242+
&snippet.postfix_snippet(&receiver_text),
243+
);
244+
for import in imports.into_iter() {
245+
builder.add_import(import);
246+
}
247+
builder.add_to(acc);
248+
},
249+
);
248250
None
249251
}
250252

@@ -254,7 +256,7 @@ mod tests {
254256

255257
use crate::{
256258
tests::{check_edit, check_edit_with_config, filtered_completion_list, TEST_CONFIG},
257-
CompletionConfig, CompletionKind, PostfixSnippet,
259+
CompletionConfig, CompletionKind, Snippet,
258260
};
259261

260262
fn check(ra_fixture: &str, expect: Expect) {
@@ -476,12 +478,13 @@ fn main() {
476478
fn custom_postfix_completion() {
477479
check_edit_with_config(
478480
CompletionConfig {
479-
postfix_snippets: vec![PostfixSnippet::new(
480-
"break".into(),
481-
&["ControlFlow::Break($receiver)".into()],
481+
snippets: vec![Snippet::new(
482482
&[],
483+
&["break".into()],
484+
&["ControlFlow::Break($receiver)".into()],
485+
"",
483486
&["core::ops::ControlFlow".into()],
484-
crate::PostfixSnippetScope::Expr,
487+
crate::SnippetScope::Expr,
485488
)
486489
.unwrap()],
487490
..TEST_CONFIG

crates/ide_completion/src/completions/snippet.rs

+17-14
Original file line numberDiff line numberDiff line change
@@ -103,18 +103,20 @@ fn add_custom_completions(
103103
) -> Option<()> {
104104
let import_scope =
105105
ImportScope::find_insert_use_container_with_macros(&ctx.token.parent()?, &ctx.sema)?;
106-
ctx.config.snippets.iter().filter(|snip| snip.scope == scope).for_each(|snip| {
107-
let imports = match snip.imports(ctx, &import_scope) {
108-
Some(imports) => imports,
109-
None => return,
110-
};
111-
let mut builder = snippet(ctx, cap, &snip.label, &snip.snippet);
112-
for import in imports.into_iter() {
113-
builder.add_import(import);
114-
}
115-
builder.detail(snip.description.as_deref().unwrap_or_default());
116-
builder.add_to(acc);
117-
});
106+
ctx.config.prefix_snippets().filter(|(_, snip)| snip.scope == scope).for_each(
107+
|(trigger, snip)| {
108+
let imports = match snip.imports(ctx, &import_scope) {
109+
Some(imports) => imports,
110+
None => return,
111+
};
112+
let mut builder = snippet(ctx, cap, &trigger, &snip.snippet());
113+
for import in imports.into_iter() {
114+
builder.add_import(import);
115+
}
116+
builder.detail(snip.description.as_deref().unwrap_or_default());
117+
builder.add_to(acc);
118+
},
119+
);
118120
None
119121
}
120122

@@ -130,9 +132,10 @@ mod tests {
130132
check_edit_with_config(
131133
CompletionConfig {
132134
snippets: vec![Snippet::new(
133-
"break".into(),
134-
&["ControlFlow::Break(())".into()],
135+
&["break".into()],
135136
&[],
137+
&["ControlFlow::Break(())".into()],
138+
"",
136139
&["core::ops::ControlFlow".into()],
137140
crate::SnippetScope::Expr,
138141
)

crates/ide_completion/src/config.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
use ide_db::helpers::{insert_use::InsertUseConfig, SnippetCap};
88

9-
use crate::snippet::{PostfixSnippet, Snippet};
9+
use crate::snippet::Snippet;
1010

1111
#[derive(Clone, Debug, PartialEq, Eq)]
1212
pub struct CompletionConfig {
@@ -17,6 +17,18 @@ pub struct CompletionConfig {
1717
pub add_call_argument_snippets: bool,
1818
pub snippet_cap: Option<SnippetCap>,
1919
pub insert_use: InsertUseConfig,
20-
pub postfix_snippets: Vec<PostfixSnippet>,
2120
pub snippets: Vec<Snippet>,
2221
}
22+
23+
impl CompletionConfig {
24+
pub fn postfix_snippets(&self) -> impl Iterator<Item = (&str, &Snippet)> {
25+
self.snippets.iter().flat_map(|snip| {
26+
snip.postfix_triggers.iter().map(move |trigger| (trigger.as_str(), snip))
27+
})
28+
}
29+
pub fn prefix_snippets(&self) -> impl Iterator<Item = (&str, &Snippet)> {
30+
self.snippets.iter().flat_map(|snip| {
31+
snip.prefix_triggers.iter().map(move |trigger| (trigger.as_str(), snip))
32+
})
33+
}
34+
}

crates/ide_completion/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use crate::{completions::Completions, context::CompletionContext, item::Completi
2929
pub use crate::{
3030
config::CompletionConfig,
3131
item::{CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit},
32-
snippet::{PostfixSnippet, PostfixSnippetScope, Snippet, SnippetScope},
32+
snippet::{Snippet, SnippetScope},
3333
};
3434

3535
//FIXME: split the following feature into fine-grained features.

crates/ide_completion/src/snippet.rs

+71-63
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,98 @@
11
//! User (postfix)-snippet definitions.
22
//!
33
//! Actual logic is implemented in [`crate::completions::postfix`] and [`crate::completions::snippet`].
4+
5+
// Feature: User Snippet Completions
6+
//
7+
// rust-analyzer allows the user to define custom (postfix)-snippets that may depend on items to be accessible for the current scope to be applicable.
8+
//
9+
// A custom snippet can be defined by adding it to the `rust-analyzer.completion.snippets` object respectively.
10+
//
11+
// [source,json]
12+
// ----
13+
// {
14+
// "rust-analyzer.completion.snippets": {
15+
// "thread spawn": {
16+
// "prefix": ["spawn", "tspawn"],
17+
// "body": [
18+
// "thread::spawn(move || {",
19+
// "\t$0",
20+
// ")};",
21+
// ],
22+
// "description": "Insert a thread::spawn call",
23+
// "requires": "std::thread",
24+
// "scope": "expr",
25+
// }
26+
// }
27+
// }
28+
// ----
29+
//
30+
// In the example above:
31+
//
32+
// * `"thread spawn"` is the name of the snippet.
33+
//
34+
// * `prefix` defines one or more trigger words that will trigger the snippets completion.
35+
// Using `postfix` will instead create a postfix snippet.
36+
//
37+
// * `body` is one or more lines of content joined via newlines for the final output.
38+
//
39+
// * `description` is an optional description of the snippet, if unset the snippet name will be used.
40+
//
41+
// * `requires` is an optional list of item paths that have to be resolvable in the current crate where the completion is rendered.
42+
// On failure of resolution the snippet won't be applicable, otherwise the snippet will insert an import for the items on insertion if
43+
// the items aren't yet in scope.
44+
//
45+
// * `scope` is an optional filter for when the snippet should be applicable. Possible values are:
46+
// ** for Snippet-Scopes: `expr`, `item` (default: `item`)
47+
// ** for Postfix-Snippet-Scopes: `expr`, `type` (default: `expr`)
48+
//
49+
// The `body` field also has access to placeholders as visible in the example as `$0`.
50+
// These placeholders take the form of `$number` or `${number:placeholder_text}` which can be traversed as tabstop in ascending order starting from 1,
51+
// with `$0` being a special case that always comes last.
52+
//
53+
// There is also a special placeholder, `${receiver}`, which will be replaced by the receiver expression for postfix snippets, or nothing in case of normal snippets.
54+
// It does not act as a tabstop.
455
use ide_db::helpers::{import_assets::LocatedImport, insert_use::ImportScope};
556
use itertools::Itertools;
657
use syntax::ast;
758

859
use crate::{context::CompletionContext, ImportEdit};
960

10-
#[derive(Clone, Debug, PartialEq, Eq)]
11-
pub enum PostfixSnippetScope {
12-
Expr,
13-
Type,
14-
}
15-
1661
#[derive(Clone, Debug, PartialEq, Eq)]
1762
pub enum SnippetScope {
1863
Item,
1964
Expr,
65+
Type,
2066
}
2167

2268
#[derive(Clone, Debug, PartialEq, Eq)]
23-
pub struct PostfixSnippet {
24-
pub scope: PostfixSnippetScope,
25-
pub label: String,
26-
snippet: String,
27-
pub description: Option<String>,
28-
pub requires: Box<[String]>,
29-
}
30-
31-
#[derive(Clone, Debug, PartialEq, Eq)]
32-
#[non_exhaustive]
3369
pub struct Snippet {
70+
pub postfix_triggers: Box<[String]>,
71+
pub prefix_triggers: Box<[String]>,
3472
pub scope: SnippetScope,
35-
pub label: String,
36-
pub snippet: String,
73+
snippet: String,
3774
pub description: Option<String>,
3875
pub requires: Box<[String]>,
3976
}
77+
4078
impl Snippet {
4179
pub fn new(
42-
label: String,
80+
prefix_triggers: &[String],
81+
postfix_triggers: &[String],
4382
snippet: &[String],
44-
description: &[String],
83+
description: &str,
4584
requires: &[String],
4685
scope: SnippetScope,
4786
) -> Option<Self> {
4887
let (snippet, description) = validate_snippet(snippet, description, requires)?;
4988
Some(Snippet {
89+
// Box::into doesn't work as that has a Copy bound 😒
90+
postfix_triggers: postfix_triggers.iter().cloned().collect(),
91+
prefix_triggers: prefix_triggers.iter().cloned().collect(),
5092
scope,
51-
label,
5293
snippet,
5394
description,
54-
requires: requires.iter().cloned().collect(), // Box::into doesn't work as that has a Copy bound 😒
95+
requires: requires.iter().cloned().collect(),
5596
})
5697
}
5798

@@ -64,52 +105,20 @@ impl Snippet {
64105
import_edits(ctx, import_scope, &self.requires)
65106
}
66107

67-
pub fn is_item(&self) -> bool {
68-
self.scope == SnippetScope::Item
69-
}
70-
71-
pub fn is_expr(&self) -> bool {
72-
self.scope == SnippetScope::Expr
73-
}
74-
}
75-
76-
impl PostfixSnippet {
77-
pub fn new(
78-
label: String,
79-
snippet: &[String],
80-
description: &[String],
81-
requires: &[String],
82-
scope: PostfixSnippetScope,
83-
) -> Option<Self> {
84-
let (snippet, description) = validate_snippet(snippet, description, requires)?;
85-
Some(PostfixSnippet {
86-
scope,
87-
label,
88-
snippet,
89-
description,
90-
requires: requires.iter().cloned().collect(), // Box::into doesn't work as that has a Copy bound 😒
91-
})
92-
}
93-
94-
/// Returns None if the required items do not resolve.
95-
pub(crate) fn imports(
96-
&self,
97-
ctx: &CompletionContext,
98-
import_scope: &ImportScope,
99-
) -> Option<Vec<ImportEdit>> {
100-
import_edits(ctx, import_scope, &self.requires)
108+
pub fn snippet(&self) -> String {
109+
self.snippet.replace("${receiver}", "")
101110
}
102111

103-
pub fn snippet(&self, receiver: &str) -> String {
104-
self.snippet.replace("$receiver", receiver)
112+
pub fn postfix_snippet(&self, receiver: &str) -> String {
113+
self.snippet.replace("${receiver}", receiver)
105114
}
106115

107116
pub fn is_item(&self) -> bool {
108-
self.scope == PostfixSnippetScope::Type
117+
self.scope == SnippetScope::Item
109118
}
110119

111120
pub fn is_expr(&self) -> bool {
112-
self.scope == PostfixSnippetScope::Expr
121+
self.scope == SnippetScope::Expr
113122
}
114123
}
115124

@@ -147,7 +156,7 @@ fn import_edits(
147156

148157
fn validate_snippet(
149158
snippet: &[String],
150-
description: &[String],
159+
description: &str,
151160
requires: &[String],
152161
) -> Option<(String, Option<String>)> {
153162
// validate that these are indeed simple paths
@@ -162,7 +171,6 @@ fn validate_snippet(
162171
return None;
163172
}
164173
let snippet = snippet.iter().join("\n");
165-
let description = description.iter().join("\n");
166-
let description = if description.is_empty() { None } else { Some(description) };
174+
let description = if description.is_empty() { None } else { Some(description.to_owned()) };
167175
Some((snippet, description))
168176
}

crates/ide_completion/src/tests.rs

-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ pub(crate) const TEST_CONFIG: CompletionConfig = CompletionConfig {
7474
group: true,
7575
skip_glob_imports: true,
7676
},
77-
postfix_snippets: Vec::new(),
7877
snippets: Vec::new(),
7978
};
8079

0 commit comments

Comments
 (0)