Add custom non-postfix snippets

This commit is contained in:
Lukas Wirth 2021-10-04 19:22:41 +02:00
parent 88f213eadd
commit 046c85ef0c
9 changed files with 315 additions and 83 deletions

View file

@ -99,7 +99,7 @@ pub use ide_assists::{
}; };
pub use ide_completion::{ pub use ide_completion::{
CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit, CompletionConfig, CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit,
PostfixSnippet, PostfixSnippet, PostfixSnippetScope, Snippet, SnippetScope,
}; };
pub use ide_db::{ pub use ide_db::{
base_db::{ base_db::{

View file

@ -3,7 +3,7 @@
mod format_like; mod format_like;
use ide_db::{ use ide_db::{
helpers::{import_assets::LocatedImport, insert_use::ImportScope, FamousDefs, SnippetCap}, helpers::{insert_use::ImportScope, FamousDefs, SnippetCap},
ty_filter::TryEnum, ty_filter::TryEnum,
}; };
use syntax::{ use syntax::{
@ -18,7 +18,7 @@ use crate::{
context::CompletionContext, context::CompletionContext,
item::{Builder, CompletionKind}, item::{Builder, CompletionKind},
patterns::ImmediateLocation, patterns::ImmediateLocation,
CompletionItem, CompletionItemKind, CompletionRelevance, Completions, ImportEdit, CompletionItem, CompletionItemKind, CompletionRelevance, Completions,
}; };
pub(crate) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) { 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)?; ImportScope::find_insert_use_container_with_macros(&ctx.token.parent()?, &ctx.sema)?;
ctx.config.postfix_snippets.iter().for_each(|snippet| { ctx.config.postfix_snippets.iter().for_each(|snippet| {
// FIXME: Support multiple imports // FIXME: Support multiple imports
let import = match snippet.requires.get(0) { let import = match snippet.imports(ctx, &import_scope) {
Some(import) => { Ok(mut imports) => imports.pop(),
let res = (|| { Err(_) => return,
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( let mut builder = postfix_snippet(
&snippet.label, &snippet.label,
@ -501,9 +477,10 @@ fn main() {
CompletionConfig { CompletionConfig {
postfix_snippets: vec![PostfixSnippet::new( postfix_snippets: vec![PostfixSnippet::new(
"break".into(), "break".into(),
&["ControlFlow::Break($target)".into()], &["ControlFlow::Break($receiver)".into()],
&[], &[],
&["core::ops::ControlFlow".into()], &["core::ops::ControlFlow".into()],
None,
) )
.unwrap()], .unwrap()],
..TEST_CONFIG ..TEST_CONFIG

View file

@ -1,11 +1,11 @@
//! This file provides snippet completions, like `pd` => `eprintln!(...)`. //! 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 syntax::T;
use crate::{ use crate::{
context::PathCompletionContext, item::Builder, CompletionContext, CompletionItem, context::PathCompletionContext, item::Builder, CompletionContext, CompletionItem,
CompletionItemKind, CompletionKind, Completions, CompletionItemKind, CompletionKind, Completions, SnippetScope,
}; };
fn snippet(ctx: &CompletionContext, cap: SnippetCap, label: &str, snippet: &str) -> Builder { 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, None => return,
}; };
if !ctx.config.snippets.is_empty() {
add_custom_completions(acc, ctx, cap, SnippetScope::Expr);
}
if can_be_stmt { if can_be_stmt {
snippet(ctx, cap, "pd", "eprintln!(\"$0 = {:?}\", $0);").add_to(acc); snippet(ctx, cap, "pd", "eprintln!(\"$0 = {:?}\", $0);").add_to(acc);
snippet(ctx, cap, "ppd", "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, None => return,
}; };
if !ctx.config.snippets.is_empty() {
add_custom_completions(acc, ctx, cap, SnippetScope::Item);
}
let mut item = snippet( let mut item = snippet(
ctx, ctx,
cap, 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}"); let item = snippet(ctx, cap, "macro_rules", "macro_rules! $1 {\n\t($2) => {\n\t\t$0\n\t};\n}");
item.add_to(acc); 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(()) }
"#,
);
}
}

View file

@ -5,8 +5,8 @@
//! completions if we are allowed to. //! completions if we are allowed to.
use ide_db::helpers::{insert_use::InsertUseConfig, SnippetCap}; use ide_db::helpers::{insert_use::InsertUseConfig, SnippetCap};
use itertools::Itertools;
use syntax::ast; use crate::snippet::{PostfixSnippet, Snippet};
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct CompletionConfig { pub struct CompletionConfig {
@ -18,45 +18,5 @@ pub struct CompletionConfig {
pub snippet_cap: Option<SnippetCap>, pub snippet_cap: Option<SnippetCap>,
pub insert_use: InsertUseConfig, pub insert_use: InsertUseConfig,
pub postfix_snippets: Vec<PostfixSnippet>, pub postfix_snippets: Vec<PostfixSnippet>,
} pub snippets: Vec<Snippet>,
#[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)
}
} }

View file

@ -9,6 +9,7 @@ mod render;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
mod snippet;
use completions::flyimport::position_for_import; use completions::flyimport::position_for_import;
use ide_db::{ use ide_db::{
@ -24,8 +25,9 @@ use text_edit::TextEdit;
use crate::{completions::Completions, context::CompletionContext, item::CompletionKind}; use crate::{completions::Completions, context::CompletionContext, item::CompletionKind};
pub use crate::{ pub use crate::{
config::{CompletionConfig, PostfixSnippet}, config::CompletionConfig,
item::{CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit}, item::{CompletionItem, CompletionItemKind, CompletionRelevance, ImportEdit},
snippet::{PostfixSnippet, PostfixSnippetScope, Snippet, SnippetScope},
}; };
//FIXME: split the following feature into fine-grained features. //FIXME: split the following feature into fine-grained features.

View file

@ -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)
}

View file

@ -75,6 +75,7 @@ pub(crate) const TEST_CONFIG: CompletionConfig = CompletionConfig {
skip_glob_imports: true, skip_glob_imports: true,
}, },
postfix_snippets: Vec::new(), postfix_snippets: Vec::new(),
snippets: Vec::new(),
}; };
pub(crate) fn completion_list(ra_fixture: &str) -> String { pub(crate) fn completion_list(ra_fixture: &str) -> String {

View file

@ -12,7 +12,8 @@ use std::{ffi::OsString, iter, path::PathBuf};
use flycheck::FlycheckConfig; use flycheck::FlycheckConfig;
use ide::{ use ide::{
AssistConfig, CompletionConfig, DiagnosticsConfig, HighlightRelatedConfig, HoverConfig, AssistConfig, CompletionConfig, DiagnosticsConfig, HighlightRelatedConfig, HoverConfig,
HoverDocFormat, InlayHintsConfig, JoinLinesConfig, PostfixSnippet, HoverDocFormat, InlayHintsConfig, JoinLinesConfig, PostfixSnippet, PostfixSnippetScope,
Snippet, SnippetScope,
}; };
use ide_db::helpers::{ use ide_db::helpers::{
insert_use::{ImportGranularity, InsertUseConfig, PrefixKind}, insert_use::{ImportGranularity, InsertUseConfig, PrefixKind},
@ -112,10 +113,12 @@ config_data! {
completion_addCallArgumentSnippets: bool = "true", completion_addCallArgumentSnippets: bool = "true",
/// Whether to add parenthesis when completing functions. /// Whether to add parenthesis when completing functions.
completion_addCallParenthesis: bool = "true", completion_addCallParenthesis: bool = "true",
/// Custom completion snippets.
completion_snippets: FxHashMap<String, SnippetDef> = "{}",
/// Whether to show postfix snippets like `dbg`, `if`, `not`, etc. /// Whether to show postfix snippets like `dbg`, `if`, `not`, etc.
completion_postfix_enable: bool = "true", completion_postfix_enable: bool = "true",
/// Custom postfix completions to show. /// Custom postfix completion snippets.
completion_postfix_snippets: FxHashMap<String, PostfixSnippetDesc> = "{}", completion_postfix_snippets: FxHashMap<String, PostfixSnippetDef> = "{}",
/// Toggles the additional completions that automatically add imports when completed. /// 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. /// Note that your client must specify the `additionalTextEdits` LSP client capability to truly have this feature enabled.
completion_autoimport_enable: bool = "true", completion_autoimport_enable: bool = "true",
@ -298,7 +301,8 @@ pub struct Config {
detached_files: Vec<AbsPathBuf>, detached_files: Vec<AbsPathBuf>,
pub discovered_projects: Option<Vec<ProjectManifest>>, pub discovered_projects: Option<Vec<ProjectManifest>>,
pub root_path: AbsPathBuf, pub root_path: AbsPathBuf,
postfix_snippets: Vec<ide::PostfixSnippet>, postfix_snippets: Vec<PostfixSnippet>,
snippets: Vec<Snippet>,
} }
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
@ -435,6 +439,7 @@ impl Config {
discovered_projects: None, discovered_projects: None,
root_path, root_path,
postfix_snippets: Default::default(), postfix_snippets: Default::default(),
snippets: Default::default(),
} }
} }
pub fn update(&mut self, mut json: serde_json::Value) { pub fn update(&mut self, mut json: serde_json::Value) {
@ -452,7 +457,33 @@ impl Config {
.completion_postfix_snippets .completion_postfix_snippets
.iter() .iter()
.flat_map(|(label, desc)| { .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(); .collect();
} }
@ -791,6 +822,7 @@ impl Config {
false false
)), )),
postfix_snippets: self.postfix_snippets.clone(), postfix_snippets: self.postfix_snippets.clone(),
snippets: self.snippets.clone(),
} }
} }
pub fn assist(&self) -> AssistConfig { 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)] #[derive(Deserialize, Debug, Clone)]
struct PostfixSnippetDesc { struct PostfixSnippetDef {
#[serde(deserialize_with = "single_or_array")] #[serde(deserialize_with = "single_or_array")]
description: Vec<String>, description: Vec<String>,
#[serde(deserialize_with = "single_or_array")] #[serde(deserialize_with = "single_or_array")]
snippet: Vec<String>, snippet: Vec<String>,
#[serde(deserialize_with = "single_or_array")] #[serde(deserialize_with = "single_or_array")]
requires: Vec<String>, 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> fn single_or_array<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>

View file

@ -145,6 +145,7 @@ fn integrated_completion_benchmark() {
skip_glob_imports: true, skip_glob_imports: true,
}, },
postfix_snippets: Vec::new(), postfix_snippets: Vec::new(),
snippets: Vec::new(),
}; };
let position = let position =
FilePosition { file_id, offset: TextSize::try_from(completion_offset).unwrap() }; FilePosition { file_id, offset: TextSize::try_from(completion_offset).unwrap() };
@ -182,6 +183,7 @@ fn integrated_completion_benchmark() {
skip_glob_imports: true, skip_glob_imports: true,
}, },
postfix_snippets: Vec::new(), postfix_snippets: Vec::new(),
snippets: Vec::new(),
}; };
let position = let position =
FilePosition { file_id, offset: TextSize::try_from(completion_offset).unwrap() }; FilePosition { file_id, offset: TextSize::try_from(completion_offset).unwrap() };