Skip to content

Commit c409cf0

Browse files
bors[bot]Veykril
andauthored
Merge #10458
10458: feat: Implement custom user snippets r=Veykril a=Veykril ![Y24dX7fOWX](https://user-images.githubusercontent.com/3757771/136059454-ceccfc2c-2c90-46da-8ad1-bac9c2e83ec1.gif) Allows us to address the following issues: - `.arc / .rc / .pin, similar to .box?` #7033 - `Add unsafe snippet` #10392, would allow users to have this without the diagnostic) - `.ok() postfix snippet is annoying` #9636, allows us to get rid of the `ok` postfix and similar ones - `Postfix vec completion` #7773 cc #772 Zulipd discussion: https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Frust-analyzer/topic/Custom.20Postfix.20snippets Co-authored-by: Lukas Wirth <[email protected]>
2 parents 86c534f + 041cfbe commit c409cf0

23 files changed

+550
-88
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ide/src/lib.rs

+4-12
Original file line numberDiff line numberDiff line change
@@ -98,7 +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,
101+
CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit, Snippet,
102+
SnippetScope,
102103
};
103104
pub use ide_db::{
104105
base_db::{
@@ -532,19 +533,10 @@ impl Analysis {
532533
&self,
533534
config: &CompletionConfig,
534535
position: FilePosition,
535-
full_import_path: &str,
536-
imported_name: String,
536+
imports: impl IntoIterator<Item = (String, String)> + std::panic::UnwindSafe,
537537
) -> Cancellable<Vec<TextEdit>> {
538538
Ok(self
539-
.with_db(|db| {
540-
ide_completion::resolve_completion_edits(
541-
db,
542-
config,
543-
position,
544-
full_import_path,
545-
imported_name,
546-
)
547-
})?
539+
.with_db(|db| ide_completion::resolve_completion_edits(db, config, position, imports))?
548540
.unwrap_or_default())
549541
}
550542

crates/ide_completion/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ itertools = "0.10.0"
1414
rustc-hash = "1.1.0"
1515
either = "1.6.1"
1616
once_cell = "1.7"
17+
smallvec = "1.4"
1718

1819
stdx = { path = "../stdx", version = "0.0.0" }
1920
syntax = { path = "../syntax", version = "0.0.0" }

crates/ide_completion/src/completions/postfix.rs

+63-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
33
mod format_like;
44

5+
use hir::Documentation;
56
use ide_db::{
6-
helpers::{FamousDefs, SnippetCap},
7+
helpers::{insert_use::ImportScope, FamousDefs, SnippetCap},
78
ty_filter::TryEnum,
89
};
910
use syntax::{
@@ -56,6 +57,10 @@ pub(crate) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
5657

5758
let postfix_snippet = build_postfix_snippet_builder(ctx, cap, &dot_receiver);
5859

60+
if !ctx.config.snippets.is_empty() {
61+
add_custom_postfix_completions(acc, ctx, &postfix_snippet, &receiver_text);
62+
}
63+
5964
let try_enum = TryEnum::from_ty(&ctx.sema, &receiver_ty.strip_references());
6065
if let Some(try_enum) = &try_enum {
6166
match try_enum {
@@ -218,13 +223,40 @@ fn build_postfix_snippet_builder<'a>(
218223
}
219224
}
220225

226+
fn add_custom_postfix_completions(
227+
acc: &mut Completions,
228+
ctx: &CompletionContext,
229+
postfix_snippet: impl Fn(&str, &str, &str) -> Builder,
230+
receiver_text: &str,
231+
) -> Option<()> {
232+
let import_scope =
233+
ImportScope::find_insert_use_container_with_macros(&ctx.token.parent()?, &ctx.sema)?;
234+
ctx.config.postfix_snippets().filter(|(_, snip)| snip.is_expr()).for_each(
235+
|(trigger, snippet)| {
236+
let imports = match snippet.imports(ctx, &import_scope) {
237+
Some(imports) => imports,
238+
None => return,
239+
};
240+
let body = snippet.postfix_snippet(&receiver_text);
241+
let mut builder =
242+
postfix_snippet(trigger, snippet.description.as_deref().unwrap_or_default(), &body);
243+
builder.documentation(Documentation::new(format!("```rust\n{}\n```", body)));
244+
for import in imports.into_iter() {
245+
builder.add_import(import);
246+
}
247+
builder.add_to(acc);
248+
},
249+
);
250+
None
251+
}
252+
221253
#[cfg(test)]
222254
mod tests {
223255
use expect_test::{expect, Expect};
224256

225257
use crate::{
226-
tests::{check_edit, filtered_completion_list},
227-
CompletionKind,
258+
tests::{check_edit, check_edit_with_config, filtered_completion_list, TEST_CONFIG},
259+
CompletionConfig, CompletionKind, Snippet,
228260
};
229261

230262
fn check(ra_fixture: &str, expect: Expect) {
@@ -442,6 +474,34 @@ fn main() {
442474
)
443475
}
444476

477+
#[test]
478+
fn custom_postfix_completion() {
479+
check_edit_with_config(
480+
CompletionConfig {
481+
snippets: vec![Snippet::new(
482+
&[],
483+
&["break".into()],
484+
&["ControlFlow::Break(${receiver})".into()],
485+
"",
486+
&["core::ops::ControlFlow".into()],
487+
crate::SnippetScope::Expr,
488+
)
489+
.unwrap()],
490+
..TEST_CONFIG
491+
},
492+
"break",
493+
r#"
494+
//- minicore: try
495+
fn main() { 42.$0 }
496+
"#,
497+
r#"
498+
use core::ops::ControlFlow;
499+
500+
fn main() { ControlFlow::Break(42) }
501+
"#,
502+
);
503+
}
504+
445505
#[test]
446506
fn postfix_completion_for_format_like_strings() {
447507
check_edit(

crates/ide_completion/src/completions/snippet.rs

+74-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
//! This file provides snippet completions, like `pd` => `eprintln!(...)`.
22
3-
use ide_db::helpers::SnippetCap;
3+
use hir::Documentation;
4+
use ide_db::helpers::{insert_use::ImportScope, SnippetCap};
45
use syntax::T;
56

67
use crate::{
78
context::PathCompletionContext, item::Builder, CompletionContext, CompletionItem,
8-
CompletionItemKind, CompletionKind, Completions,
9+
CompletionItemKind, CompletionKind, Completions, SnippetScope,
910
};
1011

1112
fn snippet(ctx: &CompletionContext, cap: SnippetCap, label: &str, snippet: &str) -> Builder {
@@ -29,6 +30,10 @@ pub(crate) fn complete_expr_snippet(acc: &mut Completions, ctx: &CompletionConte
2930
None => return,
3031
};
3132

33+
if !ctx.config.snippets.is_empty() {
34+
add_custom_completions(acc, ctx, cap, SnippetScope::Expr);
35+
}
36+
3237
if can_be_stmt {
3338
snippet(ctx, cap, "pd", "eprintln!(\"$0 = {:?}\", $0);").add_to(acc);
3439
snippet(ctx, cap, "ppd", "eprintln!(\"$0 = {:#?}\", $0);").add_to(acc);
@@ -52,6 +57,10 @@ pub(crate) fn complete_item_snippet(acc: &mut Completions, ctx: &CompletionConte
5257
None => return,
5358
};
5459

60+
if !ctx.config.snippets.is_empty() {
61+
add_custom_completions(acc, ctx, cap, SnippetScope::Item);
62+
}
63+
5564
let mut item = snippet(
5665
ctx,
5766
cap,
@@ -86,3 +95,66 @@ fn ${1:feature}() {
8695
let item = snippet(ctx, cap, "macro_rules", "macro_rules! $1 {\n\t($2) => {\n\t\t$0\n\t};\n}");
8796
item.add_to(acc);
8897
}
98+
99+
fn add_custom_completions(
100+
acc: &mut Completions,
101+
ctx: &CompletionContext,
102+
cap: SnippetCap,
103+
scope: SnippetScope,
104+
) -> Option<()> {
105+
let import_scope =
106+
ImportScope::find_insert_use_container_with_macros(&ctx.token.parent()?, &ctx.sema)?;
107+
ctx.config.prefix_snippets().filter(|(_, snip)| snip.scope == scope).for_each(
108+
|(trigger, snip)| {
109+
let imports = match snip.imports(ctx, &import_scope) {
110+
Some(imports) => imports,
111+
None => return,
112+
};
113+
let body = snip.snippet();
114+
let mut builder = snippet(ctx, cap, &trigger, &body);
115+
builder.documentation(Documentation::new(format!("```rust\n{}\n```", body)));
116+
for import in imports.into_iter() {
117+
builder.add_import(import);
118+
}
119+
builder.detail(snip.description.as_deref().unwrap_or_default());
120+
builder.add_to(acc);
121+
},
122+
);
123+
None
124+
}
125+
126+
#[cfg(test)]
127+
mod tests {
128+
use crate::{
129+
tests::{check_edit_with_config, TEST_CONFIG},
130+
CompletionConfig, Snippet,
131+
};
132+
133+
#[test]
134+
fn custom_snippet_completion() {
135+
check_edit_with_config(
136+
CompletionConfig {
137+
snippets: vec![Snippet::new(
138+
&["break".into()],
139+
&[],
140+
&["ControlFlow::Break(())".into()],
141+
"",
142+
&["core::ops::ControlFlow".into()],
143+
crate::SnippetScope::Expr,
144+
)
145+
.unwrap()],
146+
..TEST_CONFIG
147+
},
148+
"break",
149+
r#"
150+
//- minicore: try
151+
fn main() { $0 }
152+
"#,
153+
r#"
154+
use core::ops::ControlFlow;
155+
156+
fn main() { ControlFlow::Break(()) }
157+
"#,
158+
);
159+
}
160+
}

crates/ide_completion/src/config.rs

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

9+
use crate::snippet::Snippet;
10+
911
#[derive(Clone, Debug, PartialEq, Eq)]
1012
pub struct CompletionConfig {
1113
pub enable_postfix_completions: bool,
@@ -15,4 +17,18 @@ pub struct CompletionConfig {
1517
pub add_call_argument_snippets: bool,
1618
pub snippet_cap: Option<SnippetCap>,
1719
pub insert_use: InsertUseConfig,
20+
pub snippets: Vec<Snippet>,
21+
}
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+
}
1834
}

crates/ide_completion/src/context.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,8 @@ mod tests {
868868

869869
fn check_expected_type_and_name(ra_fixture: &str, expect: Expect) {
870870
let (db, pos) = position(ra_fixture);
871-
let completion_context = CompletionContext::new(&db, pos, &TEST_CONFIG).unwrap();
871+
let config = TEST_CONFIG;
872+
let completion_context = CompletionContext::new(&db, pos, &config).unwrap();
872873

873874
let ty = completion_context
874875
.expected_type

crates/ide_completion/src/item.rs

+16-16
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use ide_db::{
1111
},
1212
SymbolKind,
1313
};
14+
use smallvec::SmallVec;
1415
use stdx::{format_to, impl_from, never};
1516
use syntax::{algo, TextRange};
1617
use text_edit::TextEdit;
@@ -76,7 +77,7 @@ pub struct CompletionItem {
7677
ref_match: Option<Mutability>,
7778

7879
/// The import data to add to completion's edits.
79-
import_to_add: Option<ImportEdit>,
80+
import_to_add: SmallVec<[ImportEdit; 1]>,
8081
}
8182

8283
// We use custom debug for CompletionItem to make snapshot tests more readable.
@@ -305,7 +306,7 @@ impl CompletionItem {
305306
trigger_call_info: None,
306307
relevance: CompletionRelevance::default(),
307308
ref_match: None,
308-
import_to_add: None,
309+
imports_to_add: Default::default(),
309310
}
310311
}
311312

@@ -364,8 +365,8 @@ impl CompletionItem {
364365
self.ref_match.map(|mutability| (mutability, relevance))
365366
}
366367

367-
pub fn import_to_add(&self) -> Option<&ImportEdit> {
368-
self.import_to_add.as_ref()
368+
pub fn imports_to_add(&self) -> &[ImportEdit] {
369+
&self.import_to_add
369370
}
370371
}
371372

@@ -398,7 +399,7 @@ impl ImportEdit {
398399
pub(crate) struct Builder {
399400
source_range: TextRange,
400401
completion_kind: CompletionKind,
401-
import_to_add: Option<ImportEdit>,
402+
imports_to_add: SmallVec<[ImportEdit; 1]>,
402403
trait_name: Option<String>,
403404
label: String,
404405
insert_text: Option<String>,
@@ -422,14 +423,13 @@ impl Builder {
422423
let mut lookup = self.lookup;
423424
let mut insert_text = self.insert_text;
424425

425-
if let Some(original_path) = self
426-
.import_to_add
427-
.as_ref()
428-
.and_then(|import_edit| import_edit.import.original_path.as_ref())
429-
{
430-
lookup = lookup.or_else(|| Some(label.clone()));
431-
insert_text = insert_text.or_else(|| Some(label.clone()));
432-
format_to!(label, " (use {})", original_path)
426+
if let [import_edit] = &*self.imports_to_add {
427+
// snippets can have multiple imports, but normal completions only have up to one
428+
if let Some(original_path) = import_edit.import.original_path.as_ref() {
429+
lookup = lookup.or_else(|| Some(label.clone()));
430+
insert_text = insert_text.or_else(|| Some(label.clone()));
431+
format_to!(label, " (use {})", original_path)
432+
}
433433
} else if let Some(trait_name) = self.trait_name {
434434
insert_text = insert_text.or_else(|| Some(label.clone()));
435435
format_to!(label, " (as {})", trait_name)
@@ -456,7 +456,7 @@ impl Builder {
456456
trigger_call_info: self.trigger_call_info.unwrap_or(false),
457457
relevance: self.relevance,
458458
ref_match: self.ref_match,
459-
import_to_add: self.import_to_add,
459+
import_to_add: self.imports_to_add,
460460
}
461461
}
462462
pub(crate) fn lookup_by(&mut self, lookup: impl Into<String>) -> &mut Builder {
@@ -527,8 +527,8 @@ impl Builder {
527527
self.trigger_call_info = Some(true);
528528
self
529529
}
530-
pub(crate) fn add_import(&mut self, import_to_add: Option<ImportEdit>) -> &mut Builder {
531-
self.import_to_add = import_to_add;
530+
pub(crate) fn add_import(&mut self, import_to_add: ImportEdit) -> &mut Builder {
531+
self.imports_to_add.push(import_to_add);
532532
self
533533
}
534534
pub(crate) fn ref_match(&mut self, mutability: Mutability) -> &mut Builder {

0 commit comments

Comments
 (0)