Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide-completion/src/render/macro_.rs')
| -rw-r--r-- | crates/ide-completion/src/render/macro_.rs | 254 |
1 files changed, 254 insertions, 0 deletions
diff --git a/crates/ide-completion/src/render/macro_.rs b/crates/ide-completion/src/render/macro_.rs new file mode 100644 index 0000000000..ba18e5d216 --- /dev/null +++ b/crates/ide-completion/src/render/macro_.rs @@ -0,0 +1,254 @@ +//! Renderer for macro invocations. + +use hir::{Documentation, HirDisplay}; +use ide_db::SymbolKind; +use syntax::SmolStr; + +use crate::{ + context::PathKind, + item::{Builder, CompletionItem}, + render::RenderContext, +}; + +pub(crate) fn render_macro(ctx: RenderContext<'_>, name: hir::Name, macro_: hir::Macro) -> Builder { + let _p = profile::span("render_macro"); + render(ctx, name, macro_) +} + +fn render( + ctx @ RenderContext { completion, .. }: RenderContext<'_>, + name: hir::Name, + macro_: hir::Macro, +) -> Builder { + let source_range = if completion.is_immediately_after_macro_bang() { + cov_mark::hit!(completes_macro_call_if_cursor_at_bang_token); + completion.token.parent().map_or_else(|| ctx.source_range(), |it| it.text_range()) + } else { + ctx.source_range() + }; + + let name = name.to_smol_str(); + let docs = ctx.docs(macro_); + let docs_str = docs.as_ref().map(Documentation::as_str).unwrap_or_default(); + let is_fn_like = macro_.is_fn_like(completion.db); + let (bra, ket) = if is_fn_like { guess_macro_braces(&name, docs_str) } else { ("", "") }; + + let needs_bang = + is_fn_like && !matches!(completion.path_kind(), Some(PathKind::Mac | PathKind::Use)); + + let mut item = CompletionItem::new( + SymbolKind::from(macro_.kind(completion.db)), + source_range, + label(&ctx, needs_bang, bra, ket, &name), + ); + item.set_deprecated(ctx.is_deprecated(macro_)) + .detail(macro_.display(completion.db).to_string()) + .set_documentation(docs) + .set_relevance(ctx.completion_relevance()); + + let name = &*name; + match ctx.snippet_cap() { + Some(cap) if needs_bang && !completion.path_is_call() => { + let snippet = format!("{}!{}$0{}", name, bra, ket); + let lookup = banged_name(name); + item.insert_snippet(cap, snippet).lookup_by(lookup); + } + _ if needs_bang => { + let banged_name = banged_name(name); + item.insert_text(banged_name.clone()).lookup_by(banged_name); + } + _ => { + cov_mark::hit!(dont_insert_macro_call_parens_unncessary); + item.insert_text(name); + } + }; + if let Some(import_to_add) = ctx.import_to_add { + item.add_import(import_to_add); + } + + item +} + +fn label( + ctx: &RenderContext<'_>, + needs_bang: bool, + bra: &str, + ket: &str, + name: &SmolStr, +) -> SmolStr { + if needs_bang { + if ctx.snippet_cap().is_some() { + SmolStr::from_iter([&*name, "!", bra, "…", ket]) + } else { + banged_name(name) + } + } else { + name.clone() + } +} + +fn banged_name(name: &str) -> SmolStr { + SmolStr::from_iter([name, "!"]) +} + +fn guess_macro_braces(macro_name: &str, docs: &str) -> (&'static str, &'static str) { + let mut votes = [0, 0, 0]; + for (idx, s) in docs.match_indices(¯o_name) { + let (before, after) = (&docs[..idx], &docs[idx + s.len()..]); + // Ensure to match the full word + if after.starts_with('!') + && !before.ends_with(|c: char| c == '_' || c.is_ascii_alphanumeric()) + { + // It may have spaces before the braces like `foo! {}` + match after[1..].chars().find(|&c| !c.is_whitespace()) { + Some('{') => votes[0] += 1, + Some('[') => votes[1] += 1, + Some('(') => votes[2] += 1, + _ => {} + } + } + } + + // Insert a space before `{}`. + // We prefer the last one when some votes equal. + let (_vote, (bra, ket)) = votes + .iter() + .zip(&[(" {", "}"), ("[", "]"), ("(", ")")]) + .max_by_key(|&(&vote, _)| vote) + .unwrap(); + (*bra, *ket) +} + +#[cfg(test)] +mod tests { + use crate::tests::check_edit; + + #[test] + fn dont_insert_macro_call_parens_unncessary() { + cov_mark::check!(dont_insert_macro_call_parens_unncessary); + check_edit( + "frobnicate", + r#" +//- /main.rs crate:main deps:foo +use foo::$0; +//- /foo/lib.rs crate:foo +#[macro_export] +macro_rules! frobnicate { () => () } +"#, + r#" +use foo::frobnicate; +"#, + ); + + check_edit( + "frobnicate", + r#" +macro_rules! frobnicate { () => () } +fn main() { frob$0!(); } +"#, + r#" +macro_rules! frobnicate { () => () } +fn main() { frobnicate!(); } +"#, + ); + } + + #[test] + fn add_bang_to_parens() { + check_edit( + "frobnicate!", + r#" +macro_rules! frobnicate { () => () } +fn main() { + frob$0() +} +"#, + r#" +macro_rules! frobnicate { () => () } +fn main() { + frobnicate!() +} +"#, + ); + } + + #[test] + fn guesses_macro_braces() { + check_edit( + "vec!", + r#" +/// Creates a [`Vec`] containing the arguments. +/// +/// ``` +/// let v = vec![1, 2, 3]; +/// assert_eq!(v[0], 1); +/// assert_eq!(v[1], 2); +/// assert_eq!(v[2], 3); +/// ``` +macro_rules! vec { () => {} } + +fn main() { v$0 } +"#, + r#" +/// Creates a [`Vec`] containing the arguments. +/// +/// ``` +/// let v = vec![1, 2, 3]; +/// assert_eq!(v[0], 1); +/// assert_eq!(v[1], 2); +/// assert_eq!(v[2], 3); +/// ``` +macro_rules! vec { () => {} } + +fn main() { vec![$0] } +"#, + ); + + check_edit( + "foo!", + r#" +/// Foo +/// +/// Don't call `fooo!()` `fooo!()`, or `_foo![]` `_foo![]`, +/// call as `let _=foo! { hello world };` +macro_rules! foo { () => {} } +fn main() { $0 } +"#, + r#" +/// Foo +/// +/// Don't call `fooo!()` `fooo!()`, or `_foo![]` `_foo![]`, +/// call as `let _=foo! { hello world };` +macro_rules! foo { () => {} } +fn main() { foo! {$0} } +"#, + ) + } + + #[test] + fn completes_macro_call_if_cursor_at_bang_token() { + // Regression test for https://github.com/rust-analyzer/rust-analyzer/issues/9904 + cov_mark::check!(completes_macro_call_if_cursor_at_bang_token); + check_edit( + "foo!", + r#" +macro_rules! foo { + () => {} +} + +fn main() { + foo!$0 +} +"#, + r#" +macro_rules! foo { + () => {} +} + +fn main() { + foo!($0) +} +"#, + ); + } +} |