mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 04:53:34 +00:00
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 <lukastw97@gmail.com>
This commit is contained in:
commit
c409cf0e26
23 changed files with 550 additions and 88 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -633,6 +633,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"profile",
|
||||
"rustc-hash",
|
||||
"smallvec",
|
||||
"sourcegen",
|
||||
"stdx",
|
||||
"syntax",
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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) {
|
||||
|
@ -442,6 +474,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(
|
||||
|
|
|
@ -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(()) }
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()])
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
176
crates/ide_completion/src/snippet.rs
Normal file
176
crates/ide_completion/src/snippet.rs
Normal file
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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() };
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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`)::
|
||||
+
|
||||
--
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue