Skip to main content

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 mut const_value = None;
41            let symbol_kind = match module_def {
42                ModuleDef::Const(c) => {
43                    const_value = Some(c);
44                    SymbolKind::Const
45                }
46                ModuleDef::Static(..) => SymbolKind::Static,
47                _ => return,
48            };
49
50            let mut builder = CompletionItem::new(
51                CompletionItemKind::SymbolKind(symbol_kind),
52                source_range,
53                name.display_no_db(ctx.edition).to_smolstr(),
54                ctx.edition,
55            );
56            builder.const_value(const_value, ctx.db, ctx.display_target);
57            builder.add_to(acc, ctx.db);
58        }
59    });
60}
61
62fn unescaped_brace(prefix: &str) -> Option<TextSize> {
63    let is_ident_char = |ch: char| ch.is_alphanumeric() || ch == '_';
64    prefix
65        .trim_end_matches(is_ident_char)
66        .strip_suffix('{')
67        .filter(|it| it.chars().rev().take_while(|&ch| ch == '{').count() % 2 == 0)
68        .map(|s| TextSize::new(s.len() as u32))
69}
70
71#[cfg(test)]
72mod tests {
73    use expect_test::expect;
74
75    use crate::tests::{check_edit, check_no_kw};
76
77    #[test]
78    fn works_when_wrapped() {
79        check_no_kw(
80            r#"
81//- minicore: fmt
82macro_rules! print {
83    ($($arg:tt)*) => (std::io::_print(format_args!($($arg)*)));
84}
85fn main() {
86    let foobar = 1;
87    print!("f$0");
88}
89"#,
90            expect![[]],
91        );
92    }
93
94    #[test]
95    fn no_completion_without_brace() {
96        check_no_kw(
97            r#"
98//- minicore: fmt
99fn main() {
100    let foobar = 1;
101    format_args!("f$0");
102}
103"#,
104            expect![[]],
105        );
106    }
107
108    #[test]
109    fn no_completion_after_escaped() {
110        check_no_kw(
111            r#"
112//- minicore: fmt
113fn main() {
114    let foobar = 1;
115    format_args!("{{f$0");
116}
117"#,
118            expect![[]],
119        );
120        check_no_kw(
121            r#"
122//- minicore: fmt
123fn main() {
124    let foobar = 1;
125    format_args!("some text {{{{f$0");
126}
127"#,
128            expect![[]],
129        );
130    }
131
132    #[test]
133    fn completes_unescaped_after_escaped() {
134        check_edit(
135            "foobar",
136            r#"
137//- minicore: fmt
138fn main() {
139    let foobar = 1;
140    format_args!("{{{f$0");
141}
142"#,
143            r#"
144fn main() {
145    let foobar = 1;
146    format_args!("{{{foobar");
147}
148"#,
149        );
150        check_edit(
151            "foobar",
152            r#"
153//- minicore: fmt
154fn main() {
155    let foobar = 1;
156    format_args!("{{{{{f$0");
157}
158"#,
159            r#"
160fn main() {
161    let foobar = 1;
162    format_args!("{{{{{foobar");
163}
164"#,
165        );
166        check_edit(
167            "foobar",
168            r#"
169//- minicore: fmt
170fn main() {
171    let foobar = 1;
172    format_args!("}}{f$0");
173}
174"#,
175            r#"
176fn main() {
177    let foobar = 1;
178    format_args!("}}{foobar");
179}
180"#,
181        );
182    }
183
184    #[test]
185    fn completes_locals() {
186        check_edit(
187            "foobar",
188            r#"
189//- minicore: fmt
190fn main() {
191    let foobar = 1;
192    format_args!("{f$0");
193}
194"#,
195            r#"
196fn main() {
197    let foobar = 1;
198    format_args!("{foobar");
199}
200"#,
201        );
202        check_edit(
203            "foobar",
204            r#"
205//- minicore: fmt
206fn main() {
207    let foobar = 1;
208    format_args!("{$0");
209}
210"#,
211            r#"
212fn main() {
213    let foobar = 1;
214    format_args!("{foobar");
215}
216"#,
217        );
218    }
219
220    #[test]
221    fn completes_constants() {
222        check_edit(
223            "FOOBAR",
224            r#"
225//- minicore: fmt
226fn main() {
227    const FOOBAR: usize = 42;
228    format_args!("{f$0");
229}
230"#,
231            r#"
232fn main() {
233    const FOOBAR: usize = 42;
234    format_args!("{FOOBAR");
235}
236"#,
237        );
238
239        check_edit(
240            "FOOBAR",
241            r#"
242//- minicore: fmt
243fn main() {
244    const FOOBAR: usize = 42;
245    format_args!("{$0");
246}
247"#,
248            r#"
249fn main() {
250    const FOOBAR: usize = 42;
251    format_args!("{FOOBAR");
252}
253"#,
254        );
255    }
256
257    #[test]
258    fn completes_static_constants() {
259        check_edit(
260            "FOOBAR",
261            r#"
262//- minicore: fmt
263fn main() {
264    static FOOBAR: usize = 42;
265    format_args!("{f$0");
266}
267"#,
268            r#"
269fn main() {
270    static FOOBAR: usize = 42;
271    format_args!("{FOOBAR");
272}
273"#,
274        );
275
276        check_edit(
277            "FOOBAR",
278            r#"
279//- minicore: fmt
280fn main() {
281    static FOOBAR: usize = 42;
282    format_args!("{$0");
283}
284"#,
285            r#"
286fn main() {
287    static FOOBAR: usize = 42;
288    format_args!("{FOOBAR");
289}
290"#,
291        );
292    }
293}