Add #[rust_analyzer::macro_style()] attribute to control macro completion brace style

This commit is contained in:
Chayim Refael Friedman 2025-12-30 19:39:13 +02:00
parent c24dff6682
commit 975aa0dc8a
4 changed files with 138 additions and 13 deletions

View file

@ -99,6 +99,20 @@ fn extract_ra_completions(attr_flags: &mut AttrFlags, tt: ast::TokenTree) {
}
}
fn extract_ra_macro_style(attr_flags: &mut AttrFlags, tt: ast::TokenTree) {
let tt = TokenTreeChildren::new(&tt);
if let Ok(NodeOrToken::Token(option)) = Itertools::exactly_one(tt)
&& option.kind().is_any_identifier()
{
match option.text() {
"braces" => attr_flags.insert(AttrFlags::MACRO_STYLE_BRACES),
"brackets" => attr_flags.insert(AttrFlags::MACRO_STYLE_BRACKETS),
"parentheses" => attr_flags.insert(AttrFlags::MACRO_STYLE_PARENTHESES),
_ => {}
}
}
}
fn extract_rustc_skip_during_method_dispatch(attr_flags: &mut AttrFlags, tt: ast::TokenTree) {
let iter = TokenTreeChildren::new(&tt);
for kind in iter {
@ -163,6 +177,7 @@ fn match_attr_flags(attr_flags: &mut AttrFlags, attr: Meta) -> ControlFlow<Infal
2 => match path.segments[0].text() {
"rust_analyzer" => match path.segments[1].text() {
"completions" => extract_ra_completions(attr_flags, tt),
"macro_style" => extract_ra_macro_style(attr_flags, tt),
_ => {}
},
_ => {}
@ -291,6 +306,10 @@ bitflags::bitflags! {
const RUSTC_COINDUCTIVE = 1 << 43;
const RUSTC_FORCE_INLINE = 1 << 44;
const IS_POINTEE = 1 << 45;
const MACRO_STYLE_BRACES = 1 << 46;
const MACRO_STYLE_BRACKETS = 1 << 47;
const MACRO_STYLE_PARENTHESES = 1 << 48;
}
}

View file

@ -3429,6 +3429,50 @@ impl Macro {
pub fn is_derive(&self, db: &dyn HirDatabase) -> bool {
matches!(self.kind(db), MacroKind::Derive | MacroKind::DeriveBuiltIn)
}
pub fn preferred_brace_style(&self, db: &dyn HirDatabase) -> Option<MacroBraces> {
let attrs = self.attrs(db);
MacroBraces::extract(attrs.attrs)
}
}
// Feature: Macro Brace Style Attribute
// Crate authors can declare the preferred brace style for their macro. This will affect how completion
// insert calls to it.
//
// This is only supported on function-like macros.
//
// To do that, insert the `#[rust_analyzer::macro_style(style)]` attribute on the macro (for proc macros,
// insert it for the macro's function). `style` can be one of:
//
// - `braces` for `{...}` style.
// - `brackets` for `[...]` style.
// - `parentheses` for `(...)` style.
//
// Malformed attributes will be ignored without warnings.
//
// Note that users have no way to override this attribute, so be careful and only include things
// users definitely do not want to be completed!
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MacroBraces {
Braces,
Brackets,
Parentheses,
}
impl MacroBraces {
fn extract(attrs: AttrFlags) -> Option<Self> {
if attrs.contains(AttrFlags::MACRO_STYLE_BRACES) {
Some(Self::Braces)
} else if attrs.contains(AttrFlags::MACRO_STYLE_BRACKETS) {
Some(Self::Brackets)
} else if attrs.contains(AttrFlags::MACRO_STYLE_PARENTHESES) {
Some(Self::Parentheses)
} else {
None
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]

View file

@ -1,6 +1,6 @@
//! Renderer for macro invocations.
use hir::HirDisplay;
use hir::{HirDisplay, db::HirDatabase};
use ide_db::{SymbolKind, documentation::Documentation};
use syntax::{SmolStr, ToSmolStr, format_smolstr};
@ -46,17 +46,15 @@ fn render(
ctx.source_range()
};
let orig_name = macro_.name(ctx.db());
let (name, orig_name, escaped_name) = (
name.as_str(),
orig_name.as_str(),
name.display(ctx.db(), completion.edition).to_smolstr(),
);
let (name, escaped_name) =
(name.as_str(), name.display(ctx.db(), completion.edition).to_smolstr());
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, orig_name, docs_str) } else { ("", "") };
let (bra, ket) = if is_fn_like {
guess_macro_braces(ctx.db(), macro_, name, docs.as_ref())
} else {
("", "")
};
let needs_bang = is_fn_like && !is_use_path && !has_macro_bang;
@ -115,12 +113,24 @@ fn banged_name(name: &str) -> SmolStr {
}
fn guess_macro_braces(
db: &dyn HirDatabase,
macro_: hir::Macro,
macro_name: &str,
orig_name: &str,
docs: &str,
docs: Option<&Documentation<'_>>,
) -> (&'static str, &'static str) {
if let Some(style) = macro_.preferred_brace_style(db) {
return match style {
hir::MacroBraces::Braces => (" {", "}"),
hir::MacroBraces::Brackets => ("[", "]"),
hir::MacroBraces::Parentheses => ("(", ")"),
};
}
let orig_name = macro_.name(db);
let docs = docs.map(Documentation::as_str).unwrap_or_default();
let mut votes = [0, 0, 0];
for (idx, s) in docs.match_indices(macro_name).chain(docs.match_indices(orig_name)) {
for (idx, s) in docs.match_indices(macro_name).chain(docs.match_indices(orig_name.as_str())) {
let (before, after) = (&docs[..idx], &docs[idx + s.len()..]);
// Ensure to match the full word
if after.starts_with('!')
@ -199,6 +209,57 @@ fn main() {
);
}
#[test]
fn preferred_macro_braces() {
check_edit(
"vec!",
r#"
#[rust_analyzer::macro_style(brackets)]
macro_rules! vec { () => {} }
fn main() { v$0 }
"#,
r#"
#[rust_analyzer::macro_style(brackets)]
macro_rules! vec { () => {} }
fn main() { vec![$0] }
"#,
);
check_edit(
"foo!",
r#"
#[rust_analyzer::macro_style(braces)]
macro_rules! foo { () => {} }
fn main() { $0 }
"#,
r#"
#[rust_analyzer::macro_style(braces)]
macro_rules! foo { () => {} }
fn main() { foo! {$0} }
"#,
);
check_edit(
"bar!",
r#"
#[macro_export]
#[rust_analyzer::macro_style(brackets)]
macro_rules! foo { () => {} }
pub use crate::foo as bar;
fn main() { $0 }
"#,
r#"
#[macro_export]
#[rust_analyzer::macro_style(brackets)]
macro_rules! foo { () => {} }
pub use crate::foo as bar;
fn main() { bar![$0] }
"#,
);
}
#[test]
fn guesses_macro_braces() {
check_edit(

View file

@ -79,6 +79,7 @@ fn macro_fuzzy_completion() {
r#"
//- /lib.rs crate:dep
/// Please call me as macro_with_curlies! {}
#[rust_analyzer::macro_style(braces)]
#[macro_export]
macro_rules! macro_with_curlies {
() => {}