diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index c825e4e9cb1..35b6a758d41 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -98,8 +98,8 @@ pub use ide_assists::{ Assist, AssistConfig, AssistId, AssistKind, AssistResolveStrategy, SingleResolve, }; pub use ide_completion::{ - CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit, - PostfixSnippet, PostfixSnippetScope, Snippet, SnippetScope, + CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit, Snippet, + SnippetScope, }; pub use ide_db::{ base_db::{ diff --git a/crates/ide_completion/src/completions/postfix.rs b/crates/ide_completion/src/completions/postfix.rs index a640b7d0738..4df6c36ebe7 100644 --- a/crates/ide_completion/src/completions/postfix.rs +++ b/crates/ide_completion/src/completions/postfix.rs @@ -56,7 +56,7 @@ pub(crate) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) { let postfix_snippet = build_postfix_snippet_builder(ctx, cap, &dot_receiver); - if !ctx.config.postfix_snippets.is_empty() { + if !ctx.config.snippets.is_empty() { add_custom_postfix_completions(acc, ctx, &postfix_snippet, &receiver_text); } @@ -230,21 +230,23 @@ fn add_custom_postfix_completions( ) -> Option<()> { let import_scope = ImportScope::find_insert_use_container_with_macros(&ctx.token.parent()?, &ctx.sema)?; - ctx.config.postfix_snippets.iter().for_each(|snippet| { - let imports = match snippet.imports(ctx, &import_scope) { - Some(imports) => imports, - None => return, - }; - let mut builder = postfix_snippet( - &snippet.label, - snippet.description.as_deref().unwrap_or_default(), - &format!("{}", snippet.snippet(&receiver_text)), - ); - for import in imports.into_iter() { - builder.add_import(import); - } - builder.add_to(acc); - }); + 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 mut builder = postfix_snippet( + trigger, + snippet.description.as_deref().unwrap_or_default(), + &snippet.postfix_snippet(&receiver_text), + ); + for import in imports.into_iter() { + builder.add_import(import); + } + builder.add_to(acc); + }, + ); None } @@ -254,7 +256,7 @@ mod tests { use crate::{ tests::{check_edit, check_edit_with_config, filtered_completion_list, TEST_CONFIG}, - CompletionConfig, CompletionKind, PostfixSnippet, + CompletionConfig, CompletionKind, Snippet, }; fn check(ra_fixture: &str, expect: Expect) { @@ -476,12 +478,13 @@ fn main() { fn custom_postfix_completion() { check_edit_with_config( CompletionConfig { - postfix_snippets: vec![PostfixSnippet::new( - "break".into(), - &["ControlFlow::Break($receiver)".into()], + snippets: vec![Snippet::new( &[], + &["break".into()], + &["ControlFlow::Break($receiver)".into()], + "", &["core::ops::ControlFlow".into()], - crate::PostfixSnippetScope::Expr, + crate::SnippetScope::Expr, ) .unwrap()], ..TEST_CONFIG diff --git a/crates/ide_completion/src/completions/snippet.rs b/crates/ide_completion/src/completions/snippet.rs index 3824bf8a9a0..ed6c4c74937 100644 --- a/crates/ide_completion/src/completions/snippet.rs +++ b/crates/ide_completion/src/completions/snippet.rs @@ -103,18 +103,20 @@ fn add_custom_completions( ) -> Option<()> { let import_scope = ImportScope::find_insert_use_container_with_macros(&ctx.token.parent()?, &ctx.sema)?; - ctx.config.snippets.iter().filter(|snip| snip.scope == scope).for_each(|snip| { - let imports = match snip.imports(ctx, &import_scope) { - Some(imports) => imports, - None => return, - }; - let mut builder = snippet(ctx, cap, &snip.label, &snip.snippet); - for import in imports.into_iter() { - builder.add_import(import); - } - builder.detail(snip.description.as_deref().unwrap_or_default()); - builder.add_to(acc); - }); + 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 mut builder = snippet(ctx, cap, &trigger, &snip.snippet()); + for import in imports.into_iter() { + builder.add_import(import); + } + builder.detail(snip.description.as_deref().unwrap_or_default()); + builder.add_to(acc); + }, + ); None } @@ -130,9 +132,10 @@ mod tests { check_edit_with_config( CompletionConfig { snippets: vec![Snippet::new( - "break".into(), - &["ControlFlow::Break(())".into()], + &["break".into()], &[], + &["ControlFlow::Break(())".into()], + "", &["core::ops::ControlFlow".into()], crate::SnippetScope::Expr, ) diff --git a/crates/ide_completion/src/config.rs b/crates/ide_completion/src/config.rs index bf1dc125c7e..c659b4455a9 100644 --- a/crates/ide_completion/src/config.rs +++ b/crates/ide_completion/src/config.rs @@ -6,7 +6,7 @@ use ide_db::helpers::{insert_use::InsertUseConfig, SnippetCap}; -use crate::snippet::{PostfixSnippet, Snippet}; +use crate::snippet::Snippet; #[derive(Clone, Debug, PartialEq, Eq)] pub struct CompletionConfig { @@ -17,6 +17,18 @@ pub struct CompletionConfig { pub add_call_argument_snippets: bool, pub snippet_cap: Option, pub insert_use: InsertUseConfig, - pub postfix_snippets: Vec, pub snippets: Vec, } + +impl CompletionConfig { + pub fn postfix_snippets(&self) -> impl Iterator { + self.snippets.iter().flat_map(|snip| { + snip.postfix_triggers.iter().map(move |trigger| (trigger.as_str(), snip)) + }) + } + pub fn prefix_snippets(&self) -> impl Iterator { + self.snippets.iter().flat_map(|snip| { + snip.prefix_triggers.iter().map(move |trigger| (trigger.as_str(), snip)) + }) + } +} diff --git a/crates/ide_completion/src/lib.rs b/crates/ide_completion/src/lib.rs index 647bbc8af54..251ddfa2fc0 100644 --- a/crates/ide_completion/src/lib.rs +++ b/crates/ide_completion/src/lib.rs @@ -29,7 +29,7 @@ use crate::{completions::Completions, context::CompletionContext, item::Completi pub use crate::{ config::CompletionConfig, item::{CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit}, - snippet::{PostfixSnippet, PostfixSnippetScope, Snippet, SnippetScope}, + snippet::{Snippet, SnippetScope}, }; //FIXME: split the following feature into fine-grained features. diff --git a/crates/ide_completion/src/snippet.rs b/crates/ide_completion/src/snippet.rs index b7dd1b3297e..d527f3aef6f 100644 --- a/crates/ide_completion/src/snippet.rs +++ b/crates/ide_completion/src/snippet.rs @@ -1,6 +1,57 @@ //! 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; @@ -8,50 +59,40 @@ use syntax::ast; use crate::{context::CompletionContext, ImportEdit}; #[derive(Clone, Debug, PartialEq, Eq)] -pub enum PostfixSnippetScope { +pub enum SnippetScope { + Item, Expr, Type, } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum SnippetScope { - Item, - Expr, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PostfixSnippet { - pub scope: PostfixSnippetScope, - pub label: String, +pub struct Snippet { + pub postfix_triggers: Box<[String]>, + pub prefix_triggers: Box<[String]>, + pub scope: SnippetScope, snippet: String, pub description: Option, pub requires: Box<[String]>, } -#[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub struct Snippet { - pub scope: SnippetScope, - pub label: String, - pub snippet: String, - pub description: Option, - pub requires: Box<[String]>, -} impl Snippet { pub fn new( - label: String, + prefix_triggers: &[String], + postfix_triggers: &[String], snippet: &[String], - description: &[String], + description: &str, requires: &[String], scope: SnippetScope, ) -> Option { 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, - label, snippet, description, - requires: requires.iter().cloned().collect(), // Box::into doesn't work as that has a Copy bound 😒 + requires: requires.iter().cloned().collect(), }) } @@ -64,6 +105,14 @@ impl Snippet { 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 } @@ -73,46 +122,6 @@ impl Snippet { } } -impl PostfixSnippet { - pub fn new( - label: String, - snippet: &[String], - description: &[String], - requires: &[String], - scope: PostfixSnippetScope, - ) -> Option { - let (snippet, description) = validate_snippet(snippet, description, requires)?; - Some(PostfixSnippet { - scope, - label, - snippet, - description, - requires: requires.iter().cloned().collect(), // Box::into doesn't work as that has a Copy bound 😒 - }) - } - - /// Returns None if the required items do not resolve. - pub(crate) fn imports( - &self, - ctx: &CompletionContext, - import_scope: &ImportScope, - ) -> Option> { - import_edits(ctx, import_scope, &self.requires) - } - - pub fn snippet(&self, receiver: &str) -> String { - self.snippet.replace("$receiver", receiver) - } - - pub fn is_item(&self) -> bool { - self.scope == PostfixSnippetScope::Type - } - - pub fn is_expr(&self) -> bool { - self.scope == PostfixSnippetScope::Expr - } -} - fn import_edits( ctx: &CompletionContext, import_scope: &ImportScope, @@ -147,7 +156,7 @@ fn import_edits( fn validate_snippet( snippet: &[String], - description: &[String], + description: &str, requires: &[String], ) -> Option<(String, Option)> { // validate that these are indeed simple paths @@ -162,7 +171,6 @@ fn validate_snippet( return None; } let snippet = snippet.iter().join("\n"); - let description = description.iter().join("\n"); - let description = if description.is_empty() { None } else { Some(description) }; + 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 6f4121dd80a..9168956235d 100644 --- a/crates/ide_completion/src/tests.rs +++ b/crates/ide_completion/src/tests.rs @@ -74,7 +74,6 @@ pub(crate) const TEST_CONFIG: CompletionConfig = CompletionConfig { group: true, skip_glob_imports: true, }, - postfix_snippets: Vec::new(), snippets: Vec::new(), }; diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 9fde9db8d66..a032c2b653a 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -12,8 +12,7 @@ use std::{ffi::OsString, iter, path::PathBuf}; use flycheck::FlycheckConfig; use ide::{ AssistConfig, CompletionConfig, DiagnosticsConfig, HighlightRelatedConfig, HoverConfig, - HoverDocFormat, InlayHintsConfig, JoinLinesConfig, PostfixSnippet, PostfixSnippetScope, - Snippet, SnippetScope, + HoverDocFormat, InlayHintsConfig, JoinLinesConfig, Snippet, SnippetScope, }; use ide_db::helpers::{ insert_use::{ImportGranularity, InsertUseConfig, PrefixKind}, @@ -117,8 +116,6 @@ config_data! { completion_snippets: FxHashMap = "{}", /// Whether to show postfix snippets like `dbg`, `if`, `not`, etc. completion_postfix_enable: bool = "true", - /// Custom postfix completion snippets. - completion_postfix_snippets: FxHashMap = "{}", /// Toggles the additional completions that automatically add imports when completed. /// Note that your client must specify the `additionalTextEdits` LSP client capability to truly have this feature enabled. completion_autoimport_enable: bool = "true", @@ -301,7 +298,6 @@ pub struct Config { detached_files: Vec, pub discovered_projects: Option>, pub root_path: AbsPathBuf, - postfix_snippets: Vec, snippets: Vec, } @@ -438,7 +434,6 @@ impl Config { detached_files: Vec::new(), discovered_projects: None, root_path, - postfix_snippets: Default::default(), snippets: Default::default(), } } @@ -452,40 +447,28 @@ impl Config { .map(AbsPathBuf::assert) .collect(); self.data = ConfigData::from_json(json); - self.postfix_snippets = self - .data - .completion_postfix_snippets - .iter() - .flat_map(|(label, desc)| { - PostfixSnippet::new( - label.clone(), - &desc.snippet, - &desc.description, - &desc.requires, - match desc.scope { - PostfixSnippetScopeDef::Expr => PostfixSnippetScope::Expr, - PostfixSnippetScopeDef::Type => PostfixSnippetScope::Type, - }, - ) - }) - .collect(); - self.snippets = self - .data - .completion_snippets - .iter() - .flat_map(|(label, desc)| { - Snippet::new( - label.clone(), - &desc.snippet, - &desc.description, - &desc.requires, - match desc.scope { - SnippetScopeDef::Expr => SnippetScope::Expr, - SnippetScopeDef::Item => SnippetScope::Item, - }, - ) - }) - .collect(); + 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 { @@ -821,7 +804,6 @@ impl Config { .snippet_support?, false )), - postfix_snippets: self.postfix_snippets.clone(), snippets: self.snippets.clone(), } } @@ -953,24 +935,12 @@ impl Config { } } -#[derive(Deserialize, Debug, Clone, Copy)] -#[serde(rename_all = "snake_case")] -enum PostfixSnippetScopeDef { - Expr, - Type, -} - -impl Default for PostfixSnippetScopeDef { - fn default() -> Self { - PostfixSnippetScopeDef::Expr - } -} - #[derive(Deserialize, Debug, Clone, Copy)] #[serde(rename_all = "snake_case")] enum SnippetScopeDef { Expr, Item, + Type, } impl Default for SnippetScopeDef { @@ -979,27 +949,18 @@ impl Default for SnippetScopeDef { } } -#[derive(Deserialize, Debug, Clone)] -struct PostfixSnippetDef { - #[serde(deserialize_with = "single_or_array")] - description: Vec, - #[serde(deserialize_with = "single_or_array")] - snippet: Vec, - #[serde(deserialize_with = "single_or_array")] - requires: Vec, - #[serde(default)] - scope: PostfixSnippetScopeDef, -} - -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, Default)] +#[serde(default)] struct SnippetDef { #[serde(deserialize_with = "single_or_array")] - description: Vec, + prefix: Vec, #[serde(deserialize_with = "single_or_array")] - snippet: Vec, + postfix: Vec, + description: Option, + #[serde(deserialize_with = "single_or_array")] + body: Vec, #[serde(deserialize_with = "single_or_array")] requires: Vec, - #[serde(default)] scope: SnippetScopeDef, } @@ -1203,9 +1164,6 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json "items": { "type": "string" }, "uniqueItems": true, }, - "FxHashMap" => set! { - "type": "object", - }, "FxHashMap" => set! { "type": "object", }, diff --git a/crates/rust-analyzer/src/integrated_benchmarks.rs b/crates/rust-analyzer/src/integrated_benchmarks.rs index 4b3c83c241b..b10eb3d6e92 100644 --- a/crates/rust-analyzer/src/integrated_benchmarks.rs +++ b/crates/rust-analyzer/src/integrated_benchmarks.rs @@ -144,7 +144,6 @@ fn integrated_completion_benchmark() { group: true, skip_glob_imports: true, }, - postfix_snippets: Vec::new(), snippets: Vec::new(), }; let position = @@ -182,7 +181,6 @@ fn integrated_completion_benchmark() { group: true, skip_glob_imports: true, }, - postfix_snippets: Vec::new(), snippets: Vec::new(), }; let position =