From 046c85ef0c56d9c484291b22241a51fa7d2f3a51 Mon Sep 17 00:00:00 2001
From: Lukas Wirth <lukastw97@gmail.com>
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 4b35bfeac63..2c7e1837c3f 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 ecddf59094c..f83001c22d2 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 a896a759abf..9812f25b402 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 b28b59b0a52..bf1dc125c7e 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<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)
-    }
+    pub snippets: Vec<Snippet>,
 }
diff --git a/crates/ide_completion/src/lib.rs b/crates/ide_completion/src/lib.rs
index 62570b73863..56464c07ce2 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 00000000000..1bcb128fa9c
--- /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<String>,
+    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<String>,
+    pub requires: Box<[String]>,
+}
+
+impl Snippet {
+    pub fn new(
+        label: String,
+        snippet: &[String],
+        description: &[String],
+        requires: &[String],
+        scope: Option<SnippetScope>,
+    ) -> 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(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<Vec<ImportEdit>, ()> {
+        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<PostfixSnippetScope>,
+    ) -> 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 {
+            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<Vec<ImportEdit>, ()> {
+        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<Vec<ImportEdit>, ()> {
+    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 07a4953b28d..9f5ef2a9aa7 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 9f0bae6f8bc..58539543ee8 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<String, SnippetDef> = "{}",
         /// 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> = "{}",
+        /// Custom postfix completion snippets.
+        completion_postfix_snippets: FxHashMap<String, PostfixSnippetDef> = "{}",
         /// 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<AbsPathBuf>,
     pub discovered_projects: Option<Vec<ProjectManifest>>,
     pub root_path: AbsPathBuf,
-    postfix_snippets: Vec<ide::PostfixSnippet>,
+    postfix_snippets: Vec<PostfixSnippet>,
+    snippets: Vec<Snippet>,
 }
 
 #[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<String>,
     #[serde(deserialize_with = "single_or_array")]
     snippet: Vec<String>,
     #[serde(deserialize_with = "single_or_array")]
     requires: Vec<String>,
+    scope: Option<PostfixSnippetScopeDef>,
+}
+
+#[derive(Deserialize, Debug, Clone)]
+struct SnippetDef {
+    #[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>,
+    scope: Option<SnippetScopeDef>,
 }
 
 fn single_or_array<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
diff --git a/crates/rust-analyzer/src/integrated_benchmarks.rs b/crates/rust-analyzer/src/integrated_benchmarks.rs
index 036196c46e4..4b3c83c241b 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() };