From 7e8079fbada3235f666d5457b42ff5f1e84ef6ea Mon Sep 17 00:00:00 2001 From: Ryan Mehri Date: Mon, 2 Jun 2025 17:17:27 -0400 Subject: [PATCH] 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)