From 82fccb971e49d6d8945b7764dd9e9ed883f6148f Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sat, 15 Jan 2022 12:12:02 +0100 Subject: [PATCH] feat: Add very simplistic ident completion for format_args! macro input --- crates/hir/src/source_analyzer.rs | 27 +++-- crates/ide/src/syntax_highlighting/format.rs | 32 +----- crates/ide_completion/src/completions.rs | 1 + .../src/completions/format_string.rs | 107 ++++++++++++++++++ .../ide_completion/src/completions/postfix.rs | 2 +- crates/ide_completion/src/lib.rs | 1 + crates/ide_db/src/helpers.rs | 1 + crates/ide_db/src/helpers/format_string.rs | 31 +++++ 8 files changed, 163 insertions(+), 39 deletions(-) create mode 100644 crates/ide_completion/src/completions/format_string.rs create mode 100644 crates/ide_db/src/helpers/format_string.rs diff --git a/crates/hir/src/source_analyzer.rs b/crates/hir/src/source_analyzer.rs index ef3dfa1f335..869f4a10f84 100644 --- a/crates/hir/src/source_analyzer.rs +++ b/crates/hir/src/source_analyzer.rs @@ -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)| { diff --git a/crates/ide/src/syntax_highlighting/format.rs b/crates/ide/src/syntax_highlighting/format.rs index f83262fc5c5..0aa97a61020 100644 --- a/crates/ide/src/syntax_highlighting/format.rs +++ b/crates/ide/src/syntax_highlighting/format.rs @@ -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 { Some(match kind { FormatSpecifier::Open diff --git a/crates/ide_completion/src/completions.rs b/crates/ide_completion/src/completions.rs index 19fdc6c2442..e399213731d 100644 --- a/crates/ide_completion/src/completions.rs +++ b/crates/ide_completion/src/completions.rs @@ -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 index 00000000000..08f5a59437f --- /dev/null +++ b/crates/ide_completion/src/completions/format_string.rs @@ -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"); +} +"#, + ); + } +} diff --git a/crates/ide_completion/src/completions/postfix.rs b/crates/ide_completion/src/completions/postfix.rs index 0dfb8abb8bd..e8e0c7ea9f1 100644 --- a/crates/ide_completion/src/completions/postfix.rs +++ b/crates/ide_completion/src/completions/postfix.rs @@ -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); diff --git a/crates/ide_completion/src/lib.rs b/crates/ide_completion/src/lib.rs index 6a087edc4f2..a2217af493d 100644 --- a/crates/ide_completion/src/lib.rs +++ b/crates/ide_completion/src/lib.rs @@ -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) } diff --git a/crates/ide_db/src/helpers.rs b/crates/ide_db/src/helpers.rs index 344f8db8d00..2d3d6409338 100644 --- a/crates/ide_db/src/helpers.rs +++ b/crates/ide_db/src/helpers.rs @@ -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 index 00000000000..c615d07250e --- /dev/null +++ b/crates/ide_db/src/helpers/format_string.rs @@ -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() +}