Unnamed repository; edit this file 'description' to name the repository.
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?` https://github.com/rust-analyzer/rust-analyzer/issues/7033 - `Add unsafe snippet` https://github.com/rust-analyzer/rust-analyzer/issues/10392, would allow users to have this without the diagnostic) - `.ok() postfix snippet is annoying` https://github.com/rust-analyzer/rust-analyzer/issues/9636, allows us to get rid of the `ok` postfix and similar ones - `Postfix vec completion` https://github.com/rust-analyzer/rust-analyzer/issues/7773 cc https://github.com/rust-analyzer/rust-analyzer/issues/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]>
bors[bot] 2021-10-05
parent 86c534f · parent 041cfbe · commit c409cf0
-rw-r--r--Cargo.lock1
-rw-r--r--crates/ide/src/lib.rs16
-rw-r--r--crates/ide_completion/Cargo.toml1
-rw-r--r--crates/ide_completion/src/completions/postfix.rs66
-rw-r--r--crates/ide_completion/src/completions/snippet.rs76
-rw-r--r--crates/ide_completion/src/config.rs16
-rw-r--r--crates/ide_completion/src/context.rs3
-rw-r--r--crates/ide_completion/src/item.rs32
-rw-r--r--crates/ide_completion/src/lib.rs50
-rw-r--r--crates/ide_completion/src/render.rs10
-rw-r--r--crates/ide_completion/src/render/enum_variant.rs5
-rw-r--r--crates/ide_completion/src/render/function.rs5
-rw-r--r--crates/ide_completion/src/render/macro_.rs5
-rw-r--r--crates/ide_completion/src/snippet.rs176
-rw-r--r--crates/ide_completion/src/tests.rs17
-rw-r--r--crates/rust-analyzer/src/config.rs112
-rw-r--r--crates/rust-analyzer/src/handlers.rs6
-rw-r--r--crates/rust-analyzer/src/integrated_benchmarks.rs2
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs5
-rw-r--r--crates/rust-analyzer/src/to_proto.rs22
-rw-r--r--docs/dev/lsp-extensions.md2
-rw-r--r--docs/user/generated_config.adoc5
-rw-r--r--editors/code/package.json5
23 files changed, 550 insertions, 88 deletions
diff --git a/Cargo.lock b/Cargo.lock
index d1058c02c7..9ad34f7160 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -633,6 +633,7 @@ dependencies = [
"once_cell",
"profile",
"rustc-hash",
+ "smallvec",
"sourcegen",
"stdx",
"syntax",
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 5c47227990..35b6a758d4 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -98,7 +98,8 @@ pub use ide_assists::{
Assist, AssistConfig, AssistId, AssistKind, AssistResolveStrategy, SingleResolve,
};
pub use ide_completion::{
- CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit,
+ CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit, Snippet,
+ SnippetScope,
};
pub use ide_db::{
base_db::{
@@ -532,19 +533,10 @@ impl Analysis {
&self,
config: &CompletionConfig,
position: FilePosition,
- full_import_path: &str,
- imported_name: String,
+ imports: impl IntoIterator<Item = (String, String)> + std::panic::UnwindSafe,
) -> Cancellable<Vec<TextEdit>> {
Ok(self
- .with_db(|db| {
- ide_completion::resolve_completion_edits(
- db,
- config,
- position,
- full_import_path,
- imported_name,
- )
- })?
+ .with_db(|db| ide_completion::resolve_completion_edits(db, config, position, imports))?
.unwrap_or_default())
}
diff --git a/crates/ide_completion/Cargo.toml b/crates/ide_completion/Cargo.toml
index 8d91001560..0d4413978d 100644
--- a/crates/ide_completion/Cargo.toml
+++ b/crates/ide_completion/Cargo.toml
@@ -14,6 +14,7 @@ itertools = "0.10.0"
rustc-hash = "1.1.0"
either = "1.6.1"
once_cell = "1.7"
+smallvec = "1.4"
stdx = { path = "../stdx", version = "0.0.0" }
syntax = { path = "../syntax", version = "0.0.0" }
diff --git a/crates/ide_completion/src/completions/postfix.rs b/crates/ide_completion/src/completions/postfix.rs
index 55c49c2322..b74030c173 100644
--- a/crates/ide_completion/src/completions/postfix.rs
+++ b/crates/ide_completion/src/completions/postfix.rs
@@ -2,8 +2,9 @@
mod format_like;
+use hir::Documentation;
use ide_db::{
- helpers::{FamousDefs, SnippetCap},
+ helpers::{insert_use::ImportScope, FamousDefs, SnippetCap},
ty_filter::TryEnum,
};
use syntax::{
@@ -56,6 +57,10 @@ pub(crate) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
let postfix_snippet = build_postfix_snippet_builder(ctx, cap, &dot_receiver);
+ if !ctx.config.snippets.is_empty() {
+ add_custom_postfix_completions(acc, ctx, &postfix_snippet, &receiver_text);
+ }
+
let try_enum = TryEnum::from_ty(&ctx.sema, &receiver_ty.strip_references());
if let Some(try_enum) = &try_enum {
match try_enum {
@@ -218,13 +223,40 @@ fn build_postfix_snippet_builder<'a>(
}
}
+fn add_custom_postfix_completions(
+ acc: &mut Completions,
+ ctx: &CompletionContext,
+ postfix_snippet: impl Fn(&str, &str, &str) -> Builder,
+ receiver_text: &str,
+) -> Option<()> {
+ let import_scope =
+ ImportScope::find_insert_use_container_with_macros(&ctx.token.parent()?, &ctx.sema)?;
+ ctx.config.postfix_snippets().filter(|(_, snip)| snip.is_expr()).for_each(
+ |(trigger, snippet)| {
+ let imports = match snippet.imports(ctx, &import_scope) {
+ Some(imports) => imports,
+ None => return,
+ };
+ let body = snippet.postfix_snippet(&receiver_text);
+ let mut builder =
+ postfix_snippet(trigger, snippet.description.as_deref().unwrap_or_default(), &body);
+ builder.documentation(Documentation::new(format!("```rust\n{}\n```", body)));
+ for import in imports.into_iter() {
+ builder.add_import(import);
+ }
+ builder.add_to(acc);
+ },
+ );
+ None
+}
+
#[cfg(test)]
mod tests {
use expect_test::{expect, Expect};
use crate::{
- tests::{check_edit, filtered_completion_list},
- CompletionKind,
+ tests::{check_edit, check_edit_with_config, filtered_completion_list, TEST_CONFIG},
+ CompletionConfig, CompletionKind, Snippet,
};
fn check(ra_fixture: &str, expect: Expect) {
@@ -443,6 +475,34 @@ fn main() {
}
#[test]
+ fn custom_postfix_completion() {
+ check_edit_with_config(
+ CompletionConfig {
+ snippets: vec![Snippet::new(
+ &[],
+ &["break".into()],
+ &["ControlFlow::Break(${receiver})".into()],
+ "",
+ &["core::ops::ControlFlow".into()],
+ crate::SnippetScope::Expr,
+ )
+ .unwrap()],
+ ..TEST_CONFIG
+ },
+ "break",
+ r#"
+//- minicore: try
+fn main() { 42.$0 }
+"#,
+ r#"
+use core::ops::ControlFlow;
+
+fn main() { ControlFlow::Break(42) }
+"#,
+ );
+ }
+
+ #[test]
fn postfix_completion_for_format_like_strings() {
check_edit(
"format",
diff --git a/crates/ide_completion/src/completions/snippet.rs b/crates/ide_completion/src/completions/snippet.rs
index a896a759ab..a0e5f56129 100644
--- a/crates/ide_completion/src/completions/snippet.rs
+++ b/crates/ide_completion/src/completions/snippet.rs
@@ -1,11 +1,12 @@
//! This file provides snippet completions, like `pd` => `eprintln!(...)`.
-use ide_db::helpers::SnippetCap;
+use hir::Documentation;
+use ide_db::helpers::{insert_use::ImportScope, SnippetCap};
use syntax::T;
use crate::{
context::PathCompletionContext, item::Builder, CompletionContext, CompletionItem,
- CompletionItemKind, CompletionKind, Completions,
+ CompletionItemKind, CompletionKind, Completions, SnippetScope,
};
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
None => return,
};
+ if !ctx.config.snippets.is_empty() {
+ add_custom_completions(acc, ctx, cap, SnippetScope::Expr);
+ }
+
if can_be_stmt {
snippet(ctx, cap, "pd", "eprintln!(\"$0 = {:?}\", $0);").add_to(acc);
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
None => return,
};
+ if !ctx.config.snippets.is_empty() {
+ add_custom_completions(acc, ctx, cap, SnippetScope::Item);
+ }
+
let mut item = snippet(
ctx,
cap,
@@ -86,3 +95,66 @@ fn ${1:feature}() {
let item = snippet(ctx, cap, "macro_rules", "macro_rules! $1 {\n\t($2) => {\n\t\t$0\n\t};\n}");
item.add_to(acc);
}
+
+fn add_custom_completions(
+ acc: &mut Completions,
+ ctx: &CompletionContext,
+ cap: SnippetCap,
+ scope: SnippetScope,
+) -> Option<()> {
+ let import_scope =
+ ImportScope::find_insert_use_container_with_macros(&ctx.token.parent()?, &ctx.sema)?;
+ ctx.config.prefix_snippets().filter(|(_, snip)| snip.scope == scope).for_each(
+ |(trigger, snip)| {
+ let imports = match snip.imports(ctx, &import_scope) {
+ Some(imports) => imports,
+ None => return,
+ };
+ let body = snip.snippet();
+ let mut builder = snippet(ctx, cap, &trigger, &body);
+ builder.documentation(Documentation::new(format!("```rust\n{}\n```", body)));
+ for import in imports.into_iter() {
+ builder.add_import(import);
+ }
+ builder.detail(snip.description.as_deref().unwrap_or_default());
+ builder.add_to(acc);
+ },
+ );
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{
+ tests::{check_edit_with_config, TEST_CONFIG},
+ CompletionConfig, Snippet,
+ };
+
+ #[test]
+ fn custom_snippet_completion() {
+ check_edit_with_config(
+ CompletionConfig {
+ snippets: vec![Snippet::new(
+ &["break".into()],
+ &[],
+ &["ControlFlow::Break(())".into()],
+ "",
+ &["core::ops::ControlFlow".into()],
+ crate::SnippetScope::Expr,
+ )
+ .unwrap()],
+ ..TEST_CONFIG
+ },
+ "break",
+ r#"
+//- minicore: try
+fn main() { $0 }
+"#,
+ r#"
+use core::ops::ControlFlow;
+
+fn main() { ControlFlow::Break(()) }
+"#,
+ );
+ }
+}
diff --git a/crates/ide_completion/src/config.rs b/crates/ide_completion/src/config.rs
index c300ce887b..c659b4455a 100644
--- a/crates/ide_completion/src/config.rs
+++ b/crates/ide_completion/src/config.rs
@@ -6,6 +6,8 @@
use ide_db::helpers::{insert_use::InsertUseConfig, SnippetCap};
+use crate::snippet::Snippet;
+
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CompletionConfig {
pub enable_postfix_completions: bool,
@@ -15,4 +17,18 @@ pub struct CompletionConfig {
pub add_call_argument_snippets: bool,
pub snippet_cap: Option<SnippetCap>,
pub insert_use: InsertUseConfig,
+ pub snippets: Vec<Snippet>,
+}
+
+impl CompletionConfig {
+ pub fn postfix_snippets(&self) -> impl Iterator<Item = (&str, &Snippet)> {
+ self.snippets.iter().flat_map(|snip| {
+ snip.postfix_triggers.iter().map(move |trigger| (trigger.as_str(), snip))
+ })
+ }
+ pub fn prefix_snippets(&self) -> impl Iterator<Item = (&str, &Snippet)> {
+ self.snippets.iter().flat_map(|snip| {
+ snip.prefix_triggers.iter().map(move |trigger| (trigger.as_str(), snip))
+ })
+ }
}
diff --git a/crates/ide_completion/src/context.rs b/crates/ide_completion/src/context.rs
index d6e15e6af7..a34e529ea5 100644
--- a/crates/ide_completion/src/context.rs
+++ b/crates/ide_completion/src/context.rs
@@ -868,7 +868,8 @@ mod tests {
fn check_expected_type_and_name(ra_fixture: &str, expect: Expect) {
let (db, pos) = position(ra_fixture);
- let completion_context = CompletionContext::new(&db, pos, &TEST_CONFIG).unwrap();
+ let config = TEST_CONFIG;
+ let completion_context = CompletionContext::new(&db, pos, &config).unwrap();
let ty = completion_context
.expected_type
diff --git a/crates/ide_completion/src/item.rs b/crates/ide_completion/src/item.rs
index 2bc69d5657..4c75bd6900 100644
--- a/crates/ide_completion/src/item.rs
+++ b/crates/ide_completion/src/item.rs
@@ -11,6 +11,7 @@ use ide_db::{
},
SymbolKind,
};
+use smallvec::SmallVec;
use stdx::{format_to, impl_from, never};
use syntax::{algo, TextRange};
use text_edit::TextEdit;
@@ -76,7 +77,7 @@ pub struct CompletionItem {
ref_match: Option<Mutability>,
/// The import data to add to completion's edits.
- import_to_add: Option<ImportEdit>,
+ import_to_add: SmallVec<[ImportEdit; 1]>,
}
// We use custom debug for CompletionItem to make snapshot tests more readable.
@@ -305,7 +306,7 @@ impl CompletionItem {
trigger_call_info: None,
relevance: CompletionRelevance::default(),
ref_match: None,
- import_to_add: None,
+ imports_to_add: Default::default(),
}
}
@@ -364,8 +365,8 @@ impl CompletionItem {
self.ref_match.map(|mutability| (mutability, relevance))
}
- pub fn import_to_add(&self) -> Option<&ImportEdit> {
- self.import_to_add.as_ref()
+ pub fn imports_to_add(&self) -> &[ImportEdit] {
+ &self.import_to_add
}
}
@@ -398,7 +399,7 @@ impl ImportEdit {
pub(crate) struct Builder {
source_range: TextRange,
completion_kind: CompletionKind,
- import_to_add: Option<ImportEdit>,
+ imports_to_add: SmallVec<[ImportEdit; 1]>,
trait_name: Option<String>,
label: String,
insert_text: Option<String>,
@@ -422,14 +423,13 @@ impl Builder {
let mut lookup = self.lookup;
let mut insert_text = self.insert_text;
- if let Some(original_path) = self
- .import_to_add
- .as_ref()
- .and_then(|import_edit| import_edit.import.original_path.as_ref())
- {
- lookup = lookup.or_else(|| Some(label.clone()));
- insert_text = insert_text.or_else(|| Some(label.clone()));
- format_to!(label, " (use {})", original_path)
+ if let [import_edit] = &*self.imports_to_add {
+ // snippets can have multiple imports, but normal completions only have up to one
+ if let Some(original_path) = import_edit.import.original_path.as_ref() {
+ lookup = lookup.or_else(|| Some(label.clone()));
+ insert_text = insert_text.or_else(|| Some(label.clone()));
+ format_to!(label, " (use {})", original_path)
+ }
} else if let Some(trait_name) = self.trait_name {
insert_text = insert_text.or_else(|| Some(label.clone()));
format_to!(label, " (as {})", trait_name)
@@ -456,7 +456,7 @@ impl Builder {
trigger_call_info: self.trigger_call_info.unwrap_or(false),
relevance: self.relevance,
ref_match: self.ref_match,
- import_to_add: self.import_to_add,
+ import_to_add: self.imports_to_add,
}
}
pub(crate) fn lookup_by(&mut self, lookup: impl Into<String>) -> &mut Builder {
@@ -527,8 +527,8 @@ impl Builder {
self.trigger_call_info = Some(true);
self
}
- pub(crate) fn add_import(&mut self, import_to_add: Option<ImportEdit>) -> &mut Builder {
- self.import_to_add = import_to_add;
+ pub(crate) fn add_import(&mut self, import_to_add: ImportEdit) -> &mut Builder {
+ self.imports_to_add.push(import_to_add);
self
}
pub(crate) fn ref_match(&mut self, mutability: Mutability) -> &mut Builder {
diff --git a/crates/ide_completion/src/lib.rs b/crates/ide_completion/src/lib.rs
index f10f3772b1..251ddfa2fc 100644
--- a/crates/ide_completion/src/lib.rs
+++ b/crates/ide_completion/src/lib.rs
@@ -9,16 +9,19 @@ mod render;
#[cfg(test)]
mod tests;
+mod snippet;
use completions::flyimport::position_for_import;
use ide_db::{
base_db::FilePosition,
helpers::{
- import_assets::{LocatedImport, NameToImport},
- insert_use::ImportScope,
+ import_assets::NameToImport,
+ insert_use::{self, ImportScope},
+ mod_path_to_ast,
},
items_locator, RootDatabase,
};
+use syntax::algo;
use text_edit::TextEdit;
use crate::{completions::Completions, context::CompletionContext, item::CompletionKind};
@@ -26,6 +29,7 @@ use crate::{completions::Completions, context::CompletionContext, item::Completi
pub use crate::{
config::CompletionConfig,
item::{CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit},
+ snippet::{Snippet, SnippetScope},
};
//FIXME: split the following feature into fine-grained features.
@@ -173,31 +177,37 @@ pub fn resolve_completion_edits(
db: &RootDatabase,
config: &CompletionConfig,
position: FilePosition,
- full_import_path: &str,
- imported_name: String,
+ imports: impl IntoIterator<Item = (String, String)>,
) -> Option<Vec<TextEdit>> {
+ let _p = profile::span("resolve_completion_edits");
let ctx = CompletionContext::new(db, position, config)?;
let position_for_import = position_for_import(&ctx, None)?;
let scope = ImportScope::find_insert_use_container_with_macros(position_for_import, &ctx.sema)?;
let current_module = ctx.sema.scope(position_for_import).module()?;
let current_crate = current_module.krate();
+ let new_ast = scope.clone_for_update();
+ let mut import_insert = TextEdit::builder();
- let (import_path, item_to_import) = items_locator::items_with_name(
- &ctx.sema,
- current_crate,
- NameToImport::Exact(imported_name),
- items_locator::AssocItemSearch::Include,
- Some(items_locator::DEFAULT_QUERY_SEARCH_LIMIT.inner()),
- )
- .filter_map(|candidate| {
- current_module
- .find_use_path_prefixed(db, candidate, config.insert_use.prefix_kind)
- .zip(Some(candidate))
- })
- .find(|(mod_path, _)| mod_path.to_string() == full_import_path)?;
- let import =
- LocatedImport::new(import_path.clone(), item_to_import, item_to_import, Some(import_path));
+ // FIXME: lift out and make some tests here, this is ImportEdit::to_text_edit but changed to work with multiple edits
+ imports.into_iter().for_each(|(full_import_path, imported_name)| {
+ let items_with_name = items_locator::items_with_name(
+ &ctx.sema,
+ current_crate,
+ NameToImport::Exact(imported_name),
+ items_locator::AssocItemSearch::Include,
+ Some(items_locator::DEFAULT_QUERY_SEARCH_LIMIT.inner()),
+ );
+ let import = items_with_name
+ .filter_map(|candidate| {
+ current_module.find_use_path_prefixed(db, candidate, config.insert_use.prefix_kind)
+ })
+ .find(|mod_path| mod_path.to_string() == full_import_path);
+ if let Some(import_path) = import {
+ insert_use::insert_use(&new_ast, mod_path_to_ast(&import_path), &config.insert_use);
+ }
+ });
- ImportEdit { import, scope }.to_text_edit(config.insert_use).map(|edit| vec![edit])
+ algo::diff(scope.as_syntax_node(), new_ast.as_syntax_node()).into_text_edit(&mut import_insert);
+ Some(vec![import_insert.finish()])
}
diff --git a/crates/ide_completion/src/render.rs b/crates/ide_completion/src/render.rs
index 62a5fac534..58443f566e 100644
--- a/crates/ide_completion/src/render.rs
+++ b/crates/ide_completion/src/render.rs
@@ -212,7 +212,10 @@ fn render_resolution_(
ctx.source_range(),
local_name.to_string(),
);
- item.kind(CompletionItemKind::UnresolvedReference).add_import(import_to_add);
+ item.kind(CompletionItemKind::UnresolvedReference);
+ if let Some(import_to_add) = import_to_add {
+ item.add_import(import_to_add);
+ }
return Some(item.build());
}
};
@@ -258,9 +261,12 @@ fn render_resolution_(
}
}
item.kind(kind)
- .add_import(import_to_add)
.set_documentation(scope_def_docs(ctx.db(), resolution))
.set_deprecated(scope_def_is_deprecated(&ctx, resolution));
+
+ if let Some(import_to_add) = import_to_add {
+ item.add_import(import_to_add);
+ }
Some(item.build())
}
diff --git a/crates/ide_completion/src/render/enum_variant.rs b/crates/ide_completion/src/render/enum_variant.rs
index d5cfd8bba4..2ba86eaa0a 100644
--- a/crates/ide_completion/src/render/enum_variant.rs
+++ b/crates/ide_completion/src/render/enum_variant.rs
@@ -68,9 +68,12 @@ impl<'a> EnumRender<'a> {
item.kind(SymbolKind::Variant)
.set_documentation(self.variant.docs(self.ctx.db()))
.set_deprecated(self.ctx.is_deprecated(self.variant))
- .add_import(import_to_add)
.detail(self.detail());
+ if let Some(import_to_add) = import_to_add {
+ item.add_import(import_to_add);
+ }
+
if self.variant_kind == hir::StructKind::Tuple {
cov_mark::hit!(inserts_parens_for_tuple_enums);
let params = Params::Anonymous(self.variant.fields(self.ctx.db()).len());
diff --git a/crates/ide_completion/src/render/function.rs b/crates/ide_completion/src/render/function.rs
index 904624f9ab..cc95bd5369 100644
--- a/crates/ide_completion/src/render/function.rs
+++ b/crates/ide_completion/src/render/function.rs
@@ -97,7 +97,10 @@ impl<'a> FunctionRender<'a> {
}
}
- item.add_import(import_to_add).lookup_by(self.name);
+ if let Some(import_to_add) = import_to_add {
+ item.add_import(import_to_add);
+ }
+ item.lookup_by(self.name);
let ret_type = self.func.ret_type(self.ctx.db());
item.set_relevance(CompletionRelevance {
diff --git a/crates/ide_completion/src/render/macro_.rs b/crates/ide_completion/src/render/macro_.rs
index d1b549df1b..196b667baa 100644
--- a/crates/ide_completion/src/render/macro_.rs
+++ b/crates/ide_completion/src/render/macro_.rs
@@ -51,9 +51,12 @@ impl<'a> MacroRender<'a> {
item.kind(SymbolKind::Macro)
.set_documentation(self.docs.clone())
.set_deprecated(self.ctx.is_deprecated(self.macro_))
- .add_import(import_to_add)
.set_detail(self.detail());
+ if let Some(import_to_add) = import_to_add {
+ item.add_import(import_to_add);
+ }
+
let needs_bang = !(self.ctx.completion.in_use_tree()
|| matches!(self.ctx.completion.path_call_kind(), Some(CallKind::Mac)));
let has_parens = self.ctx.completion.path_call_kind().is_some();
diff --git a/crates/ide_completion/src/snippet.rs b/crates/ide_completion/src/snippet.rs
new file mode 100644
index 0000000000..d527f3aef6
--- /dev/null
+++ b/crates/ide_completion/src/snippet.rs
@@ -0,0 +1,176 @@
+//! User (postfix)-snippet definitions.
+//!
+//! Actual logic is implemented in [`crate::completions::postfix`] and [`crate::completions::snippet`].
+
+// Feature: User Snippet Completions
+//
+// 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.
+//
+// A custom snippet can be defined by adding it to the `rust-analyzer.completion.snippets` object respectively.
+//
+// [source,json]
+// ----
+// {
+// "rust-analyzer.completion.snippets": {
+// "thread spawn": {
+// "prefix": ["spawn", "tspawn"],
+// "body": [
+// "thread::spawn(move || {",
+// "\t$0",
+// ")};",
+// ],
+// "description": "Insert a thread::spawn call",
+// "requires": "std::thread",
+// "scope": "expr",
+// }
+// }
+// }
+// ----
+//
+// In the example above:
+//
+// * `"thread spawn"` is the name of the snippet.
+//
+// * `prefix` defines one or more trigger words that will trigger the snippets completion.
+// Using `postfix` will instead create a postfix snippet.
+//
+// * `body` is one or more lines of content joined via newlines for the final output.
+//
+// * `description` is an optional description of the snippet, if unset the snippet name will be used.
+//
+// * `requires` is an optional list of item paths that have to be resolvable in the current crate where the completion is rendered.
+// On failure of resolution the snippet won't be applicable, otherwise the snippet will insert an import for the items on insertion if
+// the items aren't yet in scope.
+//
+// * `scope` is an optional filter for when the snippet should be applicable. Possible values are:
+// ** for Snippet-Scopes: `expr`, `item` (default: `item`)
+// ** for Postfix-Snippet-Scopes: `expr`, `type` (default: `expr`)
+//
+// The `body` field also has access to placeholders as visible in the example as `$0`.
+// These placeholders take the form of `$number` or `${number:placeholder_text}` which can be traversed as tabstop in ascending order starting from 1,
+// with `$0` being a special case that always comes last.
+//
+// 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.
+// It does not act as a tabstop.
+use ide_db::helpers::{import_assets::LocatedImport, insert_use::ImportScope};
+use itertools::Itertools;
+use syntax::ast;
+
+use crate::{context::CompletionContext, ImportEdit};
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum SnippetScope {
+ Item,
+ Expr,
+ Type,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Snippet {
+ pub postfix_triggers: Box<[String]>,
+ pub prefix_triggers: Box<[String]>,
+ pub scope: SnippetScope,
+ snippet: String,
+ pub description: Option<String>,
+ pub requires: Box<[String]>,
+}
+
+impl Snippet {
+ pub fn new(
+ prefix_triggers: &[String],
+ postfix_triggers: &[String],
+ snippet: &[String],
+ description: &str,
+ requires: &[String],
+ scope: SnippetScope,
+ ) -> Option<Self> {
+ let (snippet, description) = validate_snippet(snippet, description, requires)?;
+ Some(Snippet {
+ // Box::into doesn't work as that has a Copy bound 😒
+ postfix_triggers: postfix_triggers.iter().cloned().collect(),
+ prefix_triggers: prefix_triggers.iter().cloned().collect(),
+ scope,
+ snippet,
+ description,
+ requires: requires.iter().cloned().collect(),
+ })
+ }
+
+ /// Returns None if the required items do not resolve.
+ pub(crate) fn imports(
+ &self,
+ ctx: &CompletionContext,
+ import_scope: &ImportScope,
+ ) -> Option<Vec<ImportEdit>> {
+ import_edits(ctx, import_scope, &self.requires)
+ }
+
+ pub fn snippet(&self) -> String {
+ self.snippet.replace("${receiver}", "")
+ }
+
+ pub fn postfix_snippet(&self, receiver: &str) -> String {
+ self.snippet.replace("${receiver}", receiver)
+ }
+
+ pub fn is_item(&self) -> bool {
+ self.scope == SnippetScope::Item
+ }
+
+ pub fn is_expr(&self) -> bool {
+ self.scope == SnippetScope::Expr
+ }
+}
+
+fn import_edits(
+ ctx: &CompletionContext,
+ import_scope: &ImportScope,
+ requires: &[String],
+) -> Option<Vec<ImportEdit>> {
+ let resolve = |import| {
+ let path = ast::Path::parse(import).ok()?;
+ let item = match ctx.scope.speculative_resolve(&path)? {
+ hir::PathResolution::Macro(mac) => mac.into(),
+ hir::PathResolution::Def(def) => def.into(),
+ _ => return None,
+ };
+ let path = ctx.scope.module()?.find_use_path_prefixed(
+ ctx.db,
+ item,
+ ctx.config.insert_use.prefix_kind,
+ )?;
+ Some((path.len() > 1).then(|| ImportEdit {
+ import: LocatedImport::new(path.clone(), item, item, None),
+ scope: import_scope.clone(),
+ }))
+ };
+ let mut res = Vec::with_capacity(requires.len());
+ for import in requires {
+ match resolve(import) {
+ Some(first) => res.extend(first),
+ None => return None,
+ }
+ }
+ Some(res)
+}
+
+fn validate_snippet(
+ snippet: &[String],
+ description: &str,
+ requires: &[String],
+) -> Option<(String, Option<String>)> {
+ // validate that these are indeed simple paths
+ // we can't save the paths unfortunately due to them not being Send+Sync
+ if requires.iter().any(|path| match ast::Path::parse(path) {
+ Ok(path) => path.segments().any(|seg| {
+ !matches!(seg.kind(), Some(ast::PathSegmentKind::Name(_)))
+ || seg.generic_arg_list().is_some()
+ }),
+ Err(_) => true,
+ }) {
+ return None;
+ }
+ let snippet = snippet.iter().join("\n");
+ let description = if description.is_empty() { None } else { Some(description.to_owned()) };
+ Some((snippet, description))
+}
diff --git a/crates/ide_completion/src/tests.rs b/crates/ide_completion/src/tests.rs
index 6872e3b8dc..9168956235 100644
--- a/crates/ide_completion/src/tests.rs
+++ b/crates/ide_completion/src/tests.rs
@@ -74,6 +74,7 @@ pub(crate) const TEST_CONFIG: CompletionConfig = CompletionConfig {
group: true,
skip_glob_imports: true,
},
+ snippets: Vec::new(),
};
pub(crate) fn completion_list(ra_fixture: &str) -> String {
@@ -181,13 +182,15 @@ pub(crate) fn check_edit_with_config(
let mut actual = db.file_text(position.file_id).to_string();
let mut combined_edit = completion.text_edit().to_owned();
- if let Some(import_text_edit) =
- completion.import_to_add().and_then(|edit| edit.to_text_edit(config.insert_use))
- {
- combined_edit.union(import_text_edit).expect(
- "Failed to apply completion resolve changes: change ranges overlap, but should not",
- )
- }
+ completion
+ .imports_to_add()
+ .iter()
+ .filter_map(|edit| edit.to_text_edit(config.insert_use))
+ .for_each(|text_edit| {
+ combined_edit.union(text_edit).expect(
+ "Failed to apply completion resolve changes: change ranges overlap, but should not",
+ )
+ });
combined_edit.apply(&mut actual);
assert_eq_text!(&ra_fixture_after, &actual)
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index 6c09833368..a032c2b653 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -12,7 +12,7 @@ use std::{ffi::OsString, iter, path::PathBuf};
use flycheck::FlycheckConfig;
use ide::{
AssistConfig, CompletionConfig, DiagnosticsConfig, HighlightRelatedConfig, HoverConfig,
- HoverDocFormat, InlayHintsConfig, JoinLinesConfig,
+ HoverDocFormat, InlayHintsConfig, JoinLinesConfig, Snippet, SnippetScope,
};
use ide_db::helpers::{
insert_use::{ImportGranularity, InsertUseConfig, PrefixKind},
@@ -112,6 +112,8 @@ config_data! {
completion_addCallArgumentSnippets: bool = "true",
/// Whether to add parenthesis when completing functions.
completion_addCallParenthesis: bool = "true",
+ /// Custom completion snippets.
+ completion_snippets: FxHashMap<String, SnippetDef> = "{}",
/// Whether to show postfix snippets like `dbg`, `if`, `not`, etc.
completion_postfix_enable: bool = "true",
/// Toggles the additional completions that automatically add imports when completed.
@@ -277,9 +279,9 @@ config_data! {
rustfmt_enableRangeFormatting: bool = "false",
/// Workspace symbol search scope.
- workspace_symbol_search_scope: WorskpaceSymbolSearchScopeDef = "\"workspace\"",
+ workspace_symbol_search_scope: WorkspaceSymbolSearchScopeDef = "\"workspace\"",
/// Workspace symbol search kind.
- workspace_symbol_search_kind: WorskpaceSymbolSearchKindDef = "\"only_types\"",
+ workspace_symbol_search_kind: WorkspaceSymbolSearchKindDef = "\"only_types\"",
}
}
@@ -296,6 +298,7 @@ pub struct Config {
detached_files: Vec<AbsPathBuf>,
pub discovered_projects: Option<Vec<ProjectManifest>>,
pub root_path: AbsPathBuf,
+ snippets: Vec<Snippet>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
@@ -431,6 +434,7 @@ impl Config {
detached_files: Vec::new(),
discovered_projects: None,
root_path,
+ snippets: Default::default(),
}
}
pub fn update(&mut self, mut json: serde_json::Value) {
@@ -443,6 +447,28 @@ impl Config {
.map(AbsPathBuf::assert)
.collect();
self.data = ConfigData::from_json(json);
+ self.snippets.clear();
+ for (name, def) in self.data.completion_snippets.iter() {
+ if def.prefix.is_empty() && def.postfix.is_empty() {
+ continue;
+ }
+ let scope = match def.scope {
+ SnippetScopeDef::Expr => SnippetScope::Expr,
+ SnippetScopeDef::Type => SnippetScope::Type,
+ SnippetScopeDef::Item => SnippetScope::Item,
+ };
+ match Snippet::new(
+ &def.prefix,
+ &def.postfix,
+ &def.body,
+ def.description.as_ref().unwrap_or(name),
+ &def.requires,
+ scope,
+ ) {
+ Some(snippet) => self.snippets.push(snippet),
+ None => tracing::info!("Invalid snippet {}", name),
+ }
+ }
}
pub fn json_schema() -> serde_json::Value {
@@ -778,6 +804,7 @@ impl Config {
.snippet_support?,
false
)),
+ snippets: self.snippets.clone(),
}
}
pub fn assist(&self) -> AssistConfig {
@@ -848,14 +875,14 @@ impl Config {
pub fn workspace_symbol(&self) -> WorkspaceSymbolConfig {
WorkspaceSymbolConfig {
search_scope: match self.data.workspace_symbol_search_scope {
- WorskpaceSymbolSearchScopeDef::Workspace => WorkspaceSymbolSearchScope::Workspace,
- WorskpaceSymbolSearchScopeDef::WorkspaceAndDependencies => {
+ WorkspaceSymbolSearchScopeDef::Workspace => WorkspaceSymbolSearchScope::Workspace,
+ WorkspaceSymbolSearchScopeDef::WorkspaceAndDependencies => {
WorkspaceSymbolSearchScope::WorkspaceAndDependencies
}
},
search_kind: match self.data.workspace_symbol_search_kind {
- WorskpaceSymbolSearchKindDef::OnlyTypes => WorkspaceSymbolSearchKind::OnlyTypes,
- WorskpaceSymbolSearchKindDef::AllSymbols => WorkspaceSymbolSearchKind::AllSymbols,
+ WorkspaceSymbolSearchKindDef::OnlyTypes => WorkspaceSymbolSearchKind::OnlyTypes,
+ WorkspaceSymbolSearchKindDef::AllSymbols => WorkspaceSymbolSearchKind::AllSymbols,
},
}
}
@@ -908,6 +935,66 @@ impl Config {
}
}
+#[derive(Deserialize, Debug, Clone, Copy)]
+#[serde(rename_all = "snake_case")]
+enum SnippetScopeDef {
+ Expr,
+ Item,
+ Type,
+}
+
+impl Default for SnippetScopeDef {
+ fn default() -> Self {
+ SnippetScopeDef::Expr
+ }
+}
+
+#[derive(Deserialize, Debug, Clone, Default)]
+#[serde(default)]
+struct SnippetDef {
+ #[serde(deserialize_with = "single_or_array")]
+ prefix: Vec<String>,
+ #[serde(deserialize_with = "single_or_array")]
+ postfix: Vec<String>,
+ description: Option<String>,
+ #[serde(deserialize_with = "single_or_array")]
+ body: Vec<String>,
+ #[serde(deserialize_with = "single_or_array")]
+ requires: Vec<String>,
+ scope: SnippetScopeDef,
+}
+
+fn single_or_array<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ struct SingleOrVec;
+
+ impl<'de> serde::de::Visitor<'de> for SingleOrVec {
+ type Value = Vec<String>;
+
+ fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+ formatter.write_str("string or array of strings")
+ }
+
+ fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ Ok(vec![value.to_owned()])
+ }
+
+ fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
+ where
+ A: serde::de::SeqAccess<'de>,
+ {
+ Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))
+ }
+ }
+
+ deserializer.deserialize_any(SingleOrVec)
+}
+
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
enum ManifestOrProjectJson {
@@ -939,14 +1026,14 @@ enum ImportPrefixDef {
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
-enum WorskpaceSymbolSearchScopeDef {
+enum WorkspaceSymbolSearchScopeDef {
Workspace,
WorkspaceAndDependencies,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
-enum WorskpaceSymbolSearchKindDef {
+enum WorkspaceSymbolSearchKindDef {
OnlyTypes,
AllSymbols,
}
@@ -1077,6 +1164,9 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json
"items": { "type": "string" },
"uniqueItems": true,
},
+ "FxHashMap<String, SnippetDef>" => set! {
+ "type": "object",
+ },
"FxHashMap<String, String>" => set! {
"type": "object",
},
@@ -1133,7 +1223,7 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json
"type": "array",
"items": { "type": ["string", "object"] },
},
- "WorskpaceSymbolSearchScopeDef" => set! {
+ "WorkspaceSymbolSearchScopeDef" => set! {
"type": "string",
"enum": ["workspace", "workspace_and_dependencies"],
"enumDescriptions": [
@@ -1141,7 +1231,7 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json
"Search in current workspace and dependencies"
],
},
- "WorskpaceSymbolSearchKindDef" => set! {
+ "WorkspaceSymbolSearchKindDef" => set! {
"type": "string",
"enum": ["only_types", "all_symbols"],
"enumDescriptions": [
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index 6cb6b0a8d0..ca286b7de9 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -785,8 +785,10 @@ pub(crate) fn handle_completion_resolve(
.resolve_completion_edits(
&snap.config.completion(),
FilePosition { file_id, offset },
- &resolve_data.full_import_path,
- resolve_data.imported_name,
+ resolve_data
+ .imports
+ .into_iter()
+ .map(|import| (import.full_import_path, import.imported_name)),
)?
.into_iter()
.flat_map(|edit| edit.into_iter().map(|indel| to_proto::text_edit(&line_index, indel)))
diff --git a/crates/rust-analyzer/src/integrated_benchmarks.rs b/crates/rust-analyzer/src/integrated_benchmarks.rs
index 036cfe157b..b10eb3d6e9 100644
--- a/crates/rust-analyzer/src/integrated_benchmarks.rs
+++ b/crates/rust-analyzer/src/integrated_benchmarks.rs
@@ -144,6 +144,7 @@ fn integrated_completion_benchmark() {
group: true,
skip_glob_imports: true,
},
+ snippets: Vec::new(),
};
let position =
FilePosition { file_id, offset: TextSize::try_from(completion_offset).unwrap() };
@@ -180,6 +181,7 @@ fn integrated_completion_benchmark() {
group: true,
skip_glob_imports: true,
},
+ snippets: Vec::new(),
};
let position =
FilePosition { file_id, offset: TextSize::try_from(completion_offset).unwrap() };
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 521691d5ec..19137b942e 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -520,6 +520,11 @@ pub enum WorkspaceSymbolSearchKind {
#[derive(Debug, Serialize, Deserialize)]
pub struct CompletionResolveData {
pub position: lsp_types::TextDocumentPositionParams,
+ pub imports: Vec<CompletionImport>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CompletionImport {
pub full_import_path: String,
pub imported_name: String,
}
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index 59a768397f..fc3e25064f 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -270,14 +270,20 @@ fn completion_item(
lsp_item.insert_text_format = Some(lsp_types::InsertTextFormat::Snippet);
}
if config.completion().enable_imports_on_the_fly {
- if let Some(import_edit) = item.import_to_add() {
- let import_path = &import_edit.import.import_path;
- if let Some(import_name) = import_path.segments().last() {
- let data = lsp_ext::CompletionResolveData {
- position: tdpp.clone(),
- full_import_path: import_path.to_string(),
- imported_name: import_name.to_string(),
- };
+ if let imports @ [_, ..] = item.imports_to_add() {
+ let imports: Vec<_> = imports
+ .iter()
+ .filter_map(|import_edit| {
+ let import_path = &import_edit.import.import_path;
+ let import_name = import_path.segments().last()?;
+ Some(lsp_ext::CompletionImport {
+ full_import_path: import_path.to_string(),
+ imported_name: import_name.to_string(),
+ })
+ })
+ .collect();
+ if !imports.is_empty() {
+ let data = lsp_ext::CompletionResolveData { position: tdpp.clone(), imports };
lsp_item.data = Some(to_value(data).unwrap());
}
}
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index a6a1c73111..9076b93d35 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -1,5 +1,5 @@
<!---
-lsp_ext.rs hash: ad52054176909945
+lsp_ext.rs hash: c6568e4035333f3a
If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue:
diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc
index 768216fed9..f80b978545 100644
--- a/docs/user/generated_config.adoc
+++ b/docs/user/generated_config.adoc
@@ -136,6 +136,11 @@ Only applies when `#rust-analyzer.completion.addCallParenthesis#` is set.
--
Whether to add parenthesis when completing functions.
--
+[[rust-analyzer.completion.snippets]]rust-analyzer.completion.snippets (default: `{}`)::
++
+--
+Custom completion snippets.
+--
[[rust-analyzer.completion.postfix.enable]]rust-analyzer.completion.postfix.enable (default: `true`)::
+
--
diff --git a/editors/code/package.json b/editors/code/package.json
index 4ca93b17bc..d9afdfad1b 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -585,6 +585,11 @@
"default": true,
"type": "boolean"
},
+ "rust-analyzer.completion.snippets": {
+ "markdownDescription": "Custom completion snippets.",
+ "default": {},
+ "type": "object"
+ },
"rust-analyzer.completion.postfix.enable": {
"markdownDescription": "Whether to show postfix snippets like `dbg`, `if`, `not`, etc.",
"default": true,