ide_completion/snippet.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
//! User (postfix)-snippet definitions.
//!
//! Actual logic is implemented in [`crate::completions::postfix`] and [`crate::completions::snippet`] respectively.
// Feature: User Snippet Completions
//
// rust-analyzer allows the user to define custom (postfix)-snippets that may depend on items to be accessible for the current scope to be applicable.
//
// A custom snippet can be defined by adding it to the `rust-analyzer.completion.snippets.custom` object respectively.
//
// [source,json]
// ----
// {
// "rust-analyzer.completion.snippets.custom": {
// "thread spawn": {
// "prefix": ["spawn", "tspawn"],
// "body": [
// "thread::spawn(move || {",
// "\t$0",
// "});",
// ],
// "description": "Insert a thread::spawn call",
// "requires": "std::thread",
// "scope": "expr",
// }
// }
// }
// ----
//
// In the example above:
//
// * `"thread spawn"` is the name of the snippet.
//
// * `prefix` defines one or more trigger words that will trigger the snippets completion.
// Using `postfix` will instead create a postfix snippet.
//
// * `body` is one or more lines of content joined via newlines for the final output.
//
// * `description` is an optional description of the snippet, if unset the snippet name will be used.
//
// * `requires` is an optional list of item paths that have to be resolvable in the current crate where the completion is rendered.
// On failure of resolution the snippet won't be applicable, otherwise the snippet will insert an import for the items on insertion if
// the items aren't yet in scope.
//
// * `scope` is an optional filter for when the snippet should be applicable. Possible values are:
// ** for Snippet-Scopes: `expr`, `item` (default: `item`)
// ** for Postfix-Snippet-Scopes: `expr`, `type` (default: `expr`)
//
// The `body` field also has access to placeholders as visible in the example as `$0`.
// These placeholders take the form of `$number` or `${number:placeholder_text}` which can be traversed as tabstop in ascending order starting from 1,
// with `$0` being a special case that always comes last.
//
// There is also a special placeholder, `${receiver}`, which will be replaced by the receiver expression for postfix snippets, or a `$0` tabstop in case of normal snippets.
// This replacement for normal snippets allows you to reuse a snippet for both post- and prefix in a single definition.
//
// For the VSCode editor, rust-analyzer also ships with a small set of defaults which can be removed
// by overwriting the settings object mentioned above, the defaults are:
// [source,json]
// ----
// {
// "Arc::new": {
// "postfix": "arc",
// "body": "Arc::new(${receiver})",
// "requires": "std::sync::Arc",
// "description": "Put the expression into an `Arc`",
// "scope": "expr"
// },
// "Rc::new": {
// "postfix": "rc",
// "body": "Rc::new(${receiver})",
// "requires": "std::rc::Rc",
// "description": "Put the expression into an `Rc`",
// "scope": "expr"
// },
// "Box::pin": {
// "postfix": "pinbox",
// "body": "Box::pin(${receiver})",
// "requires": "std::boxed::Box",
// "description": "Put the expression into a pinned `Box`",
// "scope": "expr"
// },
// "Ok": {
// "postfix": "ok",
// "body": "Ok(${receiver})",
// "description": "Wrap the expression in a `Result::Ok`",
// "scope": "expr"
// },
// "Err": {
// "postfix": "err",
// "body": "Err(${receiver})",
// "description": "Wrap the expression in a `Result::Err`",
// "scope": "expr"
// },
// "Some": {
// "postfix": "some",
// "body": "Some(${receiver})",
// "description": "Wrap the expression in an `Option::Some`",
// "scope": "expr"
// }
// }
// ----
use ide_db::imports::import_assets::LocatedImport;
use itertools::Itertools;
use syntax::{ast, AstNode, GreenNode, SyntaxNode};
use crate::context::CompletionContext;
/// A snippet scope describing where a snippet may apply to.
/// These may differ slightly in meaning depending on the snippet trigger.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SnippetScope {
Item,
Expr,
Type,
}
/// A user supplied snippet.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Snippet {
pub postfix_triggers: Box<[Box<str>]>,
pub prefix_triggers: Box<[Box<str>]>,
pub scope: SnippetScope,
pub description: Option<Box<str>>,
snippet: String,
// These are `ast::Path`'s but due to SyntaxNodes not being Send we store these
// and reconstruct them on demand instead. This is cheaper than reparsing them
// from strings
requires: Box<[GreenNode]>,
}
impl Snippet {
pub fn new(
prefix_triggers: &[String],
postfix_triggers: &[String],
snippet: &[String],
description: &str,
requires: &[String],
scope: SnippetScope,
) -> Option<Self> {
if prefix_triggers.is_empty() && postfix_triggers.is_empty() {
return None;
}
let (requires, snippet, description) = validate_snippet(snippet, description, requires)?;
Some(Snippet {
// Box::into doesn't work as that has a Copy bound 😒
postfix_triggers: postfix_triggers.iter().map(String::as_str).map(Into::into).collect(),
prefix_triggers: prefix_triggers.iter().map(String::as_str).map(Into::into).collect(),
scope,
snippet,
description,
requires,
})
}
/// Returns [`None`] if the required items do not resolve.
pub(crate) fn imports(&self, ctx: &CompletionContext<'_>) -> Option<Vec<LocatedImport>> {
import_edits(ctx, &self.requires)
}
pub fn snippet(&self) -> String {
self.snippet.replace("${receiver}", "$0")
}
pub fn postfix_snippet(&self, receiver: &str) -> String {
self.snippet.replace("${receiver}", receiver)
}
}
fn import_edits(ctx: &CompletionContext<'_>, requires: &[GreenNode]) -> Option<Vec<LocatedImport>> {
let import_cfg = ctx.config.import_path_config();
let resolve = |import: &GreenNode| {
let path = ast::Path::cast(SyntaxNode::new_root(import.clone()))?;
let item = match ctx.scope.speculative_resolve(&path)? {
hir::PathResolution::Def(def) => def.into(),
_ => return None,
};
let path = ctx.module.find_use_path(
ctx.db,
item,
ctx.config.insert_use.prefix_kind,
import_cfg,
)?;
Some((path.len() > 1).then(|| LocatedImport::new(path.clone(), item, item)))
};
let mut res = Vec::with_capacity(requires.len());
for import in requires {
match resolve(import) {
Some(first) => res.extend(first),
None => return None,
}
}
Some(res)
}
fn validate_snippet(
snippet: &[String],
description: &str,
requires: &[String],
) -> Option<(Box<[GreenNode]>, String, Option<Box<str>>)> {
let mut imports = Vec::with_capacity(requires.len());
for path in requires.iter() {
let use_path =
ast::SourceFile::parse(&format!("use {path};"), syntax::Edition::CURRENT_FIXME)
.syntax_node()
.descendants()
.find_map(ast::Path::cast)?;
if use_path.syntax().text() != path.as_str() {
return None;
}
let green = use_path.syntax().green().into_owned();
imports.push(green);
}
let snippet = snippet.iter().join("\n");
let description = (!description.is_empty())
.then(|| description.split_once('\n').map_or(description, |(it, _)| it))
.map(ToOwned::to_owned)
.map(Into::into);
Some((imports.into_boxed_slice(), snippet, description))
}