From 88f213eaddf811dd7616b330eaa14d1dc1060a92 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 4 Oct 2021 17:49:21 +0200 Subject: [PATCH 1/9] Initial implementation of custom postfix snippets --- crates/ide/src/lib.rs | 1 + .../ide_completion/src/completions/postfix.rs | 87 ++++++++++++++++++- crates/ide_completion/src/config.rs | 44 ++++++++++ crates/ide_completion/src/context.rs | 3 +- crates/ide_completion/src/lib.rs | 2 +- crates/ide_completion/src/tests.rs | 1 + crates/rust-analyzer/src/config.rs | 56 +++++++++++- .../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) { @@ -442,6 +495,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( 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, 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) + } } 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 = "{}", /// 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, pub discovered_projects: Option>, pub root_path: AbsPathBuf, + postfix_snippets: Vec, } #[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 { @@ -908,6 +921,47 @@ impl Config { } } +#[derive(Deserialize, Debug, Clone)] +struct PostfixSnippetDesc { + #[serde(deserialize_with = "single_or_array")] + description: Vec, + #[serde(deserialize_with = "single_or_array")] + snippet: Vec, + #[serde(deserialize_with = "single_or_array")] + requires: Vec, +} + +fn single_or_array<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + struct SingleOrVec; + + impl<'de> serde::de::Visitor<'de> for SingleOrVec { + type Value = Vec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("string or array of strings") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(vec![value.to_owned()]) + } + + fn visit_seq(self, seq: A) -> Result + 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 { 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() }; From 046c85ef0c56d9c484291b22241a51fa7d2f3a51 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 4 Oct 2021 19:22:41 +0200 Subject: [PATCH 2/9] 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() }; From 454ecd167cbe516062f9dd9441122eb62d86b42e Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 4 Oct 2021 21:44:33 +0200 Subject: [PATCH 3/9] Make multiple import edits work for completions --- Cargo.lock | 1 + crates/ide/src/lib.rs | 13 +---- crates/ide_completion/Cargo.toml | 1 + .../ide_completion/src/completions/postfix.rs | 11 +++-- .../ide_completion/src/completions/snippet.rs | 12 +++-- crates/ide_completion/src/item.rs | 32 ++++++------- crates/ide_completion/src/lib.rs | 48 ++++++++++++------- crates/ide_completion/src/render.rs | 10 +++- .../ide_completion/src/render/enum_variant.rs | 5 +- crates/ide_completion/src/render/function.rs | 5 +- crates/ide_completion/src/render/macro_.rs | 5 +- crates/ide_completion/src/snippet.rs | 8 ++-- crates/ide_completion/src/tests.rs | 16 ++++--- crates/rust-analyzer/src/config.rs | 28 ++++++++--- crates/rust-analyzer/src/handlers.rs | 6 ++- crates/rust-analyzer/src/lsp_ext.rs | 5 ++ crates/rust-analyzer/src/to_proto.rs | 22 +++++---- 17 files changed, 141 insertions(+), 87 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 2c7e1837c3..c825e4e9cb 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -533,19 +533,10 @@ impl Analysis { &self, config: &CompletionConfig, position: FilePosition, - full_import_path: &str, - imported_name: String, + imports: impl IntoIterator + std::panic::UnwindSafe, ) -> Cancellable> { 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 f83001c22d..44f2aec51b 100644 --- a/crates/ide_completion/src/completions/postfix.rs +++ b/crates/ide_completion/src/completions/postfix.rs @@ -231,9 +231,8 @@ fn add_custom_postfix_completions( 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.imports(ctx, &import_scope) { - Ok(mut imports) => imports.pop(), + let imports = match snippet.imports(ctx, &import_scope) { + Ok(imports) => imports, Err(_) => return, }; let mut builder = postfix_snippet( @@ -241,7 +240,9 @@ fn add_custom_postfix_completions( snippet.description.as_deref().unwrap_or_default(), &format!("{}", snippet.snippet(&receiver_text)), ); - builder.add_import(import); + for import in imports.into_iter() { + builder.add_import(import); + } builder.add_to(acc); }); None @@ -480,7 +481,7 @@ fn main() { &["ControlFlow::Break($receiver)".into()], &[], &["core::ops::ControlFlow".into()], - None, + crate::PostfixSnippetScope::Expr, ) .unwrap()], ..TEST_CONFIG diff --git a/crates/ide_completion/src/completions/snippet.rs b/crates/ide_completion/src/completions/snippet.rs index 9812f25b40..1dace20102 100644 --- a/crates/ide_completion/src/completions/snippet.rs +++ b/crates/ide_completion/src/completions/snippet.rs @@ -104,13 +104,15 @@ fn add_custom_completions( 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(), + let imports = match snip.imports(ctx, &import_scope) { + Ok(imports) => imports, Err(_) => return, }; let mut builder = snippet(ctx, cap, &snip.label, &snip.snippet); - builder.add_import(import).detail(snip.description.as_deref().unwrap_or_default()); + for import in imports.into_iter() { + builder.add_import(import); + } + builder.detail(snip.description.as_deref().unwrap_or_default()); builder.add_to(acc); }); None @@ -132,7 +134,7 @@ mod tests { &["ControlFlow::Break(())".into()], &[], &["core::ops::ControlFlow".into()], - None, + crate::SnippetScope::Expr, ) .unwrap()], ..TEST_CONFIG 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, /// The import data to add to completion's edits. - import_to_add: Option, + 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, + imports_to_add: SmallVec<[ImportEdit; 1]>, trait_name: Option, label: String, insert_text: Option, @@ -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) -> &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) -> &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 56464c07ce..bbfdadafc7 100644 --- a/crates/ide_completion/src/lib.rs +++ b/crates/ide_completion/src/lib.rs @@ -175,8 +175,7 @@ pub fn resolve_completion_edits( db: &RootDatabase, config: &CompletionConfig, position: FilePosition, - full_import_path: &str, - imported_name: String, + imports: impl IntoIterator, ) -> Option> { let ctx = CompletionContext::new(db, position, config)?; let position_for_import = position_for_import(&ctx, None)?; @@ -185,21 +184,34 @@ pub fn resolve_completion_edits( let current_module = ctx.sema.scope(position_for_import).module()?; let current_crate = current_module.krate(); - 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)); + Some( + imports + .into_iter() + .filter_map(|(full_import_path, imported_name)| { + 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), + ); - ImportEdit { import, scope }.to_text_edit(config.insert_use).map(|edit| vec![edit]) + ImportEdit { import, scope: scope.clone() } + .to_text_edit(config.insert_use) + .map(|edit| edit) + }) + .collect(), + ) } 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 95244a758d..4e4663b857 100644 --- a/crates/ide_completion/src/render/function.rs +++ b/crates/ide_completion/src/render/function.rs @@ -98,7 +98,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 index 1bcb128fa9..8100487a72 100644 --- a/crates/ide_completion/src/snippet.rs +++ b/crates/ide_completion/src/snippet.rs @@ -41,7 +41,7 @@ impl Snippet { snippet: &[String], description: &[String], requires: &[String], - scope: Option, + scope: SnippetScope, ) -> Option { // validate that these are indeed simple paths if requires.iter().any(|path| match ast::Path::parse(path) { @@ -57,7 +57,7 @@ impl Snippet { let description = description.iter().join("\n"); let description = if description.is_empty() { None } else { Some(description) }; Some(Snippet { - scope: scope.unwrap_or(SnippetScope::Expr), + scope, label, snippet, description, @@ -89,7 +89,7 @@ impl PostfixSnippet { snippet: &[String], description: &[String], requires: &[String], - scope: Option, + scope: PostfixSnippetScope, ) -> Option { // validate that these are indeed simple paths if requires.iter().any(|path| match ast::Path::parse(path) { @@ -105,7 +105,7 @@ impl PostfixSnippet { let description = description.iter().join("\n"); let description = if description.is_empty() { None } else { Some(description) }; Some(PostfixSnippet { - scope: scope.unwrap_or(PostfixSnippetScope::Expr), + scope, label, snippet, description, diff --git a/crates/ide_completion/src/tests.rs b/crates/ide_completion/src/tests.rs index 9f5ef2a9aa..6f4121dd80 100644 --- a/crates/ide_completion/src/tests.rs +++ b/crates/ide_completion/src/tests.rs @@ -183,13 +183,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 58539543ee..150fc8e702 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -462,10 +462,10 @@ impl Config { &desc.snippet, &desc.description, &desc.requires, - desc.scope.map(|scope| match scope { + match desc.scope { PostfixSnippetScopeDef::Expr => PostfixSnippetScope::Expr, PostfixSnippetScopeDef::Type => PostfixSnippetScope::Type, - }), + }, ) }) .collect(); @@ -479,10 +479,10 @@ impl Config { &desc.snippet, &desc.description, &desc.requires, - desc.scope.map(|scope| match scope { + match desc.scope { SnippetScopeDef::Expr => SnippetScope::Expr, SnippetScopeDef::Item => SnippetScope::Item, - }), + }, ) }) .collect(); @@ -954,17 +954,31 @@ 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, } +impl Default for SnippetScopeDef { + fn default() -> Self { + SnippetScopeDef::Expr + } +} + #[derive(Deserialize, Debug, Clone)] struct PostfixSnippetDef { #[serde(deserialize_with = "single_or_array")] @@ -973,7 +987,8 @@ struct PostfixSnippetDef { snippet: Vec, #[serde(deserialize_with = "single_or_array")] requires: Vec, - scope: Option, + #[serde(default)] + scope: PostfixSnippetScopeDef, } #[derive(Deserialize, Debug, Clone)] @@ -984,7 +999,8 @@ struct SnippetDef { snippet: Vec, #[serde(deserialize_with = "single_or_array")] requires: Vec, - scope: Option, + #[serde(default)] + scope: SnippetScopeDef, } fn single_or_array<'de, D>(deserializer: D) -> Result, D::Error> diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index e62bb9499f..6ad5cb5334 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -787,8 +787,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/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, +} + +#[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()); } } From d0c8777a44001dcf87417abbb814e9977c381935 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 4 Oct 2021 21:47:02 +0200 Subject: [PATCH 4/9] Regen generated_config.adoc --- docs/user/generated_config.adoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc index 768216fed9..7ec66c18be 100644 --- a/docs/user/generated_config.adoc +++ b/docs/user/generated_config.adoc @@ -136,11 +136,21 @@ 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`):: + -- Whether to show postfix snippets like `dbg`, `if`, `not`, etc. -- +[[rust-analyzer.completion.postfix.snippets]]rust-analyzer.completion.postfix.snippets (default: `{}`):: ++ +-- +Custom postfix completion snippets. +-- [[rust-analyzer.completion.autoimport.enable]]rust-analyzer.completion.autoimport.enable (default: `true`):: + -- From 1f7e14e78b26e34470ee5f1f1beae0720aa2b0fd Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 4 Oct 2021 22:05:30 +0200 Subject: [PATCH 5/9] Fix multiple imports acting on files on separately --- crates/ide_completion/src/lib.rs | 58 +++++++++++++++----------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/crates/ide_completion/src/lib.rs b/crates/ide_completion/src/lib.rs index bbfdadafc7..647bbc8af5 100644 --- a/crates/ide_completion/src/lib.rs +++ b/crates/ide_completion/src/lib.rs @@ -15,11 +15,13 @@ 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}; @@ -177,41 +179,35 @@ pub fn resolve_completion_edits( position: FilePosition, imports: impl IntoIterator, ) -> Option> { + 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(); - Some( - imports - .into_iter() - .filter_map(|(full_import_path, imported_name)| { - 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), - ); - - ImportEdit { import, scope: scope.clone() } - .to_text_edit(config.insert_use) - .map(|edit| edit) + // 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) }) - .collect(), - ) + .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); + } + }); + + algo::diff(scope.as_syntax_node(), new_ast.as_syntax_node()).into_text_edit(&mut import_insert); + Some(vec![import_insert.finish()]) } From ca1fdd75f15b2c47962d123aebbe0e235cf1b43b Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 4 Oct 2021 22:13:12 +0200 Subject: [PATCH 6/9] Regen docs --- crates/ide_completion/src/snippet.rs | 3 +++ crates/rust-analyzer/src/config.rs | 26 ++++++++++++++++---------- docs/dev/lsp-extensions.md | 2 +- editors/code/package.json | 10 ++++++++++ 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/crates/ide_completion/src/snippet.rs b/crates/ide_completion/src/snippet.rs index 8100487a72..9d6e18f1a7 100644 --- a/crates/ide_completion/src/snippet.rs +++ b/crates/ide_completion/src/snippet.rs @@ -1,3 +1,6 @@ +//! User (postfix)-snippet definitions. +//! +//! Actual logic is implemented in [`crate::completions::postfix`] and [`crate::completions::snippet`]. use ide_db::helpers::{import_assets::LocatedImport, insert_use::ImportScope}; use itertools::Itertools; use syntax::ast; diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 150fc8e702..9fde9db8d6 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -282,9 +282,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\"", } } @@ -893,14 +893,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, }, } } @@ -1065,14 +1065,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, } @@ -1203,6 +1203,12 @@ 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", + }, "FxHashMap" => set! { "type": "object", }, @@ -1259,7 +1265,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": [ @@ -1267,7 +1273,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/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 @@