From fb921c30396e5f3b8201143672f8d76133fdf84a Mon Sep 17 00:00:00 2001 From: Ryan Mehri Date: Mon, 2 Jun 2025 15:35:13 -0400 Subject: [PATCH 1/4] fix: don't duplicate must_use for functions and traits --- crates/ide-completion/src/completions/attribute.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ide-completion/src/completions/attribute.rs b/crates/ide-completion/src/completions/attribute.rs index 3c195f80fe..352e4444b7 100644 --- a/crates/ide-completion/src/completions/attribute.rs +++ b/crates/ide-completion/src/completions/attribute.rs @@ -264,13 +264,13 @@ static KIND_TO_ATTRIBUTES: LazyLock> = LazyLock:: FN, attrs!( item, linkable, - "cold", "ignore", "inline", "must_use", "panic_handler", "proc_macro", + "cold", "ignore", "inline", "panic_handler", "proc_macro", "proc_macro_derive", "proc_macro_attribute", "should_panic", "target_feature", "test", "track_caller" ), ), (STATIC, attrs!(item, linkable, "global_allocator", "used")), - (TRAIT, attrs!(item, "must_use")), + (TRAIT, attrs!(item)), (IMPL, attrs!(item, "automatically_derived")), (ASSOC_ITEM_LIST, attrs!(item)), (EXTERN_BLOCK, attrs!(item, "link")), From 7e8079fbada3235f666d5457b42ff5f1e84ef6ea Mon Sep 17 00:00:00 2001 From: Ryan Mehri Date: Mon, 2 Jun 2025 17:17:27 -0400 Subject: [PATCH 2/4] feat: implement completion for diagnostic module --- .../src/completions/attribute.rs | 54 +++++++++---- .../src/completions/attribute/diagnostic.rs | 60 ++++++++++++++ crates/ide-completion/src/tests/attribute.rs | 78 ++++++++++++++++++- 3 files changed, 175 insertions(+), 17 deletions(-) create mode 100644 crates/ide-completion/src/completions/attribute/diagnostic.rs diff --git a/crates/ide-completion/src/completions/attribute.rs b/crates/ide-completion/src/completions/attribute.rs index 352e4444b7..f696f0c0b9 100644 --- a/crates/ide-completion/src/completions/attribute.rs +++ b/crates/ide-completion/src/completions/attribute.rs @@ -25,6 +25,7 @@ use crate::{ mod cfg; mod derive; +mod diagnostic; mod lint; mod macro_use; mod repr; @@ -40,14 +41,23 @@ pub(crate) fn complete_known_attribute_input( extern_crate: Option<&ast::ExternCrate>, ) -> Option<()> { let attribute = fake_attribute_under_caret; - let name_ref = match attribute.path() { - Some(p) => Some(p.as_single_name_ref()?), - None => None, - }; - let (path, tt) = name_ref.zip(attribute.token_tree())?; + let path = attribute.path()?; + let name_ref = path.segment()?.name_ref(); + let (name_ref, tt) = name_ref.zip(attribute.token_tree())?; tt.l_paren_token()?; - match path.text().as_str() { + if let Some(qualifier) = path.qualifier() { + let qualifier_name_ref = qualifier.as_single_name_ref()?; + match (qualifier_name_ref.text().as_str(), name_ref.text().as_str()) { + ("diagnostic", "on_unimplemented") => { + diagnostic::complete_on_unimplemented(acc, ctx, tt) + } + _ => (), + } + return Some(()); + } + + match name_ref.text().as_str() { "repr" => repr::complete_repr(acc, ctx, tt), "feature" => lint::complete_lint( acc, @@ -139,6 +149,8 @@ pub(crate) fn complete_attribute_path( } Qualified::TypeAnchor { .. } | Qualified::With { .. } => {} } + let qualifier_path = + if let Qualified::With { path, .. } = qualified { Some(path) } else { None }; let attributes = annotated_item_kind.and_then(|kind| { if ast::Expr::can_cast(kind) { @@ -149,18 +161,28 @@ pub(crate) fn complete_attribute_path( }); let add_completion = |attr_completion: &AttrCompletion| { - let mut item = CompletionItem::new( - SymbolKind::Attribute, - ctx.source_range(), - attr_completion.label, - ctx.edition, - ); + // if we already have the qualifier of the completion, then trim it from the label and the snippet + let mut label = attr_completion.label; + let mut snippet = attr_completion.snippet; + if let Some(name_ref) = qualifier_path.and_then(|q| q.as_single_name_ref()) { + if let Some((label_qual, label_seg)) = attr_completion.label.split_once("::") { + if name_ref.text() == label_qual { + label = label_seg; + snippet = snippet.map(|snippet| { + snippet.trim_start_matches(label_qual).trim_start_matches("::") + }); + } + } + } + + let mut item = + CompletionItem::new(SymbolKind::Attribute, ctx.source_range(), label, ctx.edition); if let Some(lookup) = attr_completion.lookup { item.lookup_by(lookup); } - if let Some((snippet, cap)) = attr_completion.snippet.zip(ctx.config.snippet_cap) { + if let Some((snippet, cap)) = snippet.zip(ctx.config.snippet_cap) { item.insert_snippet(cap, snippet); } @@ -270,8 +292,8 @@ static KIND_TO_ATTRIBUTES: LazyLock> = LazyLock:: ), ), (STATIC, attrs!(item, linkable, "global_allocator", "used")), - (TRAIT, attrs!(item)), - (IMPL, attrs!(item, "automatically_derived")), + (TRAIT, attrs!(item, "diagnostic::on_unimplemented")), + (IMPL, attrs!(item, "automatically_derived", "diagnostic::do_not_recommend")), (ASSOC_ITEM_LIST, attrs!(item)), (EXTERN_BLOCK, attrs!(item, "link")), (EXTERN_ITEM_LIST, attrs!(item, "link")), @@ -311,6 +333,8 @@ const ATTRIBUTES: &[AttrCompletion] = &[ attr("deny(…)", Some("deny"), Some("deny(${0:lint})")), attr(r#"deprecated"#, Some("deprecated"), Some(r#"deprecated"#)), attr("derive(…)", Some("derive"), Some(r#"derive(${0:Debug})"#)), + attr("diagnostic::do_not_recommend", None, None), + attr("diagnostic::on_unimplemented", None, Some(r#"diagnostic::on_unimplemented(${0:keys})"#)), attr(r#"doc = "…""#, Some("doc"), Some(r#"doc = "${0:docs}""#)), attr(r#"doc(alias = "…")"#, Some("docalias"), Some(r#"doc(alias = "${0:docs}")"#)), attr(r#"doc(hidden)"#, Some("dochidden"), Some(r#"doc(hidden)"#)), diff --git a/crates/ide-completion/src/completions/attribute/diagnostic.rs b/crates/ide-completion/src/completions/attribute/diagnostic.rs new file mode 100644 index 0000000000..10c5135b4b --- /dev/null +++ b/crates/ide-completion/src/completions/attribute/diagnostic.rs @@ -0,0 +1,60 @@ +//! Completion for diagnostic attributes. + +use ide_db::SymbolKind; +use syntax::ast::{self}; + +use crate::{CompletionItem, Completions, context::CompletionContext}; + +use super::AttrCompletion; + +pub(super) fn complete_on_unimplemented( + acc: &mut Completions, + ctx: &CompletionContext<'_>, + input: ast::TokenTree, +) { + if let Some(existing_keys) = super::parse_comma_sep_expr(input) { + for attr in ATTRIBUTES { + let already_annotated = existing_keys + .iter() + .filter_map(|expr| match expr { + ast::Expr::PathExpr(path) => path.path()?.as_single_name_ref(), + ast::Expr::BinExpr(bin) + if bin.op_kind() == Some(ast::BinaryOp::Assignment { op: None }) => + { + match bin.lhs()? { + ast::Expr::PathExpr(path) => path.path()?.as_single_name_ref(), + _ => None, + } + } + _ => None, + }) + .any(|it| { + let text = it.text(); + attr.key() == text && text != "note" + }); + if already_annotated { + continue; + } + + let mut item = CompletionItem::new( + SymbolKind::BuiltinAttr, + ctx.source_range(), + attr.label, + ctx.edition, + ); + if let Some(lookup) = attr.lookup { + item.lookup_by(lookup); + } + if let Some((snippet, cap)) = attr.snippet.zip(ctx.config.snippet_cap) { + item.insert_snippet(cap, snippet); + } + item.add_to(acc, ctx.db); + } + } +} + +const ATTRIBUTES: &[AttrCompletion] = &[ + super::attr(r#"label = "…""#, Some("label"), Some(r#"label = "${0:label}""#)), + super::attr(r#"message = "…""#, Some("message"), Some(r#"message = "${0:message}""#)), + super::attr(r#"note = "…""#, Some("note"), Some(r#"note = "${0:note}""#)), +]; diff --git a/crates/ide-completion/src/tests/attribute.rs b/crates/ide-completion/src/tests/attribute.rs index 32d3b50f23..411902f111 100644 --- a/crates/ide-completion/src/tests/attribute.rs +++ b/crates/ide-completion/src/tests/attribute.rs @@ -30,6 +30,8 @@ pub struct Foo(#[m$0] i32); at deprecated at derive macro derive at derive(…) + at diagnostic::do_not_recommend + at diagnostic::on_unimplemented at doc = "…" at doc(alias = "…") at doc(hidden) @@ -472,13 +474,13 @@ fn attr_on_trait() { at cfg_attr(…) at deny(…) at deprecated + at diagnostic::on_unimplemented at doc = "…" at doc(alias = "…") at doc(hidden) at expect(…) at forbid(…) at must_use - at must_use at no_mangle at warn(…) kw crate:: @@ -498,6 +500,7 @@ fn attr_on_impl() { at cfg_attr(…) at deny(…) at deprecated + at diagnostic::do_not_recommend at doc = "…" at doc(alias = "…") at doc(hidden) @@ -532,6 +535,76 @@ fn attr_on_impl() { ); } +#[test] +fn attr_with_qualifier() { + check( + r#"#[diagnostic::$0] impl () {}"#, + expect![[r#" + at allow(…) + at automatically_derived + at cfg(…) + at cfg_attr(…) + at deny(…) + at deprecated + at do_not_recommend + at doc = "…" + at doc(alias = "…") + at doc(hidden) + at expect(…) + at forbid(…) + at must_use + at no_mangle + at warn(…) + "#]], + ); + check( + r#"#[diagnostic::$0] trait Foo {}"#, + expect![[r#" + at allow(…) + at cfg(…) + at cfg_attr(…) + at deny(…) + at deprecated + at doc = "…" + at doc(alias = "…") + at doc(hidden) + at expect(…) + at forbid(…) + at must_use + at no_mangle + at on_unimplemented + at warn(…) + "#]], + ); +} + +#[test] +fn attr_diagnostic_on_unimplemented() { + check( + r#"#[diagnostic::on_unimplemented($0)] trait Foo {}"#, + expect![[r#" + ba label = "…" + ba message = "…" + ba note = "…" + "#]], + ); + check( + r#"#[diagnostic::on_unimplemented(message = "foo", $0)] trait Foo {}"#, + expect![[r#" + ba label = "…" + ba note = "…" + "#]], + ); + check( + r#"#[diagnostic::on_unimplemented(note = "foo", $0)] trait Foo {}"#, + expect![[r#" + ba label = "…" + ba message = "…" + ba note = "…" + "#]], + ); +} + #[test] fn attr_on_extern_block() { check( @@ -619,7 +692,6 @@ fn attr_on_fn() { at link_name = "…" at link_section = "…" at must_use - at must_use at no_mangle at panic_handler at proc_macro @@ -649,6 +721,8 @@ fn attr_in_source_file_end() { at deny(…) at deprecated at derive(…) + at diagnostic::do_not_recommend + at diagnostic::on_unimplemented at doc = "…" at doc(alias = "…") at doc(hidden) From f1cc7c5c92383621c49225431d33e53cb578fdf0 Mon Sep 17 00:00:00 2001 From: Ryan Mehri Date: Wed, 4 Jun 2025 08:30:27 -0400 Subject: [PATCH 3/4] match on segments of path and some small cleanup --- .../src/completions/attribute.rs | 31 +++++++------------ .../src/completions/attribute/diagnostic.rs | 6 ++-- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/crates/ide-completion/src/completions/attribute.rs b/crates/ide-completion/src/completions/attribute.rs index f696f0c0b9..0ca90fae4e 100644 --- a/crates/ide-completion/src/completions/attribute.rs +++ b/crates/ide-completion/src/completions/attribute.rs @@ -42,31 +42,21 @@ pub(crate) fn complete_known_attribute_input( ) -> Option<()> { let attribute = fake_attribute_under_caret; let path = attribute.path()?; - let name_ref = path.segment()?.name_ref(); - let (name_ref, tt) = name_ref.zip(attribute.token_tree())?; - tt.l_paren_token()?; + let segments = path.segments().map(|s| s.name_ref()).collect::>>()?; + let segments = segments.iter().map(|n| n.text()).collect::>(); + let segments = segments.iter().map(|t| t.as_str()).collect::>(); + let tt = attribute.token_tree()?; - if let Some(qualifier) = path.qualifier() { - let qualifier_name_ref = qualifier.as_single_name_ref()?; - match (qualifier_name_ref.text().as_str(), name_ref.text().as_str()) { - ("diagnostic", "on_unimplemented") => { - diagnostic::complete_on_unimplemented(acc, ctx, tt) - } - _ => (), - } - return Some(()); - } - - match name_ref.text().as_str() { - "repr" => repr::complete_repr(acc, ctx, tt), - "feature" => lint::complete_lint( + match segments.as_slice() { + ["repr"] => repr::complete_repr(acc, ctx, tt), + ["feature"] => lint::complete_lint( acc, ctx, colon_prefix, &parse_tt_as_comma_sep_paths(tt, ctx.edition)?, FEATURES, ), - "allow" | "expect" | "deny" | "forbid" | "warn" => { + ["allow"] | ["expect"] | ["deny"] | ["forbid"] | ["warn"] => { let existing_lints = parse_tt_as_comma_sep_paths(tt, ctx.edition)?; let lints: Vec = CLIPPY_LINT_GROUPS @@ -80,13 +70,14 @@ pub(crate) fn complete_known_attribute_input( lint::complete_lint(acc, ctx, colon_prefix, &existing_lints, &lints); } - "cfg" => cfg::complete_cfg(acc, ctx), - "macro_use" => macro_use::complete_macro_use( + ["cfg"] => cfg::complete_cfg(acc, ctx), + ["macro_use"] => macro_use::complete_macro_use( acc, ctx, extern_crate, &parse_tt_as_comma_sep_paths(tt, ctx.edition)?, ), + ["diagnostic", "on_unimplemented"] => diagnostic::complete_on_unimplemented(acc, ctx, tt), _ => (), } Some(()) diff --git a/crates/ide-completion/src/completions/attribute/diagnostic.rs b/crates/ide-completion/src/completions/attribute/diagnostic.rs index 10c5135b4b..8adc974239 100644 --- a/crates/ide-completion/src/completions/attribute/diagnostic.rs +++ b/crates/ide-completion/src/completions/attribute/diagnostic.rs @@ -1,7 +1,7 @@ //! Completion for diagnostic attributes. use ide_db::SymbolKind; -use syntax::ast::{self}; +use syntax::ast; use crate::{CompletionItem, Completions, context::CompletionContext}; @@ -13,7 +13,7 @@ pub(super) fn complete_on_unimplemented( input: ast::TokenTree, ) { if let Some(existing_keys) = super::parse_comma_sep_expr(input) { - for attr in ATTRIBUTES { + for attr in ATTRIBUTE_ARGS { let already_annotated = existing_keys .iter() .filter_map(|expr| match expr { @@ -53,7 +53,7 @@ pub(super) fn complete_on_unimplemented( } } -const ATTRIBUTES: &[AttrCompletion] = &[ +const ATTRIBUTE_ARGS: &[AttrCompletion] = &[ super::attr(r#"label = "…""#, Some("label"), Some(r#"label = "${0:label}""#)), super::attr(r#"message = "…""#, Some("message"), Some(r#"message = "${0:message}""#)), super::attr(r#"note = "…""#, Some("note"), Some(r#"note = "${0:note}""#)), From b95101ce46b0c1710701ea40ea7494c67b5c555e Mon Sep 17 00:00:00 2001 From: Ryan Mehri Date: Wed, 4 Jun 2025 11:02:48 -0400 Subject: [PATCH 4/4] add qualifiers to attribute completions --- .../src/completions/attribute.rs | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/crates/ide-completion/src/completions/attribute.rs b/crates/ide-completion/src/completions/attribute.rs index 0ca90fae4e..705402c785 100644 --- a/crates/ide-completion/src/completions/attribute.rs +++ b/crates/ide-completion/src/completions/attribute.rs @@ -152,17 +152,22 @@ pub(crate) fn complete_attribute_path( }); let add_completion = |attr_completion: &AttrCompletion| { - // if we already have the qualifier of the completion, then trim it from the label and the snippet - let mut label = attr_completion.label; - let mut snippet = attr_completion.snippet; - if let Some(name_ref) = qualifier_path.and_then(|q| q.as_single_name_ref()) { - if let Some((label_qual, label_seg)) = attr_completion.label.split_once("::") { - if name_ref.text() == label_qual { - label = label_seg; - snippet = snippet.map(|snippet| { - snippet.trim_start_matches(label_qual).trim_start_matches("::") - }); - } + // if we don't already have the qualifiers of the completion, then + // add the missing parts to the label and snippet + let mut label = attr_completion.label.to_owned(); + let mut snippet = attr_completion.snippet.map(|s| s.to_owned()); + let segments = qualifier_path.iter().flat_map(|q| q.segments()).collect::>(); + let qualifiers = attr_completion.qualifiers; + let matching_qualifiers = segments + .iter() + .zip(qualifiers) + .take_while(|(s, q)| s.name_ref().is_some_and(|t| t.text() == **q)) + .count(); + if matching_qualifiers != qualifiers.len() { + let prefix = qualifiers[matching_qualifiers..].join("::"); + label = format!("{prefix}::{label}"); + if let Some(s) = snippet.as_mut() { + *s = format!("{prefix}::{s}"); } } @@ -197,6 +202,7 @@ struct AttrCompletion { label: &'static str, lookup: Option<&'static str>, snippet: Option<&'static str>, + qualifiers: &'static [&'static str], prefer_inner: bool, } @@ -205,6 +211,10 @@ impl AttrCompletion { self.lookup.unwrap_or(self.label) } + const fn qualifiers(self, qualifiers: &'static [&'static str]) -> AttrCompletion { + AttrCompletion { qualifiers, ..self } + } + const fn prefer_inner(self) -> AttrCompletion { AttrCompletion { prefer_inner: true, ..self } } @@ -215,7 +225,7 @@ const fn attr( lookup: Option<&'static str>, snippet: Option<&'static str>, ) -> AttrCompletion { - AttrCompletion { label, lookup, snippet, prefer_inner: false } + AttrCompletion { label, lookup, snippet, qualifiers: &[], prefer_inner: false } } macro_rules! attrs { @@ -324,8 +334,14 @@ const ATTRIBUTES: &[AttrCompletion] = &[ attr("deny(…)", Some("deny"), Some("deny(${0:lint})")), attr(r#"deprecated"#, Some("deprecated"), Some(r#"deprecated"#)), attr("derive(…)", Some("derive"), Some(r#"derive(${0:Debug})"#)), - attr("diagnostic::do_not_recommend", None, None), - attr("diagnostic::on_unimplemented", None, Some(r#"diagnostic::on_unimplemented(${0:keys})"#)), + attr("do_not_recommend", Some("diagnostic::do_not_recommend"), None) + .qualifiers(&["diagnostic"]), + attr( + "on_unimplemented", + Some("diagnostic::on_unimplemented"), + Some(r#"on_unimplemented(${0:keys})"#), + ) + .qualifiers(&["diagnostic"]), attr(r#"doc = "…""#, Some("doc"), Some(r#"doc = "${0:docs}""#)), attr(r#"doc(alias = "…")"#, Some("docalias"), Some(r#"doc(alias = "${0:docs}")"#)), attr(r#"doc(hidden)"#, Some("dochidden"), Some(r#"doc(hidden)"#)),