Skip to content

Commit 4c34909

Browse files
bors[bot]Veykril
andauthored
Merge #11293
11293: feat: Add very simplistic ident completion for format_args! macro input r=Veykril a=Veykril Co-authored-by: Lukas Wirth <[email protected]>
2 parents 9ee5b89 + 82fccb9 commit 4c34909

File tree

8 files changed

+163
-39
lines changed

8 files changed

+163
-39
lines changed

crates/hir/src/source_analyzer.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
//!
66
//! So, this modules should not be used during hir construction, it exists
77
//! purely for "IDE needs".
8-
use std::{iter::once, sync::Arc};
8+
use std::{
9+
iter::{self, once},
10+
sync::Arc,
11+
};
912

1013
use hir_def::{
1114
body::{
@@ -25,7 +28,7 @@ use hir_ty::{
2528
};
2629
use syntax::{
2730
ast::{self, AstNode},
28-
SyntaxNode, TextRange, TextSize,
31+
SyntaxKind, SyntaxNode, TextRange, TextSize,
2932
};
3033

3134
use crate::{
@@ -488,14 +491,20 @@ fn scope_for_offset(
488491
.scope_by_expr()
489492
.iter()
490493
.filter_map(|(id, scope)| {
491-
let source = source_map.expr_syntax(*id).ok()?;
492-
// FIXME: correctly handle macro expansion
493-
if source.file_id != offset.file_id {
494-
return None;
494+
let InFile { file_id, value } = source_map.expr_syntax(*id).ok()?;
495+
if offset.file_id == file_id {
496+
let root = db.parse_or_expand(file_id)?;
497+
let node = value.to_node(&root);
498+
return Some((node.syntax().text_range(), scope));
495499
}
496-
let root = source.file_syntax(db.upcast());
497-
let node = source.value.to_node(&root);
498-
Some((node.syntax().text_range(), scope))
500+
501+
// FIXME handle attribute expansion
502+
let source = iter::successors(file_id.call_node(db.upcast()), |it| {
503+
it.file_id.call_node(db.upcast())
504+
})
505+
.find(|it| it.file_id == offset.file_id)
506+
.filter(|it| it.value.kind() == SyntaxKind::MACRO_CALL)?;
507+
Some((source.value.text_range(), scope))
499508
})
500509
// find containing scope
501510
.min_by_key(|(expr_range, _scope)| {

crates/ide/src/syntax_highlighting/format.rs

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//! Syntax highlighting for format macro strings.
2-
use ide_db::SymbolKind;
2+
use ide_db::{helpers::format_string::is_format_string, SymbolKind};
33
use syntax::{
44
ast::{self, FormatSpecifier, HasFormatSpecifier},
5-
AstNode, AstToken, TextRange,
5+
TextRange,
66
};
77

88
use crate::{syntax_highlighting::highlights::Highlights, HlRange, HlTag};
@@ -13,7 +13,7 @@ pub(super) fn highlight_format_string(
1313
expanded_string: &ast::String,
1414
range: TextRange,
1515
) {
16-
if is_format_string(expanded_string).is_none() {
16+
if !is_format_string(expanded_string) {
1717
return;
1818
}
1919

@@ -28,32 +28,6 @@ pub(super) fn highlight_format_string(
2828
});
2929
}
3030

31-
fn is_format_string(string: &ast::String) -> Option<()> {
32-
// Check if `string` is a format string argument of a macro invocation.
33-
// `string` is a string literal, mapped down into the innermost macro expansion.
34-
// Since `format_args!` etc. remove the format string when expanding, but place all arguments
35-
// in the expanded output, we know that the string token is (part of) the format string if it
36-
// appears in `format_args!` (otherwise it would have been mapped down further).
37-
//
38-
// This setup lets us correctly highlight the components of `concat!("{}", "bla")` format
39-
// strings. It still fails for `concat!("{", "}")`, but that is rare.
40-
41-
let macro_call = string.syntax().ancestors().find_map(ast::MacroCall::cast)?;
42-
let name = macro_call.path()?.segment()?.name_ref()?;
43-
44-
if !matches!(
45-
name.text().as_str(),
46-
"format_args" | "format_args_nl" | "const_format_args" | "panic_2015" | "panic_2021"
47-
) {
48-
return None;
49-
}
50-
51-
// NB: we match against `panic_2015`/`panic_2021` here because they have a special-cased arm for
52-
// `"{}"`, which otherwise wouldn't get highlighted.
53-
54-
Some(())
55-
}
56-
5731
fn highlight_format_specifier(kind: FormatSpecifier) -> Option<HlTag> {
5832
Some(match kind {
5933
FormatSpecifier::Open

crates/ide_completion/src/completions.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub(crate) mod record;
1414
pub(crate) mod snippet;
1515
pub(crate) mod trait_impl;
1616
pub(crate) mod unqualified_path;
17+
pub(crate) mod format_string;
1718

1819
use std::iter;
1920

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//! Completes identifiers in format string literals.
2+
3+
use ide_db::helpers::format_string::is_format_string;
4+
use itertools::Itertools;
5+
use syntax::{ast, AstToken, TextRange, TextSize};
6+
7+
use crate::{context::CompletionContext, CompletionItem, CompletionItemKind, Completions};
8+
9+
/// Complete identifiers in format strings.
10+
pub(crate) fn format_string(acc: &mut Completions, ctx: &CompletionContext) {
11+
let string = match ast::String::cast(ctx.token.clone()) {
12+
Some(it) if is_format_string(&it) => it,
13+
_ => return,
14+
};
15+
let cursor = ctx.position.offset;
16+
let lit_start = ctx.token.text_range().start();
17+
let cursor_in_lit = cursor - lit_start;
18+
19+
let prefix = &string.text()[..cursor_in_lit.into()];
20+
let braces = prefix.char_indices().rev().skip_while(|&(_, c)| c.is_alphanumeric()).next_tuple();
21+
let brace_offset = match braces {
22+
// escaped brace
23+
Some(((_, '{'), (_, '{'))) => return,
24+
Some(((idx, '{'), _)) => lit_start + TextSize::from(idx as u32 + 1),
25+
_ => return,
26+
};
27+
28+
let source_range = TextRange::new(brace_offset, cursor);
29+
ctx.locals.iter().for_each(|(name, _)| {
30+
CompletionItem::new(CompletionItemKind::Binding, source_range, name.to_smol_str())
31+
.add_to(acc);
32+
})
33+
}
34+
35+
#[cfg(test)]
36+
mod tests {
37+
use expect_test::{expect, Expect};
38+
39+
use crate::tests::{check_edit, completion_list_no_kw};
40+
41+
fn check(ra_fixture: &str, expect: Expect) {
42+
let actual = completion_list_no_kw(ra_fixture);
43+
expect.assert_eq(&actual);
44+
}
45+
46+
#[test]
47+
fn no_completion_without_brace() {
48+
check(
49+
r#"
50+
macro_rules! format_args {
51+
($lit:literal $(tt:tt)*) => { 0 },
52+
}
53+
fn main() {
54+
let foobar = 1;
55+
format_args!("f$0");
56+
}
57+
"#,
58+
expect![[]],
59+
);
60+
}
61+
62+
#[test]
63+
fn completes_locals() {
64+
check_edit(
65+
"foobar",
66+
r#"
67+
macro_rules! format_args {
68+
($lit:literal $(tt:tt)*) => { 0 },
69+
}
70+
fn main() {
71+
let foobar = 1;
72+
format_args!("{f$0");
73+
}
74+
"#,
75+
r#"
76+
macro_rules! format_args {
77+
($lit:literal $(tt:tt)*) => { 0 },
78+
}
79+
fn main() {
80+
let foobar = 1;
81+
format_args!("{foobar");
82+
}
83+
"#,
84+
);
85+
check_edit(
86+
"foobar",
87+
r#"
88+
macro_rules! format_args {
89+
($lit:literal $(tt:tt)*) => { 0 },
90+
}
91+
fn main() {
92+
let foobar = 1;
93+
format_args!("{$0");
94+
}
95+
"#,
96+
r#"
97+
macro_rules! format_args {
98+
($lit:literal $(tt:tt)*) => { 0 },
99+
}
100+
fn main() {
101+
let foobar = 1;
102+
format_args!("{foobar");
103+
}
104+
"#,
105+
);
106+
}
107+
}

crates/ide_completion/src/completions/postfix.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ pub(crate) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
179179
}
180180

181181
postfix_snippet("box", "Box::new(expr)", &format!("Box::new({})", receiver_text)).add_to(acc);
182-
postfix_snippet("dbg", "dbg!(expr)", &format!("dbg!({})", receiver_text)).add_to(acc);
182+
postfix_snippet("dbg", "dbg!(expr)", &format!("dbg!({})", receiver_text)).add_to(acc); // fixme
183183
postfix_snippet("dbgr", "dbg!(&expr)", &format!("dbg!(&{})", receiver_text)).add_to(acc);
184184
postfix_snippet("call", "function(expr)", &format!("${{1}}({})", receiver_text)).add_to(acc);
185185

crates/ide_completion/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ pub fn completions(
168168
completions::flyimport::import_on_the_fly(&mut acc, &ctx);
169169
completions::lifetime::complete_lifetime(&mut acc, &ctx);
170170
completions::lifetime::complete_label(&mut acc, &ctx);
171+
completions::format_string::format_string(&mut acc, &ctx);
171172

172173
Some(acc)
173174
}

crates/ide_db/src/helpers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod merge_imports;
77
pub mod insert_whitespace_into_node;
88
pub mod node_ext;
99
pub mod rust_doc;
10+
pub mod format_string;
1011

1112
use std::{collections::VecDeque, iter};
1213

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//! Tools to work with format string literals for the `format_args!` family of macros.
2+
use syntax::{ast, AstNode, AstToken};
3+
4+
pub fn is_format_string(string: &ast::String) -> bool {
5+
// Check if `string` is a format string argument of a macro invocation.
6+
// `string` is a string literal, mapped down into the innermost macro expansion.
7+
// Since `format_args!` etc. remove the format string when expanding, but place all arguments
8+
// in the expanded output, we know that the string token is (part of) the format string if it
9+
// appears in `format_args!` (otherwise it would have been mapped down further).
10+
//
11+
// This setup lets us correctly highlight the components of `concat!("{}", "bla")` format
12+
// strings. It still fails for `concat!("{", "}")`, but that is rare.
13+
14+
(|| {
15+
let macro_call = string.syntax().ancestors().find_map(ast::MacroCall::cast)?;
16+
let name = macro_call.path()?.segment()?.name_ref()?;
17+
18+
if !matches!(
19+
name.text().as_str(),
20+
"format_args" | "format_args_nl" | "const_format_args" | "panic_2015" | "panic_2021"
21+
) {
22+
return None;
23+
}
24+
25+
// NB: we match against `panic_2015`/`panic_2021` here because they have a special-cased arm for
26+
// `"{}"`, which otherwise wouldn't get highlighted.
27+
28+
Some(())
29+
})()
30+
.is_some()
31+
}

0 commit comments

Comments
 (0)