Merge #11293
11293: feat: Add very simplistic ident completion for format_args! macro input r=Veykril a=Veykril Co-authored-by: Lukas Wirth <lukastw97@gmail.com>
This commit is contained in:
commit
4c34909308
@ -5,7 +5,10 @@
|
|||||||
//!
|
//!
|
||||||
//! So, this modules should not be used during hir construction, it exists
|
//! So, this modules should not be used during hir construction, it exists
|
||||||
//! purely for "IDE needs".
|
//! purely for "IDE needs".
|
||||||
use std::{iter::once, sync::Arc};
|
use std::{
|
||||||
|
iter::{self, once},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use hir_def::{
|
use hir_def::{
|
||||||
body::{
|
body::{
|
||||||
@ -25,7 +28,7 @@
|
|||||||
};
|
};
|
||||||
use syntax::{
|
use syntax::{
|
||||||
ast::{self, AstNode},
|
ast::{self, AstNode},
|
||||||
SyntaxNode, TextRange, TextSize,
|
SyntaxKind, SyntaxNode, TextRange, TextSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -488,14 +491,20 @@ fn scope_for_offset(
|
|||||||
.scope_by_expr()
|
.scope_by_expr()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(id, scope)| {
|
.filter_map(|(id, scope)| {
|
||||||
let source = source_map.expr_syntax(*id).ok()?;
|
let InFile { file_id, value } = source_map.expr_syntax(*id).ok()?;
|
||||||
// FIXME: correctly handle macro expansion
|
if offset.file_id == file_id {
|
||||||
if source.file_id != offset.file_id {
|
let root = db.parse_or_expand(file_id)?;
|
||||||
return None;
|
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);
|
// FIXME handle attribute expansion
|
||||||
Some((node.syntax().text_range(), scope))
|
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
|
// find containing scope
|
||||||
.min_by_key(|(expr_range, _scope)| {
|
.min_by_key(|(expr_range, _scope)| {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
//! Syntax highlighting for format macro strings.
|
//! Syntax highlighting for format macro strings.
|
||||||
use ide_db::SymbolKind;
|
use ide_db::{helpers::format_string::is_format_string, SymbolKind};
|
||||||
use syntax::{
|
use syntax::{
|
||||||
ast::{self, FormatSpecifier, HasFormatSpecifier},
|
ast::{self, FormatSpecifier, HasFormatSpecifier},
|
||||||
AstNode, AstToken, TextRange,
|
TextRange,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{syntax_highlighting::highlights::Highlights, HlRange, HlTag};
|
use crate::{syntax_highlighting::highlights::Highlights, HlRange, HlTag};
|
||||||
@ -13,7 +13,7 @@ pub(super) fn highlight_format_string(
|
|||||||
expanded_string: &ast::String,
|
expanded_string: &ast::String,
|
||||||
range: TextRange,
|
range: TextRange,
|
||||||
) {
|
) {
|
||||||
if is_format_string(expanded_string).is_none() {
|
if !is_format_string(expanded_string) {
|
||||||
return;
|
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> {
|
fn highlight_format_specifier(kind: FormatSpecifier) -> Option<HlTag> {
|
||||||
Some(match kind {
|
Some(match kind {
|
||||||
FormatSpecifier::Open
|
FormatSpecifier::Open
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
pub(crate) mod snippet;
|
pub(crate) mod snippet;
|
||||||
pub(crate) mod trait_impl;
|
pub(crate) mod trait_impl;
|
||||||
pub(crate) mod unqualified_path;
|
pub(crate) mod unqualified_path;
|
||||||
|
pub(crate) mod format_string;
|
||||||
|
|
||||||
use std::iter;
|
use std::iter;
|
||||||
|
|
||||||
|
107
crates/ide_completion/src/completions/format_string.rs
Normal file
107
crates/ide_completion/src/completions/format_string.rs
Normal file
@ -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");
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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("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("dbgr", "dbg!(&expr)", &format!("dbg!(&{})", receiver_text)).add_to(acc);
|
||||||
postfix_snippet("call", "function(expr)", &format!("${{1}}({})", receiver_text)).add_to(acc);
|
postfix_snippet("call", "function(expr)", &format!("${{1}}({})", receiver_text)).add_to(acc);
|
||||||
|
|
||||||
|
@ -168,6 +168,7 @@ pub fn completions(
|
|||||||
completions::flyimport::import_on_the_fly(&mut acc, &ctx);
|
completions::flyimport::import_on_the_fly(&mut acc, &ctx);
|
||||||
completions::lifetime::complete_lifetime(&mut acc, &ctx);
|
completions::lifetime::complete_lifetime(&mut acc, &ctx);
|
||||||
completions::lifetime::complete_label(&mut acc, &ctx);
|
completions::lifetime::complete_label(&mut acc, &ctx);
|
||||||
|
completions::format_string::format_string(&mut acc, &ctx);
|
||||||
|
|
||||||
Some(acc)
|
Some(acc)
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
pub mod insert_whitespace_into_node;
|
pub mod insert_whitespace_into_node;
|
||||||
pub mod node_ext;
|
pub mod node_ext;
|
||||||
pub mod rust_doc;
|
pub mod rust_doc;
|
||||||
|
pub mod format_string;
|
||||||
|
|
||||||
use std::{collections::VecDeque, iter};
|
use std::{collections::VecDeque, iter};
|
||||||
|
|
||||||
|
31
crates/ide_db/src/helpers/format_string.rs
Normal file
31
crates/ide_db/src/helpers/format_string.rs
Normal file
@ -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()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user