]> git.lizzy.rs Git - rust.git/commitdiff
feat: Add very simplistic ident completion for format_args! macro input
authorLukas Wirth <lukastw97@gmail.com>
Sat, 15 Jan 2022 11:12:02 +0000 (12:12 +0100)
committerLukas Wirth <lukastw97@gmail.com>
Sat, 15 Jan 2022 11:23:26 +0000 (12:23 +0100)
crates/hir/src/source_analyzer.rs
crates/ide/src/syntax_highlighting/format.rs
crates/ide_completion/src/completions.rs
crates/ide_completion/src/completions/format_string.rs [new file with mode: 0644]
crates/ide_completion/src/completions/postfix.rs
crates/ide_completion/src/lib.rs
crates/ide_db/src/helpers.rs
crates/ide_db/src/helpers/format_string.rs [new file with mode: 0644]

index ef3dfa1f335d0cabcdd7a8adccfc5b05d5153536..869f4a10f84fe361077e602b032d8bd26da2449d 100644 (file)
@@ -5,7 +5,10 @@
 //!
 //! So, this modules should not be used during hir construction, it exists
 //! purely for "IDE needs".
-use std::{iter::once, sync::Arc};
+use std::{
+    iter::{self, once},
+    sync::Arc,
+};
 
 use hir_def::{
     body::{
@@ -25,7 +28,7 @@
 };
 use syntax::{
     ast::{self, AstNode},
-    SyntaxNode, TextRange, TextSize,
+    SyntaxKind, SyntaxNode, TextRange, TextSize,
 };
 
 use crate::{
@@ -488,14 +491,20 @@ fn scope_for_offset(
         .scope_by_expr()
         .iter()
         .filter_map(|(id, scope)| {
-            let source = source_map.expr_syntax(*id).ok()?;
-            // FIXME: correctly handle macro expansion
-            if source.file_id != offset.file_id {
-                return None;
+            let InFile { file_id, value } = source_map.expr_syntax(*id).ok()?;
+            if offset.file_id == file_id {
+                let root = db.parse_or_expand(file_id)?;
+                let node = value.to_node(&root);
+                return Some((node.syntax().text_range(), scope));
             }
-            let root = source.file_syntax(db.upcast());
-            let node = source.value.to_node(&root);
-            Some((node.syntax().text_range(), scope))
+
+            // FIXME handle attribute expansion
+            let source = iter::successors(file_id.call_node(db.upcast()), |it| {
+                it.file_id.call_node(db.upcast())
+            })
+            .find(|it| it.file_id == offset.file_id)
+            .filter(|it| it.value.kind() == SyntaxKind::MACRO_CALL)?;
+            Some((source.value.text_range(), scope))
         })
         // find containing scope
         .min_by_key(|(expr_range, _scope)| {
index f83262fc5c5ead0ddd3aab5aedaadd2f87299a1e..0aa97a6102030b98bf0b84a38a79852abd9a904d 100644 (file)
@@ -1,8 +1,8 @@
 //! Syntax highlighting for format macro strings.
-use ide_db::SymbolKind;
+use ide_db::{helpers::format_string::is_format_string, SymbolKind};
 use syntax::{
     ast::{self, FormatSpecifier, HasFormatSpecifier},
-    AstNode, AstToken, TextRange,
+    TextRange,
 };
 
 use crate::{syntax_highlighting::highlights::Highlights, HlRange, HlTag};
@@ -13,7 +13,7 @@ pub(super) fn highlight_format_string(
     expanded_string: &ast::String,
     range: TextRange,
 ) {
-    if is_format_string(expanded_string).is_none() {
+    if !is_format_string(expanded_string) {
         return;
     }
 
@@ -28,32 +28,6 @@ pub(super) fn highlight_format_string(
     });
 }
 
-fn is_format_string(string: &ast::String) -> Option<()> {
-    // Check if `string` is a format string argument of a macro invocation.
-    // `string` is a string literal, mapped down into the innermost macro expansion.
-    // Since `format_args!` etc. remove the format string when expanding, but place all arguments
-    // in the expanded output, we know that the string token is (part of) the format string if it
-    // appears in `format_args!` (otherwise it would have been mapped down further).
-    //
-    // This setup lets us correctly highlight the components of `concat!("{}", "bla")` format
-    // strings. It still fails for `concat!("{", "}")`, but that is rare.
-
-    let macro_call = string.syntax().ancestors().find_map(ast::MacroCall::cast)?;
-    let name = macro_call.path()?.segment()?.name_ref()?;
-
-    if !matches!(
-        name.text().as_str(),
-        "format_args" | "format_args_nl" | "const_format_args" | "panic_2015" | "panic_2021"
-    ) {
-        return None;
-    }
-
-    // NB: we match against `panic_2015`/`panic_2021` here because they have a special-cased arm for
-    // `"{}"`, which otherwise wouldn't get highlighted.
-
-    Some(())
-}
-
 fn highlight_format_specifier(kind: FormatSpecifier) -> Option<HlTag> {
     Some(match kind {
         FormatSpecifier::Open
index 19fdc6c24423f811e8598dc4da17f33060385040..e399213731db84a87b25afb327056a35a6b69567 100644 (file)
@@ -14,6 +14,7 @@
 pub(crate) mod snippet;
 pub(crate) mod trait_impl;
 pub(crate) mod unqualified_path;
+pub(crate) mod format_string;
 
 use std::iter;
 
diff --git a/crates/ide_completion/src/completions/format_string.rs b/crates/ide_completion/src/completions/format_string.rs
new file mode 100644 (file)
index 0000000..08f5a59
--- /dev/null
@@ -0,0 +1,107 @@
+//! Completes identifiers in format string literals.
+
+use ide_db::helpers::format_string::is_format_string;
+use itertools::Itertools;
+use syntax::{ast, AstToken, TextRange, TextSize};
+
+use crate::{context::CompletionContext, CompletionItem, CompletionItemKind, Completions};
+
+/// Complete identifiers in format strings.
+pub(crate) fn format_string(acc: &mut Completions, ctx: &CompletionContext) {
+    let string = match ast::String::cast(ctx.token.clone()) {
+        Some(it) if is_format_string(&it) => it,
+        _ => return,
+    };
+    let cursor = ctx.position.offset;
+    let lit_start = ctx.token.text_range().start();
+    let cursor_in_lit = cursor - lit_start;
+
+    let prefix = &string.text()[..cursor_in_lit.into()];
+    let braces = prefix.char_indices().rev().skip_while(|&(_, c)| c.is_alphanumeric()).next_tuple();
+    let brace_offset = match braces {
+        // escaped brace
+        Some(((_, '{'), (_, '{'))) => return,
+        Some(((idx, '{'), _)) => lit_start + TextSize::from(idx as u32 + 1),
+        _ => return,
+    };
+
+    let source_range = TextRange::new(brace_offset, cursor);
+    ctx.locals.iter().for_each(|(name, _)| {
+        CompletionItem::new(CompletionItemKind::Binding, source_range, name.to_smol_str())
+            .add_to(acc);
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use expect_test::{expect, Expect};
+
+    use crate::tests::{check_edit, completion_list_no_kw};
+
+    fn check(ra_fixture: &str, expect: Expect) {
+        let actual = completion_list_no_kw(ra_fixture);
+        expect.assert_eq(&actual);
+    }
+
+    #[test]
+    fn no_completion_without_brace() {
+        check(
+            r#"
+macro_rules! format_args {
+($lit:literal $(tt:tt)*) => { 0 },
+}
+fn main() {
+let foobar = 1;
+format_args!("f$0");
+}
+"#,
+            expect![[]],
+        );
+    }
+
+    #[test]
+    fn completes_locals() {
+        check_edit(
+            "foobar",
+            r#"
+macro_rules! format_args {
+    ($lit:literal $(tt:tt)*) => { 0 },
+}
+fn main() {
+    let foobar = 1;
+    format_args!("{f$0");
+}
+"#,
+            r#"
+macro_rules! format_args {
+    ($lit:literal $(tt:tt)*) => { 0 },
+}
+fn main() {
+    let foobar = 1;
+    format_args!("{foobar");
+}
+"#,
+        );
+        check_edit(
+            "foobar",
+            r#"
+macro_rules! format_args {
+    ($lit:literal $(tt:tt)*) => { 0 },
+}
+fn main() {
+    let foobar = 1;
+    format_args!("{$0");
+}
+"#,
+            r#"
+macro_rules! format_args {
+    ($lit:literal $(tt:tt)*) => { 0 },
+}
+fn main() {
+    let foobar = 1;
+    format_args!("{foobar");
+}
+"#,
+        );
+    }
+}
index 0dfb8abb8bda2f6cd1a1aedc5655fbf484021c0f..e8e0c7ea9f1d1b65d7e66e8a3a206d4bcefe3a09 100644 (file)
@@ -179,7 +179,7 @@ pub(crate) fn complete_postfix(acc: &mut Completions, ctx: &CompletionContext) {
     }
 
     postfix_snippet("box", "Box::new(expr)", &format!("Box::new({})", receiver_text)).add_to(acc);
-    postfix_snippet("dbg", "dbg!(expr)", &format!("dbg!({})", receiver_text)).add_to(acc);
+    postfix_snippet("dbg", "dbg!(expr)", &format!("dbg!({})", receiver_text)).add_to(acc); // fixme
     postfix_snippet("dbgr", "dbg!(&expr)", &format!("dbg!(&{})", receiver_text)).add_to(acc);
     postfix_snippet("call", "function(expr)", &format!("${{1}}({})", receiver_text)).add_to(acc);
 
index 6a087edc4f226bbd5c35cf8b0352640a72bc2c01..a2217af493df216479e86840030552201ad53145 100644 (file)
@@ -168,6 +168,7 @@ pub fn completions(
     completions::flyimport::import_on_the_fly(&mut acc, &ctx);
     completions::lifetime::complete_lifetime(&mut acc, &ctx);
     completions::lifetime::complete_label(&mut acc, &ctx);
+    completions::format_string::format_string(&mut acc, &ctx);
 
     Some(acc)
 }
index 344f8db8d00a7569d6a3cf8064fce49cb7455eda..2d3d64093385c3ac2525ba66f80cf8a23530817b 100644 (file)
@@ -7,6 +7,7 @@
 pub mod insert_whitespace_into_node;
 pub mod node_ext;
 pub mod rust_doc;
+pub mod format_string;
 
 use std::{collections::VecDeque, iter};
 
diff --git a/crates/ide_db/src/helpers/format_string.rs b/crates/ide_db/src/helpers/format_string.rs
new file mode 100644 (file)
index 0000000..c615d07
--- /dev/null
@@ -0,0 +1,31 @@
+//! Tools to work with format string literals for the `format_args!` family of macros.
+use syntax::{ast, AstNode, AstToken};
+
+pub fn is_format_string(string: &ast::String) -> bool {
+    // Check if `string` is a format string argument of a macro invocation.
+    // `string` is a string literal, mapped down into the innermost macro expansion.
+    // Since `format_args!` etc. remove the format string when expanding, but place all arguments
+    // in the expanded output, we know that the string token is (part of) the format string if it
+    // appears in `format_args!` (otherwise it would have been mapped down further).
+    //
+    // This setup lets us correctly highlight the components of `concat!("{}", "bla")` format
+    // strings. It still fails for `concat!("{", "}")`, but that is rare.
+
+    (|| {
+        let macro_call = string.syntax().ancestors().find_map(ast::MacroCall::cast)?;
+        let name = macro_call.path()?.segment()?.name_ref()?;
+
+        if !matches!(
+            name.text().as_str(),
+            "format_args" | "format_args_nl" | "const_format_args" | "panic_2015" | "panic_2021"
+        ) {
+            return None;
+        }
+
+        // NB: we match against `panic_2015`/`panic_2021` here because they have a special-cased arm for
+        // `"{}"`, which otherwise wouldn't get highlighted.
+
+        Some(())
+    })()
+    .is_some()
+}