Unnamed repository; edit this file 'description' to name the repository.
Initial implementation of custom postfix snippets
| -rw-r--r-- | crates/ide/src/lib.rs | 1 | ||||
| -rw-r--r-- | crates/ide_completion/src/completions/postfix.rs | 87 | ||||
| -rw-r--r-- | crates/ide_completion/src/config.rs | 44 | ||||
| -rw-r--r-- | crates/ide_completion/src/context.rs | 3 | ||||
| -rw-r--r-- | crates/ide_completion/src/lib.rs | 2 | ||||
| -rw-r--r-- | crates/ide_completion/src/tests.rs | 1 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/config.rs | 56 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/integrated_benchmarks.rs | 2 |
8 files changed, 189 insertions, 7 deletions
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index 5c47227990..4b35bfeac6 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -99,6 +99,7 @@ pub use ide_assists::{ }; pub use ide_completion::{ CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit, + PostfixSnippet, }; 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 55c49c2322..ecddf59094 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::{FamousDefs, SnippetCap}, + helpers::{import_assets::LocatedImport, 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, + CompletionItem, CompletionItemKind, CompletionRelevance, Completions, ImportEdit, }; pub(crate) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) { @@ -56,6 +56,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.postfix_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 +222,62 @@ 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.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 mut builder = postfix_snippet( + &snippet.label, + snippet.description.as_deref().unwrap_or_default(), + &format!("{}", snippet.snippet(&receiver_text)), + ); + 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, PostfixSnippet, }; fn check(ra_fixture: &str, expect: Expect) { @@ -443,6 +496,32 @@ fn main() { } #[test] + fn custom_postfix_completion() { + check_edit_with_config( + CompletionConfig { + postfix_snippets: vec![PostfixSnippet::new( + "break".into(), + &["ControlFlow::Break($target)".into()], + &[], + &["core::ops::ControlFlow".into()], + ) + .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/config.rs b/crates/ide_completion/src/config.rs index c300ce887b..b28b59b0a5 100644 --- a/crates/ide_completion/src/config.rs +++ b/crates/ide_completion/src/config.rs @@ -5,6 +5,8 @@ //! completions if we are allowed to. use ide_db::helpers::{insert_use::InsertUseConfig, SnippetCap}; +use itertools::Itertools; +use syntax::ast; #[derive(Clone, Debug, PartialEq, Eq)] pub struct CompletionConfig { @@ -15,4 +17,46 @@ pub struct CompletionConfig { pub add_call_argument_snippets: bool, pub snippet_cap: Option<SnippetCap>, pub insert_use: InsertUseConfig, + pub postfix_snippets: Vec<PostfixSnippet>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PostfixSnippet { + pub label: String, + snippet: String, + pub description: Option<String>, + pub requires: Box<[String]>, +} + +impl PostfixSnippet { + pub fn new( + label: String, + snippet: &[String], + description: &[String], + requires: &[String], + ) -> Option<Self> { + // 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) + } } diff --git a/crates/ide_completion/src/context.rs b/crates/ide_completion/src/context.rs index ddf8722bc8..3d59156d00 100644 --- a/crates/ide_completion/src/context.rs +++ b/crates/ide_completion/src/context.rs @@ -869,7 +869,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/lib.rs b/crates/ide_completion/src/lib.rs index f10f3772b1..62570b7386 100644 --- a/crates/ide_completion/src/lib.rs +++ b/crates/ide_completion/src/lib.rs @@ -24,7 +24,7 @@ use text_edit::TextEdit; use crate::{completions::Completions, context::CompletionContext, item::CompletionKind}; pub use crate::{ - config::CompletionConfig, + config::{CompletionConfig, PostfixSnippet}, item::{CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit}, }; diff --git a/crates/ide_completion/src/tests.rs b/crates/ide_completion/src/tests.rs index 6872e3b8dc..07a4953b28 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, }, + postfix_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 6c09833368..9f0bae6f8b 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, PostfixSnippet, }; use ide_db::helpers::{ insert_use::{ImportGranularity, InsertUseConfig, PrefixKind}, @@ -114,6 +114,8 @@ config_data! { completion_addCallParenthesis: bool = "true", /// Whether to show postfix snippets like `dbg`, `if`, `not`, etc. completion_postfix_enable: bool = "true", + /// Custom postfix completions to show. + completion_postfix_snippets: FxHashMap<String, PostfixSnippetDesc> = "{}", /// 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", @@ -296,6 +298,7 @@ pub struct Config { detached_files: Vec<AbsPathBuf>, pub discovered_projects: Option<Vec<ProjectManifest>>, pub root_path: AbsPathBuf, + postfix_snippets: Vec<ide::PostfixSnippet>, } #[derive(Debug, Clone, Eq, PartialEq)] @@ -431,6 +434,7 @@ impl Config { detached_files: Vec::new(), discovered_projects: None, root_path, + postfix_snippets: Default::default(), } } pub fn update(&mut self, mut json: serde_json::Value) { @@ -443,6 +447,14 @@ 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) + }) + .collect(); } pub fn json_schema() -> serde_json::Value { @@ -778,6 +790,7 @@ impl Config { .snippet_support?, false )), + postfix_snippets: self.postfix_snippets.clone(), } } pub fn assist(&self) -> AssistConfig { @@ -909,6 +922,47 @@ impl Config { } #[derive(Deserialize, Debug, Clone)] +struct PostfixSnippetDesc { + #[serde(deserialize_with = "single_or_array")] + description: Vec<String>, + #[serde(deserialize_with = "single_or_array")] + snippet: Vec<String>, + #[serde(deserialize_with = "single_or_array")] + requires: Vec<String>, +} + +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 { Manifest(PathBuf), diff --git a/crates/rust-analyzer/src/integrated_benchmarks.rs b/crates/rust-analyzer/src/integrated_benchmarks.rs index 036cfe157b..036196c46e 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, }, + postfix_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, }, + postfix_snippets: Vec::new(), }; let position = FilePosition { file_id, offset: TextSize::try_from(completion_offset).unwrap() }; |