Unnamed repository; edit this file 'description' to name the repository.
Merge pull request #20741 from A4-Tacks/comp-macro-segment
feat: Add macro segment completion
Lukas Wirth 4 months ago
parent f9e29cf · parent b5baf71 · commit e08a786
-rw-r--r--crates/ide-completion/src/completions.rs1
-rw-r--r--crates/ide-completion/src/completions/macro_def.rs31
-rw-r--r--crates/ide-completion/src/context.rs5
-rw-r--r--crates/ide-completion/src/context/analysis.rs17
-rw-r--r--crates/ide-completion/src/lib.rs3
-rw-r--r--crates/ide-completion/src/tests/special.rs229
-rw-r--r--crates/ide-db/src/syntax_helpers/node_ext.rs39
7 files changed, 321 insertions, 4 deletions
diff --git a/crates/ide-completion/src/completions.rs b/crates/ide-completion/src/completions.rs
index b28764f6fc..355687b203 100644
--- a/crates/ide-completion/src/completions.rs
+++ b/crates/ide-completion/src/completions.rs
@@ -13,6 +13,7 @@ pub(crate) mod format_string;
pub(crate) mod item_list;
pub(crate) mod keyword;
pub(crate) mod lifetime;
+pub(crate) mod macro_def;
pub(crate) mod mod_;
pub(crate) mod pattern;
pub(crate) mod postfix;
diff --git a/crates/ide-completion/src/completions/macro_def.rs b/crates/ide-completion/src/completions/macro_def.rs
new file mode 100644
index 0000000000..2c8e7a2e62
--- /dev/null
+++ b/crates/ide-completion/src/completions/macro_def.rs
@@ -0,0 +1,31 @@
+//! Completion for macro meta-variable segments
+
+use ide_db::SymbolKind;
+
+use crate::{CompletionItem, Completions, context::CompletionContext};
+
+pub(crate) fn complete_macro_segment(acc: &mut Completions, ctx: &CompletionContext<'_>) {
+ for &label in MACRO_SEGMENTS {
+ let item =
+ CompletionItem::new(SymbolKind::BuiltinAttr, ctx.source_range(), label, ctx.edition);
+ item.add_to(acc, ctx.db);
+ }
+}
+
+const MACRO_SEGMENTS: &[&str] = &[
+ "ident",
+ "block",
+ "stmt",
+ "expr",
+ "pat",
+ "ty",
+ "lifetime",
+ "literal",
+ "path",
+ "meta",
+ "tt",
+ "item",
+ "vis",
+ "expr_2021",
+ "pat_param",
+];
diff --git a/crates/ide-completion/src/context.rs b/crates/ide-completion/src/context.rs
index 963e396704..d116f665ad 100644
--- a/crates/ide-completion/src/context.rs
+++ b/crates/ide-completion/src/context.rs
@@ -13,7 +13,7 @@ use hir::{
};
use ide_db::{
FilePosition, FxHashMap, FxHashSet, RootDatabase, famous_defs::FamousDefs,
- helpers::is_editable_crate,
+ helpers::is_editable_crate, syntax_helpers::node_ext::is_in_macro_matcher,
};
use itertools::Either;
use syntax::{
@@ -389,6 +389,7 @@ pub(crate) enum CompletionAnalysis<'db> {
fake_attribute_under_caret: Option<ast::Attr>,
extern_crate: Option<ast::ExternCrate>,
},
+ MacroSegment,
}
/// Information about the field or method access we are completing.
@@ -729,7 +730,7 @@ impl<'db> CompletionContext<'db> {
let prev_token = original_token.prev_token()?;
// only has a single colon
- if prev_token.kind() != T![:] {
+ if prev_token.kind() != T![:] && !is_in_macro_matcher(&original_token) {
return None;
}
diff --git a/crates/ide-completion/src/context/analysis.rs b/crates/ide-completion/src/context/analysis.rs
index 49fb36ad04..65bae5b66e 100644
--- a/crates/ide-completion/src/context/analysis.rs
+++ b/crates/ide-completion/src/context/analysis.rs
@@ -5,7 +5,7 @@ use hir::{ExpandResult, InFile, Semantics, Type, TypeInfo, Variant};
use ide_db::{
RootDatabase, active_parameter::ActiveParameter, syntax_helpers::node_ext::find_loops,
};
-use itertools::Either;
+use itertools::{Either, Itertools};
use stdx::always;
use syntax::{
AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken,
@@ -510,6 +510,21 @@ fn analyze<'db>(
colon_prefix,
extern_crate: p.ancestors().find_map(ast::ExternCrate::cast),
}
+ } else if p.kind() == SyntaxKind::TOKEN_TREE
+ && p.ancestors().any(|it| ast::Macro::can_cast(it.kind()))
+ {
+ if let Some([_ident, colon, _name, dollar]) = fake_ident_token
+ .siblings_with_tokens(Direction::Prev)
+ .filter(|it| !it.kind().is_trivia())
+ .take(4)
+ .collect_array()
+ && dollar.kind() == T![$]
+ && colon.kind() == T![:]
+ {
+ CompletionAnalysis::MacroSegment
+ } else {
+ return None;
+ }
} else {
return None;
}
diff --git a/crates/ide-completion/src/lib.rs b/crates/ide-completion/src/lib.rs
index 33ab43fa61..69ca2af772 100644
--- a/crates/ide-completion/src/lib.rs
+++ b/crates/ide-completion/src/lib.rs
@@ -263,6 +263,9 @@ pub fn completions(
extern_crate.as_ref(),
);
}
+ CompletionAnalysis::MacroSegment => {
+ completions::macro_def::complete_macro_segment(acc, ctx);
+ }
CompletionAnalysis::UnexpandedAttrTT { .. } | CompletionAnalysis::String { .. } => (),
}
}
diff --git a/crates/ide-completion/src/tests/special.rs b/crates/ide-completion/src/tests/special.rs
index 59a0c144c8..b82b23541c 100644
--- a/crates/ide-completion/src/tests/special.rs
+++ b/crates/ide-completion/src/tests/special.rs
@@ -482,6 +482,226 @@ fn foo() {}
}
#[test]
+fn completes_macro_segment() {
+ check(
+ r#"
+macro_rules! foo {
+ ($x:e$0) => ();
+}
+"#,
+ expect![[r#"
+ ba block
+ ba expr
+ ba expr_2021
+ ba ident
+ ba item
+ ba lifetime
+ ba literal
+ ba meta
+ ba pat
+ ba pat_param
+ ba path
+ ba stmt
+ ba tt
+ ba ty
+ ba vis
+ "#]],
+ );
+
+ check(
+ r#"
+macro_rules! foo {
+ ($x:$0) => ();
+}
+"#,
+ expect![[r#"
+ ba block
+ ba expr
+ ba expr_2021
+ ba ident
+ ba item
+ ba lifetime
+ ba literal
+ ba meta
+ ba pat
+ ba pat_param
+ ba path
+ ba stmt
+ ba tt
+ ba ty
+ ba vis
+ "#]],
+ );
+
+ check(
+ r#"
+macro_rules! foo {
+ ($($x:$0)*) => ();
+}
+"#,
+ expect![[r#"
+ ba block
+ ba expr
+ ba expr_2021
+ ba ident
+ ba item
+ ba lifetime
+ ba literal
+ ba meta
+ ba pat
+ ba pat_param
+ ba path
+ ba stmt
+ ba tt
+ ba ty
+ ba vis
+ "#]],
+ );
+
+ check(
+ r#"
+macro foo {
+ ($($x:$0)*) => ();
+}
+"#,
+ expect![[r#"
+ ba block
+ ba expr
+ ba expr_2021
+ ba ident
+ ba item
+ ba lifetime
+ ba literal
+ ba meta
+ ba pat
+ ba pat_param
+ ba path
+ ba stmt
+ ba tt
+ ba ty
+ ba vis
+ "#]],
+ );
+
+ check(
+ r#"
+macro foo($($x:$0)*) {
+ xxx;
+}
+"#,
+ expect![[r#"
+ ba block
+ ba expr
+ ba expr_2021
+ ba ident
+ ba item
+ ba lifetime
+ ba literal
+ ba meta
+ ba pat
+ ba pat_param
+ ba path
+ ba stmt
+ ba tt
+ ba ty
+ ba vis
+ "#]],
+ );
+
+ check_edit(
+ "expr",
+ r#"
+macro foo($($x:$0)*) {
+ xxx;
+}
+"#,
+ r#"
+macro foo($($x:expr)*) {
+ xxx;
+}
+"#,
+ );
+
+ check(
+ r#"
+macro_rules! foo {
+ ($fn : e$0) => ();
+}
+"#,
+ expect![[r#"
+ ba block
+ ba expr
+ ba expr_2021
+ ba ident
+ ba item
+ ba lifetime
+ ba literal
+ ba meta
+ ba pat
+ ba pat_param
+ ba path
+ ba stmt
+ ba tt
+ ba ty
+ ba vis
+ "#]],
+ );
+
+ check_edit(
+ "expr",
+ r#"
+macro foo($($x:ex$0)*) {
+ xxx;
+}
+"#,
+ r#"
+macro foo($($x:expr)*) {
+ xxx;
+}
+"#,
+ );
+}
+
+#[test]
+fn completes_in_macro_body() {
+ check(
+ r#"
+macro_rules! foo {
+ ($x:expr) => ($y:$0);
+}
+"#,
+ expect![[r#""#]],
+ );
+
+ check(
+ r#"
+macro_rules! foo {
+ ($x:expr) => ({$y:$0});
+}
+"#,
+ expect![[r#""#]],
+ );
+
+ check(
+ r#"
+macro foo {
+ ($x:expr) => ($y:$0);
+}
+"#,
+ expect![[r#""#]],
+ );
+
+ check(
+ r#"
+macro foo($x:expr) {
+ $y:$0
+}
+"#,
+ expect![[r#""#]],
+ );
+}
+
+#[test]
fn function_mod_share_name() {
check_no_kw(
r#"
@@ -946,6 +1166,15 @@ fn foo { crate:$0 }
Some(':'),
expect![""],
);
+
+ check_with_trigger_character(
+ r#"
+macro_rules! bar { ($($x:tt)*) => ($($x)*); }
+fn foo { bar!(crate:$0) }
+"#,
+ Some(':'),
+ expect![""],
+ );
}
#[test]
diff --git a/crates/ide-db/src/syntax_helpers/node_ext.rs b/crates/ide-db/src/syntax_helpers/node_ext.rs
index acce066b83..94ecf6a02d 100644
--- a/crates/ide-db/src/syntax_helpers/node_ext.rs
+++ b/crates/ide-db/src/syntax_helpers/node_ext.rs
@@ -1,12 +1,15 @@
//! Various helper functions to work with SyntaxNodes.
use std::ops::ControlFlow;
+use either::Either;
use itertools::Itertools;
use parser::T;
use span::Edition;
use syntax::{
- AstNode, AstToken, Preorder, RustLanguage, WalkEvent,
+ AstNode, AstToken, Direction, Preorder, RustLanguage, SyntaxToken, WalkEvent,
+ algo::non_trivia_sibling,
ast::{self, HasLoopBody, MacroCall, PathSegmentKind, VisibilityKind},
+ syntax_editor::Element,
};
pub fn expr_as_name_ref(expr: &ast::Expr) -> Option<ast::NameRef> {
@@ -542,3 +545,37 @@ pub fn macro_call_for_string_token(string: &ast::String) -> Option<MacroCall> {
let macro_call = string.syntax().parent_ancestors().find_map(ast::MacroCall::cast)?;
Some(macro_call)
}
+
+pub fn is_in_macro_matcher(token: &SyntaxToken) -> bool {
+ let Some(macro_def) = token
+ .parent_ancestors()
+ .map_while(Either::<ast::TokenTree, ast::Macro>::cast)
+ .find_map(Either::right)
+ else {
+ return false;
+ };
+ let range = token.text_range();
+ let Some(body) = (match macro_def {
+ ast::Macro::MacroDef(macro_def) => {
+ if let Some(args) = macro_def.args() {
+ return args.syntax().text_range().contains_range(range);
+ }
+ macro_def.body()
+ }
+ ast::Macro::MacroRules(macro_rules) => macro_rules.token_tree(),
+ }) else {
+ return false;
+ };
+ if !body.syntax().text_range().contains_range(range) {
+ return false;
+ }
+ body.token_trees_and_tokens().filter_map(|tt| tt.into_node()).any(|tt| {
+ let Some(next) = non_trivia_sibling(tt.syntax().syntax_element(), Direction::Next) else {
+ return false;
+ };
+ let Some(next_next) = next.next_sibling_or_token() else { return false };
+ next.kind() == T![=]
+ && next_next.kind() == T![>]
+ && tt.syntax().text_range().contains_range(range)
+ })
+}