//!
//! 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::{
};
use syntax::{
ast::{self, AstNode},
- SyntaxNode, TextRange, TextSize,
+ SyntaxKind, SyntaxNode, TextRange, TextSize,
};
use crate::{
.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)| {
//! 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};
expanded_string: &ast::String,
range: TextRange,
) {
- if is_format_string(expanded_string).is_none() {
+ if !is_format_string(expanded_string) {
return;
}
});
}
-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
pub(crate) mod snippet;
pub(crate) mod trait_impl;
pub(crate) mod unqualified_path;
+pub(crate) mod format_string;
use std::iter;
--- /dev/null
+//! 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");
+}
+"#,
+ );
+ }
+}
}
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);
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)
}
pub mod insert_whitespace_into_node;
pub mod node_ext;
pub mod rust_doc;
+pub mod format_string;
use std::{collections::VecDeque, iter};
--- /dev/null
+//! 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()
+}