ide_completion/completions/
format_string.rs

1//! Completes identifiers in format string literals.
2
3use hir::{ModuleDef, ScopeDef};
4use ide_db::{SymbolKind, syntax_helpers::format_string::is_format_string};
5use itertools::Itertools;
6use syntax::{AstToken, TextRange, TextSize, ToSmolStr, ast};
7
8use crate::{CompletionItem, CompletionItemKind, Completions, context::CompletionContext};
9
10/// Complete identifiers in format strings.
11pub(crate) fn format_string(
12    acc: &mut Completions,
13    ctx: &CompletionContext<'_>,
14    original: &ast::String,
15    expanded: &ast::String,
16) {
17    if !is_format_string(expanded) {
18        return;
19    }
20    let cursor = ctx.position.offset;
21    let lit_start = ctx.original_token.text_range().start();
22    let cursor_in_lit = cursor - lit_start;
23
24    let prefix = &original.text()[..cursor_in_lit.into()];
25    let Some(brace_offset) = unescaped_brace(prefix) else { return };
26    let brace_offset = lit_start + brace_offset + TextSize::of('{');
27
28    let source_range = TextRange::new(brace_offset, cursor);
29    ctx.locals.iter().sorted_by_key(|&(k, _)| k.clone()).for_each(|(name, _)| {
30        CompletionItem::new(
31            CompletionItemKind::Binding,
32            source_range,
33            name.display_no_db(ctx.edition).to_smolstr(),
34            ctx.edition,
35        )
36        .add_to(acc, ctx.db);
37    });
38    ctx.scope.process_all_names(&mut |name, scope| {
39        if let ScopeDef::ModuleDef(module_def) = scope {
40            let symbol_kind = match module_def {
41                ModuleDef::Const(..) => SymbolKind::Const,
42                ModuleDef::Static(..) => SymbolKind::Static,
43                _ => return,
44            };
45
46            CompletionItem::new(
47                CompletionItemKind::SymbolKind(symbol_kind),
48                source_range,
49                name.display_no_db(ctx.edition).to_smolstr(),
50                ctx.edition,
51            )
52            .add_to(acc, ctx.db);
53        }
54    });
55}
56
57fn unescaped_brace(prefix: &str) -> Option<TextSize> {
58    let is_ident_char = |ch: char| ch.is_alphanumeric() || ch == '_';
59    prefix
60        .trim_end_matches(is_ident_char)
61        .strip_suffix('{')
62        .filter(|it| it.chars().rev().take_while(|&ch| ch == '{').count() % 2 == 0)
63        .map(|s| TextSize::new(s.len() as u32))
64}
65
66#[cfg(test)]
67mod tests {
68    use expect_test::expect;
69
70    use crate::tests::{check_edit, check_no_kw};
71
72    #[test]
73    fn works_when_wrapped() {
74        check_no_kw(
75            r#"
76//- minicore: fmt
77macro_rules! print {
78    ($($arg:tt)*) => (std::io::_print(format_args!($($arg)*)));
79}
80fn main() {
81    let foobar = 1;
82    print!("f$0");
83}
84"#,
85            expect![[]],
86        );
87    }
88
89    #[test]
90    fn no_completion_without_brace() {
91        check_no_kw(
92            r#"
93//- minicore: fmt
94fn main() {
95    let foobar = 1;
96    format_args!("f$0");
97}
98"#,
99            expect![[]],
100        );
101    }
102
103    #[test]
104    fn no_completion_after_escaped() {
105        check_no_kw(
106            r#"
107//- minicore: fmt
108fn main() {
109    let foobar = 1;
110    format_args!("{{f$0");
111}
112"#,
113            expect![[]],
114        );
115        check_no_kw(
116            r#"
117//- minicore: fmt
118fn main() {
119    let foobar = 1;
120    format_args!("some text {{{{f$0");
121}
122"#,
123            expect![[]],
124        );
125    }
126
127    #[test]
128    fn completes_unescaped_after_escaped() {
129        check_edit(
130            "foobar",
131            r#"
132//- minicore: fmt
133fn main() {
134    let foobar = 1;
135    format_args!("{{{f$0");
136}
137"#,
138            r#"
139fn main() {
140    let foobar = 1;
141    format_args!("{{{foobar");
142}
143"#,
144        );
145        check_edit(
146            "foobar",
147            r#"
148//- minicore: fmt
149fn main() {
150    let foobar = 1;
151    format_args!("{{{{{f$0");
152}
153"#,
154            r#"
155fn main() {
156    let foobar = 1;
157    format_args!("{{{{{foobar");
158}
159"#,
160        );
161        check_edit(
162            "foobar",
163            r#"
164//- minicore: fmt
165fn main() {
166    let foobar = 1;
167    format_args!("}}{f$0");
168}
169"#,
170            r#"
171fn main() {
172    let foobar = 1;
173    format_args!("}}{foobar");
174}
175"#,
176        );
177    }
178
179    #[test]
180    fn completes_locals() {
181        check_edit(
182            "foobar",
183            r#"
184//- minicore: fmt
185fn main() {
186    let foobar = 1;
187    format_args!("{f$0");
188}
189"#,
190            r#"
191fn main() {
192    let foobar = 1;
193    format_args!("{foobar");
194}
195"#,
196        );
197        check_edit(
198            "foobar",
199            r#"
200//- minicore: fmt
201fn main() {
202    let foobar = 1;
203    format_args!("{$0");
204}
205"#,
206            r#"
207fn main() {
208    let foobar = 1;
209    format_args!("{foobar");
210}
211"#,
212        );
213    }
214
215    #[test]
216    fn completes_constants() {
217        check_edit(
218            "FOOBAR",
219            r#"
220//- minicore: fmt
221fn main() {
222    const FOOBAR: usize = 42;
223    format_args!("{f$0");
224}
225"#,
226            r#"
227fn main() {
228    const FOOBAR: usize = 42;
229    format_args!("{FOOBAR");
230}
231"#,
232        );
233
234        check_edit(
235            "FOOBAR",
236            r#"
237//- minicore: fmt
238fn main() {
239    const FOOBAR: usize = 42;
240    format_args!("{$0");
241}
242"#,
243            r#"
244fn main() {
245    const FOOBAR: usize = 42;
246    format_args!("{FOOBAR");
247}
248"#,
249        );
250    }
251
252    #[test]
253    fn completes_static_constants() {
254        check_edit(
255            "FOOBAR",
256            r#"
257//- minicore: fmt
258fn main() {
259    static FOOBAR: usize = 42;
260    format_args!("{f$0");
261}
262"#,
263            r#"
264fn main() {
265    static FOOBAR: usize = 42;
266    format_args!("{FOOBAR");
267}
268"#,
269        );
270
271        check_edit(
272            "FOOBAR",
273            r#"
274//- minicore: fmt
275fn main() {
276    static FOOBAR: usize = 42;
277    format_args!("{$0");
278}
279"#,
280            r#"
281fn main() {
282    static FOOBAR: usize = 42;
283    format_args!("{FOOBAR");
284}
285"#,
286        );
287    }
288}