mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 13:03:31 +00:00
3998: Make add_function generate functions in other modules via qualified path r=matklad a=TimoFreiberg Additional feature for #3639 - [x] Add tests for paths with more segments - [x] Make generating the function in another file work - [x] Add `pub` or `pub(crate)` to the generated function if it's generated in a different module - [x] Make the assist jump to the edited file - [x] Enable file support in the `check_assist` helper 4006: Syntax highlighting for format strings r=matklad a=ltentrup I have an implementation for syntax highlighting for format string modifiers `{}`. The first commit refactors the changes in #3826 into a separate struct. The second commit implements the highlighting: first we check in a macro call whether the macro is a format macro from `std`. In this case, we remember the format string node. If we encounter this node during syntax highlighting, we check for the format modifiers `{}` using regular expressions. There are a few places which I am not quite sure: - Is the way I extract the macro names correct? - Is the `HighlightTag::Attribute` suitable for highlighting the `{}`? Let me know what you think, any feedback is welcome! Co-authored-by: Timo Freiberg <timo.freiberg@gmail.com> Co-authored-by: Leander Tentrup <leander.tentrup@gmail.com> Co-authored-by: Leander Tentrup <ltentrup@users.noreply.github.com>
This commit is contained in:
commit
51a0058d4c
12 changed files with 940 additions and 95 deletions
|
@ -10,7 +10,7 @@ use ra_syntax::{
|
|||
};
|
||||
use ra_text_edit::TextEditBuilder;
|
||||
|
||||
use crate::{AssistAction, AssistId, AssistLabel, GroupLabel, ResolvedAssist};
|
||||
use crate::{AssistAction, AssistFile, AssistId, AssistLabel, GroupLabel, ResolvedAssist};
|
||||
use algo::SyntaxRewriter;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -180,6 +180,7 @@ pub(crate) struct ActionBuilder {
|
|||
edit: TextEditBuilder,
|
||||
cursor_position: Option<TextUnit>,
|
||||
target: Option<TextRange>,
|
||||
file: AssistFile,
|
||||
}
|
||||
|
||||
impl ActionBuilder {
|
||||
|
@ -241,11 +242,16 @@ impl ActionBuilder {
|
|||
algo::diff(&node, &new).into_text_edit(&mut self.edit)
|
||||
}
|
||||
|
||||
pub(crate) fn set_file(&mut self, assist_file: AssistFile) {
|
||||
self.file = assist_file
|
||||
}
|
||||
|
||||
fn build(self) -> AssistAction {
|
||||
AssistAction {
|
||||
edit: self.edit.finish(),
|
||||
cursor_position: self.cursor_position,
|
||||
target: self.target,
|
||||
file: self.file,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ fn doctest_add_function() {
|
|||
struct Baz;
|
||||
fn baz() -> Baz { Baz }
|
||||
fn foo() {
|
||||
bar<|>("", baz());
|
||||
bar<|>("", baz());
|
||||
}
|
||||
|
||||
"#####,
|
||||
|
@ -74,7 +74,7 @@ fn foo() {
|
|||
struct Baz;
|
||||
fn baz() -> Baz { Baz }
|
||||
fn foo() {
|
||||
bar("", baz());
|
||||
bar("", baz());
|
||||
}
|
||||
|
||||
fn bar(arg: &str, baz: Baz) {
|
||||
|
|
|
@ -3,8 +3,8 @@ use ra_syntax::{
|
|||
SyntaxKind, SyntaxNode, TextUnit,
|
||||
};
|
||||
|
||||
use crate::{Assist, AssistCtx, AssistId};
|
||||
use ast::{edit::IndentLevel, ArgListOwner, CallExpr, Expr};
|
||||
use crate::{Assist, AssistCtx, AssistFile, AssistId};
|
||||
use ast::{edit::IndentLevel, ArgListOwner, ModuleItemOwner};
|
||||
use hir::HirDisplay;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
|
@ -16,7 +16,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
|||
// struct Baz;
|
||||
// fn baz() -> Baz { Baz }
|
||||
// fn foo() {
|
||||
// bar<|>("", baz());
|
||||
// bar<|>("", baz());
|
||||
// }
|
||||
//
|
||||
// ```
|
||||
|
@ -25,7 +25,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
|||
// struct Baz;
|
||||
// fn baz() -> Baz { Baz }
|
||||
// fn foo() {
|
||||
// bar("", baz());
|
||||
// bar("", baz());
|
||||
// }
|
||||
//
|
||||
// fn bar(arg: &str, baz: Baz) {
|
||||
|
@ -38,21 +38,30 @@ pub(crate) fn add_function(ctx: AssistCtx) -> Option<Assist> {
|
|||
let call = path_expr.syntax().parent().and_then(ast::CallExpr::cast)?;
|
||||
let path = path_expr.path()?;
|
||||
|
||||
if path.qualifier().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if ctx.sema.resolve_path(&path).is_some() {
|
||||
// The function call already resolves, no need to add a function
|
||||
return None;
|
||||
}
|
||||
|
||||
let function_builder = FunctionBuilder::from_call(&ctx, &call)?;
|
||||
let target_module = if let Some(qualifier) = path.qualifier() {
|
||||
if let Some(hir::PathResolution::Def(hir::ModuleDef::Module(module))) =
|
||||
ctx.sema.resolve_path(&qualifier)
|
||||
{
|
||||
Some(module.definition_source(ctx.sema.db))
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let function_builder = FunctionBuilder::from_call(&ctx, &call, &path, target_module)?;
|
||||
|
||||
ctx.add_assist(AssistId("add_function"), "Add function", |edit| {
|
||||
edit.target(call.syntax().text_range());
|
||||
|
||||
if let Some(function_template) = function_builder.render() {
|
||||
edit.set_file(function_template.file);
|
||||
edit.set_cursor(function_template.cursor_offset);
|
||||
edit.insert(function_template.insert_offset, function_template.fn_def.to_string());
|
||||
}
|
||||
|
@ -63,29 +72,67 @@ struct FunctionTemplate {
|
|||
insert_offset: TextUnit,
|
||||
cursor_offset: TextUnit,
|
||||
fn_def: ast::SourceFile,
|
||||
file: AssistFile,
|
||||
}
|
||||
|
||||
struct FunctionBuilder {
|
||||
append_fn_at: SyntaxNode,
|
||||
target: GeneratedFunctionTarget,
|
||||
fn_name: ast::Name,
|
||||
type_params: Option<ast::TypeParamList>,
|
||||
params: ast::ParamList,
|
||||
file: AssistFile,
|
||||
needs_pub: bool,
|
||||
}
|
||||
|
||||
impl FunctionBuilder {
|
||||
fn from_call(ctx: &AssistCtx, call: &ast::CallExpr) -> Option<Self> {
|
||||
let append_fn_at = next_space_for_fn(&call)?;
|
||||
let fn_name = fn_name(&call)?;
|
||||
/// Prepares a generated function that matches `call` in `generate_in`
|
||||
/// (or as close to `call` as possible, if `generate_in` is `None`)
|
||||
fn from_call(
|
||||
ctx: &AssistCtx,
|
||||
call: &ast::CallExpr,
|
||||
path: &ast::Path,
|
||||
target_module: Option<hir::InFile<hir::ModuleSource>>,
|
||||
) -> Option<Self> {
|
||||
let needs_pub = target_module.is_some();
|
||||
let mut file = AssistFile::default();
|
||||
let target = if let Some(target_module) = target_module {
|
||||
let (in_file, target) = next_space_for_fn_in_module(ctx.sema.db, target_module)?;
|
||||
file = in_file;
|
||||
target
|
||||
} else {
|
||||
next_space_for_fn_after_call_site(&call)?
|
||||
};
|
||||
let fn_name = fn_name(&path)?;
|
||||
let (type_params, params) = fn_args(ctx, &call)?;
|
||||
Some(Self { append_fn_at, fn_name, type_params, params })
|
||||
Some(Self { target, fn_name, type_params, params, file, needs_pub })
|
||||
}
|
||||
|
||||
fn render(self) -> Option<FunctionTemplate> {
|
||||
let placeholder_expr = ast::make::expr_todo();
|
||||
let fn_body = ast::make::block_expr(vec![], Some(placeholder_expr));
|
||||
let fn_def = ast::make::fn_def(self.fn_name, self.type_params, self.params, fn_body);
|
||||
let fn_def = ast::make::add_newlines(2, fn_def);
|
||||
let fn_def = IndentLevel::from_node(&self.append_fn_at).increase_indent(fn_def);
|
||||
let insert_offset = self.append_fn_at.text_range().end();
|
||||
let mut fn_def = ast::make::fn_def(self.fn_name, self.type_params, self.params, fn_body);
|
||||
if self.needs_pub {
|
||||
fn_def = ast::make::add_pub_crate_modifier(fn_def);
|
||||
}
|
||||
|
||||
let (fn_def, insert_offset) = match self.target {
|
||||
GeneratedFunctionTarget::BehindItem(it) => {
|
||||
let with_leading_blank_line = ast::make::add_leading_newlines(2, fn_def);
|
||||
let indented = IndentLevel::from_node(&it).increase_indent(with_leading_blank_line);
|
||||
(indented, it.text_range().end())
|
||||
}
|
||||
GeneratedFunctionTarget::InEmptyItemList(it) => {
|
||||
let indent_once = IndentLevel(1);
|
||||
let indent = IndentLevel::from_node(it.syntax());
|
||||
|
||||
let fn_def = ast::make::add_leading_newlines(1, fn_def);
|
||||
let fn_def = indent_once.increase_indent(fn_def);
|
||||
let fn_def = ast::make::add_trailing_newlines(1, fn_def);
|
||||
let fn_def = indent.increase_indent(fn_def);
|
||||
(fn_def, it.syntax().text_range().start() + TextUnit::from_usize(1))
|
||||
}
|
||||
};
|
||||
|
||||
let cursor_offset_from_fn_start = fn_def
|
||||
.syntax()
|
||||
.descendants()
|
||||
|
@ -94,19 +141,24 @@ impl FunctionBuilder {
|
|||
.text_range()
|
||||
.start();
|
||||
let cursor_offset = insert_offset + cursor_offset_from_fn_start;
|
||||
Some(FunctionTemplate { insert_offset, cursor_offset, fn_def })
|
||||
Some(FunctionTemplate { insert_offset, cursor_offset, fn_def, file: self.file })
|
||||
}
|
||||
}
|
||||
|
||||
fn fn_name(call: &CallExpr) -> Option<ast::Name> {
|
||||
let name = call.expr()?.syntax().to_string();
|
||||
enum GeneratedFunctionTarget {
|
||||
BehindItem(SyntaxNode),
|
||||
InEmptyItemList(ast::ItemList),
|
||||
}
|
||||
|
||||
fn fn_name(call: &ast::Path) -> Option<ast::Name> {
|
||||
let name = call.segment()?.syntax().to_string();
|
||||
Some(ast::make::name(&name))
|
||||
}
|
||||
|
||||
/// Computes the type variables and arguments required for the generated function
|
||||
fn fn_args(
|
||||
ctx: &AssistCtx,
|
||||
call: &CallExpr,
|
||||
call: &ast::CallExpr,
|
||||
) -> Option<(Option<ast::TypeParamList>, ast::ParamList)> {
|
||||
let mut arg_names = Vec::new();
|
||||
let mut arg_types = Vec::new();
|
||||
|
@ -158,9 +210,9 @@ fn deduplicate_arg_names(arg_names: &mut Vec<String>) {
|
|||
}
|
||||
}
|
||||
|
||||
fn fn_arg_name(fn_arg: &Expr) -> Option<String> {
|
||||
fn fn_arg_name(fn_arg: &ast::Expr) -> Option<String> {
|
||||
match fn_arg {
|
||||
Expr::CastExpr(cast_expr) => fn_arg_name(&cast_expr.expr()?),
|
||||
ast::Expr::CastExpr(cast_expr) => fn_arg_name(&cast_expr.expr()?),
|
||||
_ => Some(
|
||||
fn_arg
|
||||
.syntax()
|
||||
|
@ -172,7 +224,7 @@ fn fn_arg_name(fn_arg: &Expr) -> Option<String> {
|
|||
}
|
||||
}
|
||||
|
||||
fn fn_arg_type(ctx: &AssistCtx, fn_arg: &Expr) -> Option<String> {
|
||||
fn fn_arg_type(ctx: &AssistCtx, fn_arg: &ast::Expr) -> Option<String> {
|
||||
let ty = ctx.sema.type_of_expr(fn_arg)?;
|
||||
if ty.is_unknown() {
|
||||
return None;
|
||||
|
@ -184,7 +236,7 @@ fn fn_arg_type(ctx: &AssistCtx, fn_arg: &Expr) -> Option<String> {
|
|||
/// directly after the current block
|
||||
/// We want to write the generated function directly after
|
||||
/// fns, impls or macro calls, but inside mods
|
||||
fn next_space_for_fn(expr: &CallExpr) -> Option<SyntaxNode> {
|
||||
fn next_space_for_fn_after_call_site(expr: &ast::CallExpr) -> Option<GeneratedFunctionTarget> {
|
||||
let mut ancestors = expr.syntax().ancestors().peekable();
|
||||
let mut last_ancestor: Option<SyntaxNode> = None;
|
||||
while let Some(next_ancestor) = ancestors.next() {
|
||||
|
@ -201,7 +253,32 @@ fn next_space_for_fn(expr: &CallExpr) -> Option<SyntaxNode> {
|
|||
}
|
||||
last_ancestor = Some(next_ancestor);
|
||||
}
|
||||
last_ancestor
|
||||
last_ancestor.map(GeneratedFunctionTarget::BehindItem)
|
||||
}
|
||||
|
||||
fn next_space_for_fn_in_module(
|
||||
db: &dyn hir::db::AstDatabase,
|
||||
module: hir::InFile<hir::ModuleSource>,
|
||||
) -> Option<(AssistFile, GeneratedFunctionTarget)> {
|
||||
let file = module.file_id.original_file(db);
|
||||
let assist_file = AssistFile::TargetFile(file);
|
||||
let assist_item = match module.value {
|
||||
hir::ModuleSource::SourceFile(it) => {
|
||||
if let Some(last_item) = it.items().last() {
|
||||
GeneratedFunctionTarget::BehindItem(last_item.syntax().clone())
|
||||
} else {
|
||||
GeneratedFunctionTarget::BehindItem(it.syntax().clone())
|
||||
}
|
||||
}
|
||||
hir::ModuleSource::Module(it) => {
|
||||
if let Some(last_item) = it.item_list().and_then(|it| it.items().last()) {
|
||||
GeneratedFunctionTarget::BehindItem(last_item.syntax().clone())
|
||||
} else {
|
||||
GeneratedFunctionTarget::InEmptyItemList(it.item_list()?)
|
||||
}
|
||||
}
|
||||
};
|
||||
Some((assist_file, assist_item))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -713,6 +790,111 @@ fn bar(baz_1: Baz, baz_2: Baz, arg_1: &str, arg_2: &str) {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_function_in_module() {
|
||||
check_assist(
|
||||
add_function,
|
||||
r"
|
||||
mod bar {}
|
||||
|
||||
fn foo() {
|
||||
bar::my_fn<|>()
|
||||
}
|
||||
",
|
||||
r"
|
||||
mod bar {
|
||||
pub(crate) fn my_fn() {
|
||||
<|>todo!()
|
||||
}
|
||||
}
|
||||
|
||||
fn foo() {
|
||||
bar::my_fn()
|
||||
}
|
||||
",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_function_in_module_containing_other_items() {
|
||||
check_assist(
|
||||
add_function,
|
||||
r"
|
||||
mod bar {
|
||||
fn something_else() {}
|
||||
}
|
||||
|
||||
fn foo() {
|
||||
bar::my_fn<|>()
|
||||
}
|
||||
",
|
||||
r"
|
||||
mod bar {
|
||||
fn something_else() {}
|
||||
|
||||
pub(crate) fn my_fn() {
|
||||
<|>todo!()
|
||||
}
|
||||
}
|
||||
|
||||
fn foo() {
|
||||
bar::my_fn()
|
||||
}
|
||||
",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_function_in_nested_module() {
|
||||
check_assist(
|
||||
add_function,
|
||||
r"
|
||||
mod bar {
|
||||
mod baz {}
|
||||
}
|
||||
|
||||
fn foo() {
|
||||
bar::baz::my_fn<|>()
|
||||
}
|
||||
",
|
||||
r"
|
||||
mod bar {
|
||||
mod baz {
|
||||
pub(crate) fn my_fn() {
|
||||
<|>todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn foo() {
|
||||
bar::baz::my_fn()
|
||||
}
|
||||
",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_function_in_another_file() {
|
||||
check_assist(
|
||||
add_function,
|
||||
r"
|
||||
//- /main.rs
|
||||
mod foo;
|
||||
|
||||
fn main() {
|
||||
foo::bar<|>()
|
||||
}
|
||||
//- /foo.rs
|
||||
",
|
||||
r"
|
||||
|
||||
|
||||
pub(crate) fn bar() {
|
||||
<|>todo!()
|
||||
}",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_function_not_applicable_if_function_already_exists() {
|
||||
check_assist_not_applicable(
|
||||
|
|
|
@ -17,7 +17,7 @@ mod doc_tests;
|
|||
pub mod utils;
|
||||
pub mod ast_transform;
|
||||
|
||||
use ra_db::FileRange;
|
||||
use ra_db::{FileId, FileRange};
|
||||
use ra_ide_db::RootDatabase;
|
||||
use ra_syntax::{TextRange, TextUnit};
|
||||
use ra_text_edit::TextEdit;
|
||||
|
@ -54,6 +54,7 @@ pub struct AssistAction {
|
|||
pub cursor_position: Option<TextUnit>,
|
||||
// FIXME: This belongs to `AssistLabel`
|
||||
pub target: Option<TextRange>,
|
||||
pub file: AssistFile,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -63,6 +64,18 @@ pub struct ResolvedAssist {
|
|||
pub action: AssistAction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AssistFile {
|
||||
CurrentFile,
|
||||
TargetFile(FileId),
|
||||
}
|
||||
|
||||
impl Default for AssistFile {
|
||||
fn default() -> Self {
|
||||
Self::CurrentFile
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all the assists applicable at the given position.
|
||||
///
|
||||
/// Assists are returned in the "unresolved" state, that is only labels are
|
||||
|
@ -184,7 +197,7 @@ mod helpers {
|
|||
use ra_ide_db::{symbol_index::SymbolsDatabase, RootDatabase};
|
||||
use test_utils::{add_cursor, assert_eq_text, extract_range_or_offset, RangeOrOffset};
|
||||
|
||||
use crate::{AssistCtx, AssistHandler};
|
||||
use crate::{AssistCtx, AssistFile, AssistHandler};
|
||||
use hir::Semantics;
|
||||
|
||||
pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) {
|
||||
|
@ -246,7 +259,13 @@ mod helpers {
|
|||
(Some(assist), ExpectedResult::After(after)) => {
|
||||
let action = assist.0[0].action.clone().unwrap();
|
||||
|
||||
let mut actual = action.edit.apply(&text_without_caret);
|
||||
let assisted_file_text = if let AssistFile::TargetFile(file_id) = action.file {
|
||||
db.file_text(file_id).as_ref().to_owned()
|
||||
} else {
|
||||
text_without_caret
|
||||
};
|
||||
|
||||
let mut actual = action.edit.apply(&assisted_file_text);
|
||||
match action.cursor_position {
|
||||
None => {
|
||||
if let RangeOrOffset::Offset(before_cursor_pos) = range_or_offset {
|
||||
|
|
|
@ -37,6 +37,10 @@ fn action_to_edit(
|
|||
file_id: FileId,
|
||||
assist_label: &AssistLabel,
|
||||
) -> SourceChange {
|
||||
let file_id = match action.file {
|
||||
ra_assists::AssistFile::TargetFile(it) => it,
|
||||
_ => file_id,
|
||||
};
|
||||
let file_edit = SourceFileEdit { file_id, edit: action.edit };
|
||||
SourceChange::source_file_edit(assist_label.label.clone(), file_edit)
|
||||
.with_cursor_opt(action.cursor_position.map(|offset| FilePosition { offset, file_id }))
|
||||
|
|
82
crates/ra_ide/src/snapshots/highlight_strings.html
Normal file
82
crates/ra_ide/src/snapshots/highlight_strings.html
Normal file
|
@ -0,0 +1,82 @@
|
|||
|
||||
<style>
|
||||
body { margin: 0; }
|
||||
pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; }
|
||||
|
||||
.lifetime { color: #DFAF8F; font-style: italic; }
|
||||
.comment { color: #7F9F7F; }
|
||||
.struct, .enum { color: #7CB8BB; }
|
||||
.enum_variant { color: #BDE0F3; }
|
||||
.string_literal { color: #CC9393; }
|
||||
.field { color: #94BFF3; }
|
||||
.function { color: #93E0E3; }
|
||||
.parameter { color: #94BFF3; }
|
||||
.text { color: #DCDCCC; }
|
||||
.type { color: #7CB8BB; }
|
||||
.builtin_type { color: #8CD0D3; }
|
||||
.type_param { color: #DFAF8F; }
|
||||
.attribute { color: #94BFF3; }
|
||||
.numeric_literal { color: #BFEBBF; }
|
||||
.macro { color: #94BFF3; }
|
||||
.module { color: #AFD8AF; }
|
||||
.variable { color: #DCDCCC; }
|
||||
.mutable { text-decoration: underline; }
|
||||
|
||||
.keyword { color: #F0DFAF; font-weight: bold; }
|
||||
.keyword.unsafe { color: #BC8383; font-weight: bold; }
|
||||
.control { font-style: italic; }
|
||||
</style>
|
||||
<pre><code><span class="macro">macro_rules!</span> println {
|
||||
($($arg:tt)*) => ({
|
||||
$<span class="keyword">crate</span>::io::_print($<span class="keyword">crate</span>::format_args_nl!($($arg)*));
|
||||
})
|
||||
}
|
||||
#[rustc_builtin_macro]
|
||||
<span class="macro">macro_rules!</span> format_args_nl {
|
||||
($fmt:expr) => {{ <span class="comment">/* compiler built-in */</span> }};
|
||||
($fmt:expr, $($args:tt)*) => {{ <span class="comment">/* compiler built-in */</span> }};
|
||||
}
|
||||
|
||||
<span class="keyword">fn</span> <span class="function declaration">main</span>() {
|
||||
<span class="comment">// from https://doc.rust-lang.org/std/fmt/index.html</span>
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello"</span>); <span class="comment">// => "Hello"</span>
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello, </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="string_literal">"world"</span>); <span class="comment">// => "Hello, world!"</span>
|
||||
<span class="macro">println!</span>(<span class="string_literal">"The number is </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="numeric_literal">1</span>); <span class="comment">// => "The number is 1"</span>
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="attribute">:</span><span class="attribute">?</span><span class="attribute">}</span><span class="string_literal">"</span>, (<span class="numeric_literal">3</span>, <span class="numeric_literal">4</span>)); <span class="comment">// => "(3, 4)"</span>
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="variable">value</span><span class="attribute">}</span><span class="string_literal">"</span>, value=<span class="numeric_literal">4</span>); <span class="comment">// => "4"</span>
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="numeric_literal">1</span>, <span class="numeric_literal">2</span>); <span class="comment">// => "1 2"</span>
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="attribute">:</span><span class="numeric_literal">0</span><span class="numeric_literal">4</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="numeric_literal">42</span>); <span class="comment">// => "0042" with leading zerosV</span>
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="numeric_literal">1</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="numeric_literal">0</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="numeric_literal">1</span>, <span class="numeric_literal">2</span>); <span class="comment">// => "2 1 1 2"</span>
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="variable">argument</span><span class="attribute">}</span><span class="string_literal">"</span>, argument = <span class="string_literal">"test"</span>); <span class="comment">// => "test"</span>
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="variable">name</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="numeric_literal">1</span>, name = <span class="numeric_literal">2</span>); <span class="comment">// => "2 1"</span>
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="variable">a</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="variable">c</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="variable">b</span><span class="attribute">}</span><span class="string_literal">"</span>, a=<span class="string_literal">"a"</span>, b=<span class="char_literal">'b'</span>, c=<span class="numeric_literal">3</span>); <span class="comment">// => "a 3 b"</span>
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">:</span><span class="numeric_literal">5</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="string_literal">"x"</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">:</span><span class="numeric_literal">1</span><span class="attribute">$</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="string_literal">"x"</span>, <span class="numeric_literal">5</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="numeric_literal">1</span><span class="attribute">:</span><span class="numeric_literal">0</span><span class="attribute">$</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="numeric_literal">5</span>, <span class="string_literal">"x"</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">:</span><span class="variable">width</span><span class="attribute">$</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="string_literal">"x"</span>, width = <span class="numeric_literal">5</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">:</span><span class="attribute"><</span><span class="numeric_literal">5</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="string_literal">"x"</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">:</span><span class="attribute">-</span><span class="attribute"><</span><span class="numeric_literal">5</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="string_literal">"x"</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">:</span><span class="attribute">^</span><span class="numeric_literal">5</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="string_literal">"x"</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">:</span><span class="attribute">></span><span class="numeric_literal">5</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="string_literal">"x"</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">:</span><span class="attribute">+</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="numeric_literal">5</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="attribute">:</span><span class="attribute">#</span><span class="variable">x</span><span class="string_literal">}!"</span>, <span class="numeric_literal">27</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">:</span><span class="numeric_literal">0</span><span class="numeric_literal">5</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="numeric_literal">5</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">:</span><span class="numeric_literal">0</span><span class="numeric_literal">5</span><span class="attribute">}</span><span class="string_literal">!"</span>, -<span class="numeric_literal">5</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="attribute">:</span><span class="attribute">#</span><span class="numeric_literal">0</span><span class="numeric_literal">10</span><span class="variable">x</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="numeric_literal">27</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="numeric_literal">0</span><span class="attribute">}</span><span class="string_literal"> is </span><span class="attribute">{</span><span class="numeric_literal">1</span><span class="attribute">:</span><span class="attribute">.</span><span class="numeric_literal">5</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="string_literal">"x"</span>, <span class="numeric_literal">0.01</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="numeric_literal">1</span><span class="attribute">}</span><span class="string_literal"> is </span><span class="attribute">{</span><span class="numeric_literal">2</span><span class="attribute">:</span><span class="attribute">.</span><span class="numeric_literal">0</span><span class="attribute">$</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="numeric_literal">5</span>, <span class="string_literal">"x"</span>, <span class="numeric_literal">0.01</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="numeric_literal">0</span><span class="attribute">}</span><span class="string_literal"> is </span><span class="attribute">{</span><span class="numeric_literal">2</span><span class="attribute">:</span><span class="attribute">.</span><span class="numeric_literal">1</span><span class="attribute">$</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="string_literal">"x"</span>, <span class="numeric_literal">5</span>, <span class="numeric_literal">0.01</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal"> is </span><span class="attribute">{</span><span class="attribute">:</span><span class="attribute">.</span><span class="attribute">*</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="string_literal">"x"</span>, <span class="numeric_literal">5</span>, <span class="numeric_literal">0.01</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal"> is </span><span class="attribute">{</span><span class="numeric_literal">2</span><span class="attribute">:</span><span class="attribute">.</span><span class="attribute">*</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="string_literal">"x"</span>, <span class="numeric_literal">5</span>, <span class="numeric_literal">0.01</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal"> is </span><span class="attribute">{</span><span class="variable">number</span><span class="attribute">:</span><span class="attribute">.</span><span class="variable">prec</span><span class="attribute">$</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="string_literal">"x"</span>, prec = <span class="numeric_literal">5</span>, number = <span class="numeric_literal">0.01</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">, `</span><span class="attribute">{</span><span class="variable">name</span><span class="attribute">:</span><span class="attribute">.</span><span class="attribute">*</span><span class="attribute">}</span><span class="string_literal">` has 3 fractional digits"</span>, <span class="string_literal">"Hello"</span>, <span class="numeric_literal">3</span>, name=<span class="numeric_literal">1234.56</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">, `</span><span class="attribute">{</span><span class="variable">name</span><span class="attribute">:</span><span class="attribute">.</span><span class="attribute">*</span><span class="attribute">}</span><span class="string_literal">` has 3 characters"</span>, <span class="string_literal">"Hello"</span>, <span class="numeric_literal">3</span>, name=<span class="string_literal">"1234.56"</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">, `</span><span class="attribute">{</span><span class="variable">name</span><span class="attribute">:</span><span class="attribute">></span><span class="numeric_literal">8</span><span class="attribute">.</span><span class="attribute">*</span><span class="attribute">}</span><span class="string_literal">` has 3 right-aligned characters"</span>, <span class="string_literal">"Hello"</span>, <span class="numeric_literal">3</span>, name=<span class="string_literal">"1234.56"</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"Hello {{}}"</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"{{ Hello"</span>);
|
||||
|
||||
<span class="macro">println!</span>(<span class="string_literal">r"Hello, </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="string_literal">"world"</span>);
|
||||
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="variable">\x41</span><span class="attribute">}</span><span class="string_literal">"</span>, A = <span class="numeric_literal">92</span>);
|
||||
<span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="variable">ничоси</span><span class="attribute">}</span><span class="string_literal">"</span>, ничоси = <span class="numeric_literal">92</span>);
|
||||
}</code></pre>
|
|
@ -12,7 +12,7 @@ use ra_ide_db::{
|
|||
};
|
||||
use ra_prof::profile;
|
||||
use ra_syntax::{
|
||||
ast::{self, HasQuotes, HasStringValue},
|
||||
ast::{self, HasFormatSpecifier, HasQuotes, HasStringValue},
|
||||
AstNode, AstToken, Direction, NodeOrToken, SyntaxElement,
|
||||
SyntaxKind::*,
|
||||
SyntaxToken, TextRange, WalkEvent, T,
|
||||
|
@ -21,6 +21,7 @@ use rustc_hash::FxHashMap;
|
|||
|
||||
use crate::{call_info::ActiveParameter, Analysis, FileId};
|
||||
|
||||
use ast::FormatSpecifier;
|
||||
pub(crate) use html::highlight_as_html;
|
||||
pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag};
|
||||
|
||||
|
@ -31,6 +32,81 @@ pub struct HighlightedRange {
|
|||
pub binding_hash: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct HighlightedRangeStack {
|
||||
stack: Vec<Vec<HighlightedRange>>,
|
||||
}
|
||||
|
||||
/// We use a stack to implement the flattening logic for the highlighted
|
||||
/// syntax ranges.
|
||||
impl HighlightedRangeStack {
|
||||
fn new() -> Self {
|
||||
Self { stack: vec![Vec::new()] }
|
||||
}
|
||||
|
||||
fn push(&mut self) {
|
||||
self.stack.push(Vec::new());
|
||||
}
|
||||
|
||||
/// Flattens the highlighted ranges.
|
||||
///
|
||||
/// For example `#[cfg(feature = "foo")]` contains the nested ranges:
|
||||
/// 1) parent-range: Attribute [0, 23)
|
||||
/// 2) child-range: String [16, 21)
|
||||
///
|
||||
/// The following code implements the flattening, for our example this results to:
|
||||
/// `[Attribute [0, 16), String [16, 21), Attribute [21, 23)]`
|
||||
fn pop(&mut self) {
|
||||
let children = self.stack.pop().unwrap();
|
||||
let prev = self.stack.last_mut().unwrap();
|
||||
let needs_flattening = !children.is_empty()
|
||||
&& !prev.is_empty()
|
||||
&& children.first().unwrap().range.is_subrange(&prev.last().unwrap().range);
|
||||
if !needs_flattening {
|
||||
prev.extend(children);
|
||||
} else {
|
||||
let mut parent = prev.pop().unwrap();
|
||||
for ele in children {
|
||||
assert!(ele.range.is_subrange(&parent.range));
|
||||
let mut cloned = parent.clone();
|
||||
parent.range = TextRange::from_to(parent.range.start(), ele.range.start());
|
||||
cloned.range = TextRange::from_to(ele.range.end(), cloned.range.end());
|
||||
if !parent.range.is_empty() {
|
||||
prev.push(parent);
|
||||
}
|
||||
prev.push(ele);
|
||||
parent = cloned;
|
||||
}
|
||||
if !parent.range.is_empty() {
|
||||
prev.push(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, range: HighlightedRange) {
|
||||
self.stack
|
||||
.last_mut()
|
||||
.expect("during DFS traversal, the stack must not be empty")
|
||||
.push(range)
|
||||
}
|
||||
|
||||
fn flattened(mut self) -> Vec<HighlightedRange> {
|
||||
assert_eq!(
|
||||
self.stack.len(),
|
||||
1,
|
||||
"after DFS traversal, the stack should only contain a single element"
|
||||
);
|
||||
let mut res = self.stack.pop().unwrap();
|
||||
res.sort_by_key(|range| range.range.start());
|
||||
// Check that ranges are sorted and disjoint
|
||||
assert!(res
|
||||
.iter()
|
||||
.zip(res.iter().skip(1))
|
||||
.all(|(left, right)| left.range.end() <= right.range.start()));
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn highlight(
|
||||
db: &RootDatabase,
|
||||
file_id: FileId,
|
||||
|
@ -57,52 +133,18 @@ pub(crate) fn highlight(
|
|||
let mut bindings_shadow_count: FxHashMap<Name, u32> = FxHashMap::default();
|
||||
// We use a stack for the DFS traversal below.
|
||||
// When we leave a node, the we use it to flatten the highlighted ranges.
|
||||
let mut res: Vec<Vec<HighlightedRange>> = vec![Vec::new()];
|
||||
let mut stack = HighlightedRangeStack::new();
|
||||
|
||||
let mut current_macro_call: Option<ast::MacroCall> = None;
|
||||
let mut format_string: Option<SyntaxElement> = None;
|
||||
|
||||
// Walk all nodes, keeping track of whether we are inside a macro or not.
|
||||
// If in macro, expand it first and highlight the expanded code.
|
||||
for event in root.preorder_with_tokens() {
|
||||
match &event {
|
||||
WalkEvent::Enter(_) => res.push(Vec::new()),
|
||||
WalkEvent::Leave(_) => {
|
||||
/* Flattens the highlighted ranges.
|
||||
*
|
||||
* For example `#[cfg(feature = "foo")]` contains the nested ranges:
|
||||
* 1) parent-range: Attribute [0, 23)
|
||||
* 2) child-range: String [16, 21)
|
||||
*
|
||||
* The following code implements the flattening, for our example this results to:
|
||||
* `[Attribute [0, 16), String [16, 21), Attribute [21, 23)]`
|
||||
*/
|
||||
let children = res.pop().unwrap();
|
||||
let prev = res.last_mut().unwrap();
|
||||
let needs_flattening = !children.is_empty()
|
||||
&& !prev.is_empty()
|
||||
&& children.first().unwrap().range.is_subrange(&prev.last().unwrap().range);
|
||||
if !needs_flattening {
|
||||
prev.extend(children);
|
||||
} else {
|
||||
let mut parent = prev.pop().unwrap();
|
||||
for ele in children {
|
||||
assert!(ele.range.is_subrange(&parent.range));
|
||||
let mut cloned = parent.clone();
|
||||
parent.range = TextRange::from_to(parent.range.start(), ele.range.start());
|
||||
cloned.range = TextRange::from_to(ele.range.end(), cloned.range.end());
|
||||
if !parent.range.is_empty() {
|
||||
prev.push(parent);
|
||||
}
|
||||
prev.push(ele);
|
||||
parent = cloned;
|
||||
}
|
||||
if !parent.range.is_empty() {
|
||||
prev.push(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
WalkEvent::Enter(_) => stack.push(),
|
||||
WalkEvent::Leave(_) => stack.pop(),
|
||||
};
|
||||
let current = res.last_mut().expect("during DFS traversal, the stack must not be empty");
|
||||
|
||||
let event_range = match &event {
|
||||
WalkEvent::Enter(it) => it.text_range(),
|
||||
|
@ -119,7 +161,7 @@ pub(crate) fn highlight(
|
|||
WalkEvent::Enter(Some(mc)) => {
|
||||
current_macro_call = Some(mc.clone());
|
||||
if let Some(range) = macro_call_range(&mc) {
|
||||
current.push(HighlightedRange {
|
||||
stack.add(HighlightedRange {
|
||||
range,
|
||||
highlight: HighlightTag::Macro.into(),
|
||||
binding_hash: None,
|
||||
|
@ -130,6 +172,7 @@ pub(crate) fn highlight(
|
|||
WalkEvent::Leave(Some(mc)) => {
|
||||
assert!(current_macro_call == Some(mc));
|
||||
current_macro_call = None;
|
||||
format_string = None;
|
||||
continue;
|
||||
}
|
||||
_ => (),
|
||||
|
@ -150,6 +193,30 @@ pub(crate) fn highlight(
|
|||
};
|
||||
let token = sema.descend_into_macros(token.clone());
|
||||
let parent = token.parent();
|
||||
|
||||
// Check if macro takes a format string and remember it for highlighting later.
|
||||
// The macros that accept a format string expand to a compiler builtin macros
|
||||
// `format_args` and `format_args_nl`.
|
||||
if let Some(fmt_macro_call) = parent.parent().and_then(ast::MacroCall::cast) {
|
||||
if let Some(name) =
|
||||
fmt_macro_call.path().and_then(|p| p.segment()).and_then(|s| s.name_ref())
|
||||
{
|
||||
match name.text().as_str() {
|
||||
"format_args" | "format_args_nl" => {
|
||||
format_string = parent
|
||||
.children_with_tokens()
|
||||
.filter(|t| t.kind() != WHITESPACE)
|
||||
.nth(1)
|
||||
.filter(|e| {
|
||||
ast::String::can_cast(e.kind())
|
||||
|| ast::RawString::can_cast(e.kind())
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We only care Name and Name_ref
|
||||
match (token.kind(), parent.kind()) {
|
||||
(IDENT, NAME) | (IDENT, NAME_REF) => parent.into(),
|
||||
|
@ -161,27 +228,72 @@ pub(crate) fn highlight(
|
|||
|
||||
if let Some(token) = element.as_token().cloned().and_then(ast::RawString::cast) {
|
||||
let expanded = element_to_highlight.as_token().unwrap().clone();
|
||||
if highlight_injection(current, &sema, token, expanded).is_some() {
|
||||
if highlight_injection(&mut stack, &sema, token, expanded).is_some() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let is_format_string = format_string.as_ref() == Some(&element_to_highlight);
|
||||
|
||||
if let Some((highlight, binding_hash)) =
|
||||
highlight_element(&sema, &mut bindings_shadow_count, element_to_highlight)
|
||||
highlight_element(&sema, &mut bindings_shadow_count, element_to_highlight.clone())
|
||||
{
|
||||
current.push(HighlightedRange { range, highlight, binding_hash });
|
||||
stack.add(HighlightedRange { range, highlight, binding_hash });
|
||||
if let Some(string) =
|
||||
element_to_highlight.as_token().cloned().and_then(ast::String::cast)
|
||||
{
|
||||
stack.push();
|
||||
if is_format_string {
|
||||
string.lex_format_specifier(|piece_range, kind| {
|
||||
if let Some(highlight) = highlight_format_specifier(kind) {
|
||||
stack.add(HighlightedRange {
|
||||
range: piece_range + range.start(),
|
||||
highlight: highlight.into(),
|
||||
binding_hash: None,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
stack.pop();
|
||||
} else if let Some(string) =
|
||||
element_to_highlight.as_token().cloned().and_then(ast::RawString::cast)
|
||||
{
|
||||
stack.push();
|
||||
if is_format_string {
|
||||
string.lex_format_specifier(|piece_range, kind| {
|
||||
if let Some(highlight) = highlight_format_specifier(kind) {
|
||||
stack.add(HighlightedRange {
|
||||
range: piece_range + range.start(),
|
||||
highlight: highlight.into(),
|
||||
binding_hash: None,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(res.len(), 1, "after DFS traversal, the stack should only contain a single element");
|
||||
let mut res = res.pop().unwrap();
|
||||
res.sort_by_key(|range| range.range.start());
|
||||
// Check that ranges are sorted and disjoint
|
||||
assert!(res
|
||||
.iter()
|
||||
.zip(res.iter().skip(1))
|
||||
.all(|(left, right)| left.range.end() <= right.range.start()));
|
||||
res
|
||||
stack.flattened()
|
||||
}
|
||||
|
||||
fn highlight_format_specifier(kind: FormatSpecifier) -> Option<HighlightTag> {
|
||||
Some(match kind {
|
||||
FormatSpecifier::Open
|
||||
| FormatSpecifier::Close
|
||||
| FormatSpecifier::Colon
|
||||
| FormatSpecifier::Fill
|
||||
| FormatSpecifier::Align
|
||||
| FormatSpecifier::Sign
|
||||
| FormatSpecifier::NumberSign
|
||||
| FormatSpecifier::DollarSign
|
||||
| FormatSpecifier::Dot
|
||||
| FormatSpecifier::Asterisk
|
||||
| FormatSpecifier::QuestionMark => HighlightTag::Attribute,
|
||||
FormatSpecifier::Integer | FormatSpecifier::Zero => HighlightTag::NumericLiteral,
|
||||
FormatSpecifier::Identifier => HighlightTag::Local,
|
||||
})
|
||||
}
|
||||
|
||||
fn macro_call_range(macro_call: &ast::MacroCall) -> Option<TextRange> {
|
||||
|
@ -359,7 +471,7 @@ fn highlight_name_by_syntax(name: ast::Name) -> Highlight {
|
|||
}
|
||||
|
||||
fn highlight_injection(
|
||||
acc: &mut Vec<HighlightedRange>,
|
||||
acc: &mut HighlightedRangeStack,
|
||||
sema: &Semantics<RootDatabase>,
|
||||
literal: ast::RawString,
|
||||
expanded: SyntaxToken,
|
||||
|
@ -372,7 +484,7 @@ fn highlight_injection(
|
|||
let (analysis, tmp_file_id) = Analysis::from_single_file(value);
|
||||
|
||||
if let Some(range) = literal.open_quote_text_range() {
|
||||
acc.push(HighlightedRange {
|
||||
acc.add(HighlightedRange {
|
||||
range,
|
||||
highlight: HighlightTag::StringLiteral.into(),
|
||||
binding_hash: None,
|
||||
|
@ -382,12 +494,12 @@ fn highlight_injection(
|
|||
for mut h in analysis.highlight(tmp_file_id).unwrap() {
|
||||
if let Some(r) = literal.map_range_up(h.range) {
|
||||
h.range = r;
|
||||
acc.push(h)
|
||||
acc.add(h)
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(range) = literal.close_quote_text_range() {
|
||||
acc.push(HighlightedRange {
|
||||
acc.add(HighlightedRange {
|
||||
range,
|
||||
highlight: HighlightTag::StringLiteral.into(),
|
||||
binding_hash: None,
|
||||
|
|
|
@ -168,3 +168,73 @@ macro_rules! test {}
|
|||
);
|
||||
let _ = analysis.highlight(file_id).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_highlighting() {
|
||||
// The format string detection is based on macro-expansion,
|
||||
// thus, we have to copy the macro definition from `std`
|
||||
let (analysis, file_id) = single_file(
|
||||
r#"
|
||||
macro_rules! println {
|
||||
($($arg:tt)*) => ({
|
||||
$crate::io::_print($crate::format_args_nl!($($arg)*));
|
||||
})
|
||||
}
|
||||
#[rustc_builtin_macro]
|
||||
macro_rules! format_args_nl {
|
||||
($fmt:expr) => {{ /* compiler built-in */ }};
|
||||
($fmt:expr, $($args:tt)*) => {{ /* compiler built-in */ }};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// from https://doc.rust-lang.org/std/fmt/index.html
|
||||
println!("Hello"); // => "Hello"
|
||||
println!("Hello, {}!", "world"); // => "Hello, world!"
|
||||
println!("The number is {}", 1); // => "The number is 1"
|
||||
println!("{:?}", (3, 4)); // => "(3, 4)"
|
||||
println!("{value}", value=4); // => "4"
|
||||
println!("{} {}", 1, 2); // => "1 2"
|
||||
println!("{:04}", 42); // => "0042" with leading zerosV
|
||||
println!("{1} {} {0} {}", 1, 2); // => "2 1 1 2"
|
||||
println!("{argument}", argument = "test"); // => "test"
|
||||
println!("{name} {}", 1, name = 2); // => "2 1"
|
||||
println!("{a} {c} {b}", a="a", b='b', c=3); // => "a 3 b"
|
||||
println!("Hello {:5}!", "x");
|
||||
println!("Hello {:1$}!", "x", 5);
|
||||
println!("Hello {1:0$}!", 5, "x");
|
||||
println!("Hello {:width$}!", "x", width = 5);
|
||||
println!("Hello {:<5}!", "x");
|
||||
println!("Hello {:-<5}!", "x");
|
||||
println!("Hello {:^5}!", "x");
|
||||
println!("Hello {:>5}!", "x");
|
||||
println!("Hello {:+}!", 5);
|
||||
println!("{:#x}!", 27);
|
||||
println!("Hello {:05}!", 5);
|
||||
println!("Hello {:05}!", -5);
|
||||
println!("{:#010x}!", 27);
|
||||
println!("Hello {0} is {1:.5}", "x", 0.01);
|
||||
println!("Hello {1} is {2:.0$}", 5, "x", 0.01);
|
||||
println!("Hello {0} is {2:.1$}", "x", 5, 0.01);
|
||||
println!("Hello {} is {:.*}", "x", 5, 0.01);
|
||||
println!("Hello {} is {2:.*}", "x", 5, 0.01);
|
||||
println!("Hello {} is {number:.prec$}", "x", prec = 5, number = 0.01);
|
||||
println!("{}, `{name:.*}` has 3 fractional digits", "Hello", 3, name=1234.56);
|
||||
println!("{}, `{name:.*}` has 3 characters", "Hello", 3, name="1234.56");
|
||||
println!("{}, `{name:>8.*}` has 3 right-aligned characters", "Hello", 3, name="1234.56");
|
||||
println!("Hello {{}}");
|
||||
println!("{{ Hello");
|
||||
|
||||
println!(r"Hello, {}!", "world");
|
||||
|
||||
println!("{\x41}", A = 92);
|
||||
println!("{ничоси}", ничоси = 92);
|
||||
}"#
|
||||
.trim(),
|
||||
);
|
||||
|
||||
let dst_file = project_dir().join("crates/ra_ide/src/snapshots/highlight_strings.html");
|
||||
let actual_html = &analysis.highlight_as_html(file_id, false).unwrap();
|
||||
let expected_html = &read_text(&dst_file);
|
||||
fs::write(dst_file, &actual_html).unwrap();
|
||||
assert_eq_text!(expected_html, actual_html);
|
||||
}
|
||||
|
|
|
@ -293,11 +293,20 @@ pub fn fn_def(
|
|||
ast_from_text(&format!("fn {}{}{} {}", fn_name, type_params, params, body))
|
||||
}
|
||||
|
||||
pub fn add_newlines(amount_of_newlines: usize, t: impl AstNode) -> ast::SourceFile {
|
||||
pub fn add_leading_newlines(amount_of_newlines: usize, t: impl AstNode) -> ast::SourceFile {
|
||||
let newlines = "\n".repeat(amount_of_newlines);
|
||||
ast_from_text(&format!("{}{}", newlines, t.syntax()))
|
||||
}
|
||||
|
||||
pub fn add_trailing_newlines(amount_of_newlines: usize, t: impl AstNode) -> ast::SourceFile {
|
||||
let newlines = "\n".repeat(amount_of_newlines);
|
||||
ast_from_text(&format!("{}{}", t.syntax(), newlines))
|
||||
}
|
||||
|
||||
pub fn add_pub_crate_modifier(fn_def: ast::FnDef) -> ast::FnDef {
|
||||
ast_from_text(&format!("pub(crate) {}", fn_def))
|
||||
}
|
||||
|
||||
fn ast_from_text<N: AstNode>(text: &str) -> N {
|
||||
let parse = SourceFile::parse(text);
|
||||
let node = parse.tree().syntax().descendants().find_map(N::cast).unwrap();
|
||||
|
|
|
@ -172,3 +172,362 @@ impl RawString {
|
|||
Some(range + contents_range.start())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FormatSpecifier {
|
||||
Open,
|
||||
Close,
|
||||
Integer,
|
||||
Identifier,
|
||||
Colon,
|
||||
Fill,
|
||||
Align,
|
||||
Sign,
|
||||
NumberSign,
|
||||
Zero,
|
||||
DollarSign,
|
||||
Dot,
|
||||
Asterisk,
|
||||
QuestionMark,
|
||||
}
|
||||
|
||||
pub trait HasFormatSpecifier: AstToken {
|
||||
fn char_ranges(
|
||||
&self,
|
||||
) -> Option<Vec<(TextRange, Result<char, rustc_lexer::unescape::EscapeError>)>>;
|
||||
|
||||
fn lex_format_specifier<F>(&self, mut callback: F)
|
||||
where
|
||||
F: FnMut(TextRange, FormatSpecifier),
|
||||
{
|
||||
let char_ranges = if let Some(char_ranges) = self.char_ranges() {
|
||||
char_ranges
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let mut chars = char_ranges.iter().peekable();
|
||||
|
||||
while let Some((range, first_char)) = chars.next() {
|
||||
match first_char {
|
||||
Ok('{') => {
|
||||
// Format specifier, see syntax at https://doc.rust-lang.org/std/fmt/index.html#syntax
|
||||
if let Some((_, Ok('{'))) = chars.peek() {
|
||||
// Escaped format specifier, `{{`
|
||||
chars.next();
|
||||
continue;
|
||||
}
|
||||
|
||||
callback(*range, FormatSpecifier::Open);
|
||||
|
||||
// check for integer/identifier
|
||||
match chars
|
||||
.peek()
|
||||
.and_then(|next| next.1.as_ref().ok())
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
'0'..='9' => {
|
||||
// integer
|
||||
read_integer(&mut chars, &mut callback);
|
||||
}
|
||||
c if c == '_' || c.is_alphabetic() => {
|
||||
// identifier
|
||||
read_identifier(&mut chars, &mut callback);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some((_, Ok(':'))) = chars.peek() {
|
||||
skip_char_and_emit(&mut chars, FormatSpecifier::Colon, &mut callback);
|
||||
|
||||
// check for fill/align
|
||||
let mut cloned = chars.clone().take(2);
|
||||
let first = cloned
|
||||
.next()
|
||||
.and_then(|next| next.1.as_ref().ok())
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
let second = cloned
|
||||
.next()
|
||||
.and_then(|next| next.1.as_ref().ok())
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
match second {
|
||||
'<' | '^' | '>' => {
|
||||
// alignment specifier, first char specifies fillment
|
||||
skip_char_and_emit(
|
||||
&mut chars,
|
||||
FormatSpecifier::Fill,
|
||||
&mut callback,
|
||||
);
|
||||
skip_char_and_emit(
|
||||
&mut chars,
|
||||
FormatSpecifier::Align,
|
||||
&mut callback,
|
||||
);
|
||||
}
|
||||
_ => match first {
|
||||
'<' | '^' | '>' => {
|
||||
skip_char_and_emit(
|
||||
&mut chars,
|
||||
FormatSpecifier::Align,
|
||||
&mut callback,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
|
||||
// check for sign
|
||||
match chars
|
||||
.peek()
|
||||
.and_then(|next| next.1.as_ref().ok())
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
'+' | '-' => {
|
||||
skip_char_and_emit(
|
||||
&mut chars,
|
||||
FormatSpecifier::Sign,
|
||||
&mut callback,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// check for `#`
|
||||
if let Some((_, Ok('#'))) = chars.peek() {
|
||||
skip_char_and_emit(
|
||||
&mut chars,
|
||||
FormatSpecifier::NumberSign,
|
||||
&mut callback,
|
||||
);
|
||||
}
|
||||
|
||||
// check for `0`
|
||||
let mut cloned = chars.clone().take(2);
|
||||
let first = cloned.next().and_then(|next| next.1.as_ref().ok()).copied();
|
||||
let second = cloned.next().and_then(|next| next.1.as_ref().ok()).copied();
|
||||
|
||||
if first == Some('0') && second != Some('$') {
|
||||
skip_char_and_emit(&mut chars, FormatSpecifier::Zero, &mut callback);
|
||||
}
|
||||
|
||||
// width
|
||||
match chars
|
||||
.peek()
|
||||
.and_then(|next| next.1.as_ref().ok())
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
'0'..='9' => {
|
||||
read_integer(&mut chars, &mut callback);
|
||||
if let Some((_, Ok('$'))) = chars.peek() {
|
||||
skip_char_and_emit(
|
||||
&mut chars,
|
||||
FormatSpecifier::DollarSign,
|
||||
&mut callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
c if c == '_' || c.is_alphabetic() => {
|
||||
read_identifier(&mut chars, &mut callback);
|
||||
if chars.peek().and_then(|next| next.1.as_ref().ok()).copied()
|
||||
!= Some('$')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
skip_char_and_emit(
|
||||
&mut chars,
|
||||
FormatSpecifier::DollarSign,
|
||||
&mut callback,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// precision
|
||||
if let Some((_, Ok('.'))) = chars.peek() {
|
||||
skip_char_and_emit(&mut chars, FormatSpecifier::Dot, &mut callback);
|
||||
|
||||
match chars
|
||||
.peek()
|
||||
.and_then(|next| next.1.as_ref().ok())
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
'*' => {
|
||||
skip_char_and_emit(
|
||||
&mut chars,
|
||||
FormatSpecifier::Asterisk,
|
||||
&mut callback,
|
||||
);
|
||||
}
|
||||
'0'..='9' => {
|
||||
read_integer(&mut chars, &mut callback);
|
||||
if let Some((_, Ok('$'))) = chars.peek() {
|
||||
skip_char_and_emit(
|
||||
&mut chars,
|
||||
FormatSpecifier::DollarSign,
|
||||
&mut callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
c if c == '_' || c.is_alphabetic() => {
|
||||
read_identifier(&mut chars, &mut callback);
|
||||
if chars.peek().and_then(|next| next.1.as_ref().ok()).copied()
|
||||
!= Some('$')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
skip_char_and_emit(
|
||||
&mut chars,
|
||||
FormatSpecifier::DollarSign,
|
||||
&mut callback,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// type
|
||||
match chars
|
||||
.peek()
|
||||
.and_then(|next| next.1.as_ref().ok())
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
'?' => {
|
||||
skip_char_and_emit(
|
||||
&mut chars,
|
||||
FormatSpecifier::QuestionMark,
|
||||
&mut callback,
|
||||
);
|
||||
}
|
||||
c if c == '_' || c.is_alphabetic() => {
|
||||
read_identifier(&mut chars, &mut callback);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut cloned = chars.clone().take(2);
|
||||
let first = cloned.next().and_then(|next| next.1.as_ref().ok()).copied();
|
||||
let second = cloned.next().and_then(|next| next.1.as_ref().ok()).copied();
|
||||
if first != Some('}') {
|
||||
continue;
|
||||
}
|
||||
if second == Some('}') {
|
||||
// Escaped format end specifier, `}}`
|
||||
continue;
|
||||
}
|
||||
skip_char_and_emit(&mut chars, FormatSpecifier::Close, &mut callback);
|
||||
}
|
||||
_ => {
|
||||
while let Some((_, Ok(next_char))) = chars.peek() {
|
||||
match next_char {
|
||||
'{' => break,
|
||||
_ => {}
|
||||
}
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn skip_char_and_emit<'a, I, F>(
|
||||
chars: &mut std::iter::Peekable<I>,
|
||||
emit: FormatSpecifier,
|
||||
callback: &mut F,
|
||||
) where
|
||||
I: Iterator<Item = &'a (TextRange, Result<char, rustc_lexer::unescape::EscapeError>)>,
|
||||
F: FnMut(TextRange, FormatSpecifier),
|
||||
{
|
||||
let (range, _) = chars.next().unwrap();
|
||||
callback(*range, emit);
|
||||
}
|
||||
|
||||
fn read_integer<'a, I, F>(chars: &mut std::iter::Peekable<I>, callback: &mut F)
|
||||
where
|
||||
I: Iterator<Item = &'a (TextRange, Result<char, rustc_lexer::unescape::EscapeError>)>,
|
||||
F: FnMut(TextRange, FormatSpecifier),
|
||||
{
|
||||
let (mut range, c) = chars.next().unwrap();
|
||||
assert!(c.as_ref().unwrap().is_ascii_digit());
|
||||
while let Some((r, Ok(next_char))) = chars.peek() {
|
||||
if next_char.is_ascii_digit() {
|
||||
chars.next();
|
||||
range = range.extend_to(r);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
callback(range, FormatSpecifier::Integer);
|
||||
}
|
||||
|
||||
fn read_identifier<'a, I, F>(chars: &mut std::iter::Peekable<I>, callback: &mut F)
|
||||
where
|
||||
I: Iterator<Item = &'a (TextRange, Result<char, rustc_lexer::unescape::EscapeError>)>,
|
||||
F: FnMut(TextRange, FormatSpecifier),
|
||||
{
|
||||
let (mut range, c) = chars.next().unwrap();
|
||||
assert!(c.as_ref().unwrap().is_alphabetic() || *c.as_ref().unwrap() == '_');
|
||||
while let Some((r, Ok(next_char))) = chars.peek() {
|
||||
if *next_char == '_' || next_char.is_ascii_digit() || next_char.is_alphabetic() {
|
||||
chars.next();
|
||||
range = range.extend_to(r);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
callback(range, FormatSpecifier::Identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasFormatSpecifier for String {
|
||||
fn char_ranges(
|
||||
&self,
|
||||
) -> Option<Vec<(TextRange, Result<char, rustc_lexer::unescape::EscapeError>)>> {
|
||||
let text = self.text().as_str();
|
||||
let text = &text[self.text_range_between_quotes()? - self.syntax().text_range().start()];
|
||||
let offset = self.text_range_between_quotes()?.start() - self.syntax().text_range().start();
|
||||
|
||||
let mut res = Vec::with_capacity(text.len());
|
||||
rustc_lexer::unescape::unescape_str(text, &mut |range, unescaped_char| {
|
||||
res.push((
|
||||
TextRange::from_to(
|
||||
TextUnit::from_usize(range.start),
|
||||
TextUnit::from_usize(range.end),
|
||||
) + offset,
|
||||
unescaped_char,
|
||||
))
|
||||
});
|
||||
|
||||
Some(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasFormatSpecifier for RawString {
|
||||
fn char_ranges(
|
||||
&self,
|
||||
) -> Option<Vec<(TextRange, Result<char, rustc_lexer::unescape::EscapeError>)>> {
|
||||
let text = self.text().as_str();
|
||||
let text = &text[self.text_range_between_quotes()? - self.syntax().text_range().start()];
|
||||
let offset = self.text_range_between_quotes()?.start() - self.syntax().text_range().start();
|
||||
|
||||
let mut res = Vec::with_capacity(text.len());
|
||||
for (idx, c) in text.char_indices() {
|
||||
res.push((
|
||||
TextRange::from_to(
|
||||
TextUnit::from_usize(idx),
|
||||
TextUnit::from_usize(idx + c.len_utf8()),
|
||||
) + offset,
|
||||
Ok(c),
|
||||
));
|
||||
}
|
||||
Some(res)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ Adds a stub function with a signature matching the function under the cursor.
|
|||
struct Baz;
|
||||
fn baz() -> Baz { Baz }
|
||||
fn foo() {
|
||||
bar┃("", baz());
|
||||
bar┃("", baz());
|
||||
}
|
||||
|
||||
|
||||
|
@ -73,7 +73,7 @@ fn foo() {
|
|||
struct Baz;
|
||||
fn baz() -> Baz { Baz }
|
||||
fn foo() {
|
||||
bar("", baz());
|
||||
bar("", baz());
|
||||
}
|
||||
|
||||
fn bar(arg: &str, baz: Baz) {
|
||||
|
|
|
@ -37,11 +37,13 @@ export async function applySourceChange(ctx: Ctx, change: ra.SourceChange) {
|
|||
toReveal.position,
|
||||
);
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || editor.document.uri.toString() !== uri.toString()) {
|
||||
if (!editor || !editor.selection.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (!editor.selection.isEmpty) {
|
||||
return;
|
||||
|
||||
if (editor.document.uri !== uri) {
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
}
|
||||
editor.selection = new vscode.Selection(position, position);
|
||||
editor.revealRange(
|
||||
|
|
Loading…
Reference in a new issue