From 046c85ef0c56d9c484291b22241a51fa7d2f3a51 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 4 Oct 2021 19:22:41 +0200 Subject: [PATCH] Add custom non-postfix snippets --- crates/ide/src/lib.rs | 2 +- .../ide_completion/src/completions/postfix.rs | 37 +--- .../ide_completion/src/completions/snippet.rs | 68 ++++++- crates/ide_completion/src/config.rs | 46 +---- crates/ide_completion/src/lib.rs | 4 +- crates/ide_completion/src/snippet.rs | 170 ++++++++++++++++++ crates/ide_completion/src/tests.rs | 1 + crates/rust-analyzer/src/config.rs | 68 ++++++- .../src/integrated_benchmarks.rs | 2 + 9 files changed, 315 insertions(+), 83 deletions(-) create mode 100644 crates/ide_completion/src/snippet.rs diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 4b35bfeac6..2c7e1837c3 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -99,7 +99,7 @@ pub use ide_assists::{ }; pub use ide_completion::{ CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit, - PostfixSnippet, + PostfixSnippet, PostfixSnippetScope, 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 ecddf59094..f83001c22d 100644 --- a/crates/ide_completion/src/completions/postfix.rs +++ b/crates/ide_completion/src/completions/postfix.rs @@ -3,7 +3,7 @@ mod format_like; use ide_db::{ - helpers::{import_assets::LocatedImport, insert_use::ImportScope, FamousDefs, SnippetCap}, + helpers::{insert_use::ImportScope, FamousDefs, SnippetCap}, ty_filter::TryEnum, }; use syntax::{ @@ -18,7 +18,7 @@ use crate::{ context::CompletionContext, item::{Builder, CompletionKind}, patterns::ImmediateLocation, - CompletionItem, CompletionItemKind, CompletionRelevance, Completions, ImportEdit, + CompletionItem, CompletionItemKind, CompletionRelevance, Completions, }; pub(crate) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) { @@ -232,33 +232,9 @@ fn add_custom_postfix_completions( ImportScope::find_insert_use_container_with_macros(&ctx.token.parent()?, &ctx.sema)?; ctx.config.postfix_snippets.iter().for_each(|snippet| { // FIXME: Support multiple imports - let import = match snippet.requires.get(0) { - Some(import) => { - let res = (|| { - let path = ast::Path::parse(import).ok()?; - match ctx.scope.speculative_resolve(&path)? { - hir::PathResolution::Macro(_) => None, - hir::PathResolution::Def(def) => { - let item = def.into(); - 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(), - })) - } - _ => None, - } - })(); - match res { - Some(it) => it, - None => return, - } - } - None => None, + let import = match snippet.imports(ctx, &import_scope) { + Ok(mut imports) => imports.pop(), + Err(_) => return, }; let mut builder = postfix_snippet( &snippet.label, @@ -501,9 +477,10 @@ fn main() { CompletionConfig { postfix_snippets: vec![PostfixSnippet::new( "break".into(), - &["ControlFlow::Break($target)".into()], + &["ControlFlow::Break($receiver)".into()], &[], &["core::ops::ControlFlow".into()], + None, ) .unwrap()], ..TEST_CONFIG diff --git a/crates/ide_completion/src/completions/snippet.rs b/crates/ide_completion/src/completions/snippet.rs index a896a759ab..9812f25b40 100644 --- a/crates/ide_completion/src/completions/snippet.rs +++ b/crates/ide_completion/src/completions/snippet.rs @@ -1,11 +1,11 @@ //! This file provides snippet completions, like `pd` => `eprintln!(...)`. -use ide_db::helpers::SnippetCap; +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 +29,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 +56,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 +94,59 @@ 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.snippets.iter().filter(|snip| snip.scope == scope).for_each(|snip| { + // FIXME: Support multiple imports + let import = match snip.imports(ctx, &import_scope) { + Ok(mut imports) => imports.pop(), + Err(_) => return, + }; + let mut builder = snippet(ctx, cap, &snip.label, &snip.snippet); + builder.add_import(import).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()], + None, + ) + .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 b28b59b0a5..bf1dc125c7 100644 --- a/crates/ide_completion/src/config.rs +++ b/crates/ide_completion/src/config.rs @@ -5,8 +5,8 @@ //! completions if we are allowed to. use ide_db::helpers::{insert_use::InsertUseConfig, SnippetCap}; -use itertools::Itertools; -use syntax::ast; + +use crate::snippet::{PostfixSnippet, Snippet}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct CompletionConfig { @@ -18,45 +18,5 @@ pub struct CompletionConfig { pub snippet_cap: Option, pub insert_use: InsertUseConfig, pub postfix_snippets: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PostfixSnippet { - pub label: String, - snippet: String, - pub description: Option, - pub requires: Box<[String]>, -} - -impl PostfixSnippet { - pub fn new( - label: String, - snippet: &[String], - description: &[String], - requires: &[String], - ) -> Option { - // validate that these are indeed simple paths - 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 = description.iter().join("\n"); - let description = if description.is_empty() { None } else { Some(description) }; - Some(PostfixSnippet { - label, - snippet, - description, - requires: requires.iter().cloned().collect(), // Box::into doesn't work as that has a Copy bound 😒 - }) - } - - pub fn snippet(&self, receiver: &str) -> String { - self.snippet.replace("$receiver", receiver) - } + pub snippets: Vec, } diff --git a/crates/ide_completion/src/lib.rs b/crates/ide_completion/src/lib.rs index 62570b7386..56464c07ce 100644 --- a/crates/ide_completion/src/lib.rs +++ b/crates/ide_completion/src/lib.rs @@ -9,6 +9,7 @@ mod render; #[cfg(test)] mod tests; +mod snippet; use completions::flyimport::position_for_import; use ide_db::{ @@ -24,8 +25,9 @@ use text_edit::TextEdit; use crate::{completions::Completions, context::CompletionContext, item::CompletionKind}; pub use crate::{ - config::{CompletionConfig, PostfixSnippet}, + config::CompletionConfig, item::{CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit}, + snippet::{PostfixSnippet, PostfixSnippetScope, 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 new file mode 100644 index 0000000000..1bcb128fa9 --- /dev/null +++ b/crates/ide_completion/src/snippet.rs @@ -0,0 +1,170 @@ +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 PostfixSnippetScope { + 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, + 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, + snippet: &[String], + description: &[String], + requires: &[String], + scope: Option, + ) -> Option { + // validate that these are indeed simple paths + 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 = description.iter().join("\n"); + let description = if description.is_empty() { None } else { Some(description) }; + Some(Snippet { + scope: scope.unwrap_or(SnippetScope::Expr), + label, + snippet, + description, + requires: requires.iter().cloned().collect(), // Box::into doesn't work as that has a Copy bound 😒 + }) + } + + // FIXME: This shouldn't be fallible + pub(crate) fn imports( + &self, + ctx: &CompletionContext, + import_scope: &ImportScope, + ) -> Result, ()> { + import_edits(ctx, import_scope, &self.requires) + } + + pub fn is_item(&self) -> bool { + self.scope == SnippetScope::Item + } + + pub fn is_expr(&self) -> bool { + self.scope == SnippetScope::Expr + } +} + +impl PostfixSnippet { + pub fn new( + label: String, + snippet: &[String], + description: &[String], + requires: &[String], + scope: Option, + ) -> Option { + // validate that these are indeed simple paths + 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 = description.iter().join("\n"); + let description = if description.is_empty() { None } else { Some(description) }; + Some(PostfixSnippet { + scope: scope.unwrap_or(PostfixSnippetScope::Expr), + label, + snippet, + description, + requires: requires.iter().cloned().collect(), // Box::into doesn't work as that has a Copy bound 😒 + }) + } + + // FIXME: This shouldn't be fallible + pub(crate) fn imports( + &self, + ctx: &CompletionContext, + import_scope: &ImportScope, + ) -> Result, ()> { + 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, + requires: &[String], +) -> Result, ()> { + let resolve = |import| { + let path = ast::Path::parse(import).ok()?; + match ctx.scope.speculative_resolve(&path)? { + hir::PathResolution::Macro(_) => None, + hir::PathResolution::Def(def) => { + let item = def.into(); + 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(), + })) + } + _ => None, + } + }; + let mut res = Vec::with_capacity(requires.len()); + for import in requires { + match resolve(import) { + Some(first) => res.extend(first), + None => return Err(()), + } + } + Ok(res) +} diff --git a/crates/ide_completion/src/tests.rs b/crates/ide_completion/src/tests.rs index 07a4953b28..9f5ef2a9aa 100644 --- a/crates/ide_completion/src/tests.rs +++ b/crates/ide_completion/src/tests.rs @@ -75,6 +75,7 @@ pub(crate) const TEST_CONFIG: CompletionConfig = CompletionConfig { skip_glob_imports: true, }, postfix_snippets: Vec::new(), + snippets: Vec::new(), }; pub(crate) fn completion_list(ra_fixture: &str) -> String { diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 9f0bae6f8b..58539543ee 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -12,7 +12,8 @@ use std::{ffi::OsString, iter, path::PathBuf}; use flycheck::FlycheckConfig; use ide::{ AssistConfig, CompletionConfig, DiagnosticsConfig, HighlightRelatedConfig, HoverConfig, - HoverDocFormat, InlayHintsConfig, JoinLinesConfig, PostfixSnippet, + HoverDocFormat, InlayHintsConfig, JoinLinesConfig, PostfixSnippet, PostfixSnippetScope, + Snippet, SnippetScope, }; use ide_db::helpers::{ insert_use::{ImportGranularity, InsertUseConfig, PrefixKind}, @@ -112,10 +113,12 @@ config_data! { completion_addCallArgumentSnippets: bool = "true", /// Whether to add parenthesis when completing functions. completion_addCallParenthesis: bool = "true", + /// Custom completion snippets. + completion_snippets: FxHashMap = "{}", /// Whether to show postfix snippets like `dbg`, `if`, `not`, etc. completion_postfix_enable: bool = "true", - /// Custom postfix completions to show. - completion_postfix_snippets: FxHashMap = "{}", + /// 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", @@ -298,7 +301,8 @@ pub struct Config { detached_files: Vec, pub discovered_projects: Option>, pub root_path: AbsPathBuf, - postfix_snippets: Vec, + postfix_snippets: Vec, + snippets: Vec, } #[derive(Debug, Clone, Eq, PartialEq)] @@ -435,6 +439,7 @@ impl Config { discovered_projects: None, root_path, postfix_snippets: Default::default(), + snippets: Default::default(), } } pub fn update(&mut self, mut json: serde_json::Value) { @@ -452,7 +457,33 @@ impl Config { .completion_postfix_snippets .iter() .flat_map(|(label, desc)| { - PostfixSnippet::new(label.clone(), &desc.snippet, &desc.description, &desc.requires) + PostfixSnippet::new( + label.clone(), + &desc.snippet, + &desc.description, + &desc.requires, + desc.scope.map(|scope| match 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, + desc.scope.map(|scope| match scope { + SnippetScopeDef::Expr => SnippetScope::Expr, + SnippetScopeDef::Item => SnippetScope::Item, + }), + ) }) .collect(); } @@ -791,6 +822,7 @@ impl Config { false )), postfix_snippets: self.postfix_snippets.clone(), + snippets: self.snippets.clone(), } } pub fn assist(&self) -> AssistConfig { @@ -921,14 +953,38 @@ impl Config { } } +#[derive(Deserialize, Debug, Clone, Copy)] +enum PostfixSnippetScopeDef { + Expr, + Type, +} + +#[derive(Deserialize, Debug, Clone, Copy)] +enum SnippetScopeDef { + Expr, + Item, +} + #[derive(Deserialize, Debug, Clone)] -struct PostfixSnippetDesc { +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, + scope: Option, +} + +#[derive(Deserialize, Debug, Clone)] +struct SnippetDef { + #[serde(deserialize_with = "single_or_array")] + description: Vec, + #[serde(deserialize_with = "single_or_array")] + snippet: Vec, + #[serde(deserialize_with = "single_or_array")] + requires: Vec, + scope: Option, } fn single_or_array<'de, D>(deserializer: D) -> Result, D::Error> diff --git a/crates/rust-analyzer/src/integrated_benchmarks.rs b/crates/rust-analyzer/src/integrated_benchmarks.rs index 036196c46e..4b3c83c241 100644 --- a/crates/rust-analyzer/src/integrated_benchmarks.rs +++ b/crates/rust-analyzer/src/integrated_benchmarks.rs @@ -145,6 +145,7 @@ fn integrated_completion_benchmark() { skip_glob_imports: true, }, postfix_snippets: Vec::new(), + snippets: Vec::new(), }; let position = FilePosition { file_id, offset: TextSize::try_from(completion_offset).unwrap() }; @@ -182,6 +183,7 @@ fn integrated_completion_benchmark() { skip_glob_imports: true, }, postfix_snippets: Vec::new(), + snippets: Vec::new(), }; let position = FilePosition { file_id, offset: TextSize::try_from(completion_offset).unwrap() };