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:
bors[bot] 2020-04-24 20:10:54 +00:00 committed by GitHub
commit 51a0058d4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 940 additions and 95 deletions

View file

@ -10,7 +10,7 @@ use ra_syntax::{
}; };
use ra_text_edit::TextEditBuilder; use ra_text_edit::TextEditBuilder;
use crate::{AssistAction, AssistId, AssistLabel, GroupLabel, ResolvedAssist}; use crate::{AssistAction, AssistFile, AssistId, AssistLabel, GroupLabel, ResolvedAssist};
use algo::SyntaxRewriter; use algo::SyntaxRewriter;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -180,6 +180,7 @@ pub(crate) struct ActionBuilder {
edit: TextEditBuilder, edit: TextEditBuilder,
cursor_position: Option<TextUnit>, cursor_position: Option<TextUnit>,
target: Option<TextRange>, target: Option<TextRange>,
file: AssistFile,
} }
impl ActionBuilder { impl ActionBuilder {
@ -241,11 +242,16 @@ impl ActionBuilder {
algo::diff(&node, &new).into_text_edit(&mut self.edit) 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 { fn build(self) -> AssistAction {
AssistAction { AssistAction {
edit: self.edit.finish(), edit: self.edit.finish(),
cursor_position: self.cursor_position, cursor_position: self.cursor_position,
target: self.target, target: self.target,
file: self.file,
} }
} }
} }

View file

@ -3,8 +3,8 @@ use ra_syntax::{
SyntaxKind, SyntaxNode, TextUnit, SyntaxKind, SyntaxNode, TextUnit,
}; };
use crate::{Assist, AssistCtx, AssistId}; use crate::{Assist, AssistCtx, AssistFile, AssistId};
use ast::{edit::IndentLevel, ArgListOwner, CallExpr, Expr}; use ast::{edit::IndentLevel, ArgListOwner, ModuleItemOwner};
use hir::HirDisplay; use hir::HirDisplay;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
@ -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 call = path_expr.syntax().parent().and_then(ast::CallExpr::cast)?;
let path = path_expr.path()?; let path = path_expr.path()?;
if path.qualifier().is_some() {
return None;
}
if ctx.sema.resolve_path(&path).is_some() { if ctx.sema.resolve_path(&path).is_some() {
// The function call already resolves, no need to add a function // The function call already resolves, no need to add a function
return None; 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| { ctx.add_assist(AssistId("add_function"), "Add function", |edit| {
edit.target(call.syntax().text_range()); edit.target(call.syntax().text_range());
if let Some(function_template) = function_builder.render() { if let Some(function_template) = function_builder.render() {
edit.set_file(function_template.file);
edit.set_cursor(function_template.cursor_offset); edit.set_cursor(function_template.cursor_offset);
edit.insert(function_template.insert_offset, function_template.fn_def.to_string()); edit.insert(function_template.insert_offset, function_template.fn_def.to_string());
} }
@ -63,29 +72,67 @@ struct FunctionTemplate {
insert_offset: TextUnit, insert_offset: TextUnit,
cursor_offset: TextUnit, cursor_offset: TextUnit,
fn_def: ast::SourceFile, fn_def: ast::SourceFile,
file: AssistFile,
} }
struct FunctionBuilder { struct FunctionBuilder {
append_fn_at: SyntaxNode, target: GeneratedFunctionTarget,
fn_name: ast::Name, fn_name: ast::Name,
type_params: Option<ast::TypeParamList>, type_params: Option<ast::TypeParamList>,
params: ast::ParamList, params: ast::ParamList,
file: AssistFile,
needs_pub: bool,
} }
impl FunctionBuilder { impl FunctionBuilder {
fn from_call(ctx: &AssistCtx, call: &ast::CallExpr) -> Option<Self> { /// Prepares a generated function that matches `call` in `generate_in`
let append_fn_at = next_space_for_fn(&call)?; /// (or as close to `call` as possible, if `generate_in` is `None`)
let fn_name = fn_name(&call)?; 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)?; 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> { fn render(self) -> Option<FunctionTemplate> {
let placeholder_expr = ast::make::expr_todo(); let placeholder_expr = ast::make::expr_todo();
let fn_body = ast::make::block_expr(vec![], Some(placeholder_expr)); 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 mut 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); if self.needs_pub {
let fn_def = IndentLevel::from_node(&self.append_fn_at).increase_indent(fn_def); fn_def = ast::make::add_pub_crate_modifier(fn_def);
let insert_offset = self.append_fn_at.text_range().end(); }
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 let cursor_offset_from_fn_start = fn_def
.syntax() .syntax()
.descendants() .descendants()
@ -94,19 +141,24 @@ impl FunctionBuilder {
.text_range() .text_range()
.start(); .start();
let cursor_offset = insert_offset + cursor_offset_from_fn_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> { enum GeneratedFunctionTarget {
let name = call.expr()?.syntax().to_string(); 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)) Some(ast::make::name(&name))
} }
/// Computes the type variables and arguments required for the generated function /// Computes the type variables and arguments required for the generated function
fn fn_args( fn fn_args(
ctx: &AssistCtx, ctx: &AssistCtx,
call: &CallExpr, call: &ast::CallExpr,
) -> Option<(Option<ast::TypeParamList>, ast::ParamList)> { ) -> Option<(Option<ast::TypeParamList>, ast::ParamList)> {
let mut arg_names = Vec::new(); let mut arg_names = Vec::new();
let mut arg_types = 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 { 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( _ => Some(
fn_arg fn_arg
.syntax() .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)?; let ty = ctx.sema.type_of_expr(fn_arg)?;
if ty.is_unknown() { if ty.is_unknown() {
return None; return None;
@ -184,7 +236,7 @@ fn fn_arg_type(ctx: &AssistCtx, fn_arg: &Expr) -> Option<String> {
/// directly after the current block /// directly after the current block
/// We want to write the generated function directly after /// We want to write the generated function directly after
/// fns, impls or macro calls, but inside mods /// 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 ancestors = expr.syntax().ancestors().peekable();
let mut last_ancestor: Option<SyntaxNode> = None; let mut last_ancestor: Option<SyntaxNode> = None;
while let Some(next_ancestor) = ancestors.next() { 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 = 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)] #[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] #[test]
fn add_function_not_applicable_if_function_already_exists() { fn add_function_not_applicable_if_function_already_exists() {
check_assist_not_applicable( check_assist_not_applicable(

View file

@ -17,7 +17,7 @@ mod doc_tests;
pub mod utils; pub mod utils;
pub mod ast_transform; pub mod ast_transform;
use ra_db::FileRange; use ra_db::{FileId, FileRange};
use ra_ide_db::RootDatabase; use ra_ide_db::RootDatabase;
use ra_syntax::{TextRange, TextUnit}; use ra_syntax::{TextRange, TextUnit};
use ra_text_edit::TextEdit; use ra_text_edit::TextEdit;
@ -54,6 +54,7 @@ pub struct AssistAction {
pub cursor_position: Option<TextUnit>, pub cursor_position: Option<TextUnit>,
// FIXME: This belongs to `AssistLabel` // FIXME: This belongs to `AssistLabel`
pub target: Option<TextRange>, pub target: Option<TextRange>,
pub file: AssistFile,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -63,6 +64,18 @@ pub struct ResolvedAssist {
pub action: AssistAction, 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. /// Return all the assists applicable at the given position.
/// ///
/// Assists are returned in the "unresolved" state, that is only labels are /// 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 ra_ide_db::{symbol_index::SymbolsDatabase, RootDatabase};
use test_utils::{add_cursor, assert_eq_text, extract_range_or_offset, RangeOrOffset}; 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; use hir::Semantics;
pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) { pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) {
@ -246,7 +259,13 @@ mod helpers {
(Some(assist), ExpectedResult::After(after)) => { (Some(assist), ExpectedResult::After(after)) => {
let action = assist.0[0].action.clone().unwrap(); 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 { match action.cursor_position {
None => { None => {
if let RangeOrOffset::Offset(before_cursor_pos) = range_or_offset { if let RangeOrOffset::Offset(before_cursor_pos) = range_or_offset {

View file

@ -37,6 +37,10 @@ fn action_to_edit(
file_id: FileId, file_id: FileId,
assist_label: &AssistLabel, assist_label: &AssistLabel,
) -> SourceChange { ) -> SourceChange {
let file_id = match action.file {
ra_assists::AssistFile::TargetFile(it) => it,
_ => file_id,
};
let file_edit = SourceFileEdit { file_id, edit: action.edit }; let file_edit = SourceFileEdit { file_id, edit: action.edit };
SourceChange::source_file_edit(assist_label.label.clone(), file_edit) SourceChange::source_file_edit(assist_label.label.clone(), file_edit)
.with_cursor_opt(action.cursor_position.map(|offset| FilePosition { offset, file_id })) .with_cursor_opt(action.cursor_position.map(|offset| FilePosition { offset, file_id }))

View 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)*) =&gt; ({
$<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) =&gt; {{ <span class="comment">/* compiler built-in */</span> }};
($fmt:expr, $($args:tt)*) =&gt; {{ <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">// =&gt; "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">// =&gt; "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">// =&gt; "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">// =&gt; "(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">// =&gt; "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">// =&gt; "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">// =&gt; "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">// =&gt; "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">// =&gt; "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">// =&gt; "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">// =&gt; "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">&lt;</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">&lt;</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">&gt;</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">&gt;</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>

View file

@ -12,7 +12,7 @@ use ra_ide_db::{
}; };
use ra_prof::profile; use ra_prof::profile;
use ra_syntax::{ use ra_syntax::{
ast::{self, HasQuotes, HasStringValue}, ast::{self, HasFormatSpecifier, HasQuotes, HasStringValue},
AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, AstNode, AstToken, Direction, NodeOrToken, SyntaxElement,
SyntaxKind::*, SyntaxKind::*,
SyntaxToken, TextRange, WalkEvent, T, SyntaxToken, TextRange, WalkEvent, T,
@ -21,6 +21,7 @@ use rustc_hash::FxHashMap;
use crate::{call_info::ActiveParameter, Analysis, FileId}; use crate::{call_info::ActiveParameter, Analysis, FileId};
use ast::FormatSpecifier;
pub(crate) use html::highlight_as_html; pub(crate) use html::highlight_as_html;
pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag}; pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag};
@ -31,6 +32,81 @@ pub struct HighlightedRange {
pub binding_hash: Option<u64>, 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( pub(crate) fn highlight(
db: &RootDatabase, db: &RootDatabase,
file_id: FileId, file_id: FileId,
@ -57,52 +133,18 @@ pub(crate) fn highlight(
let mut bindings_shadow_count: FxHashMap<Name, u32> = FxHashMap::default(); let mut bindings_shadow_count: FxHashMap<Name, u32> = FxHashMap::default();
// We use a stack for the DFS traversal below. // We use a stack for the DFS traversal below.
// When we leave a node, the we use it to flatten the highlighted ranges. // 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 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. // 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. // If in macro, expand it first and highlight the expanded code.
for event in root.preorder_with_tokens() { for event in root.preorder_with_tokens() {
match &event { match &event {
WalkEvent::Enter(_) => res.push(Vec::new()), WalkEvent::Enter(_) => stack.push(),
WalkEvent::Leave(_) => { WalkEvent::Leave(_) => stack.pop(),
/* 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);
}
}
}
}; };
let current = res.last_mut().expect("during DFS traversal, the stack must not be empty");
let event_range = match &event { let event_range = match &event {
WalkEvent::Enter(it) => it.text_range(), WalkEvent::Enter(it) => it.text_range(),
@ -119,7 +161,7 @@ pub(crate) fn highlight(
WalkEvent::Enter(Some(mc)) => { WalkEvent::Enter(Some(mc)) => {
current_macro_call = Some(mc.clone()); current_macro_call = Some(mc.clone());
if let Some(range) = macro_call_range(&mc) { if let Some(range) = macro_call_range(&mc) {
current.push(HighlightedRange { stack.add(HighlightedRange {
range, range,
highlight: HighlightTag::Macro.into(), highlight: HighlightTag::Macro.into(),
binding_hash: None, binding_hash: None,
@ -130,6 +172,7 @@ pub(crate) fn highlight(
WalkEvent::Leave(Some(mc)) => { WalkEvent::Leave(Some(mc)) => {
assert!(current_macro_call == Some(mc)); assert!(current_macro_call == Some(mc));
current_macro_call = None; current_macro_call = None;
format_string = None;
continue; continue;
} }
_ => (), _ => (),
@ -150,6 +193,30 @@ pub(crate) fn highlight(
}; };
let token = sema.descend_into_macros(token.clone()); let token = sema.descend_into_macros(token.clone());
let parent = token.parent(); 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 // We only care Name and Name_ref
match (token.kind(), parent.kind()) { match (token.kind(), parent.kind()) {
(IDENT, NAME) | (IDENT, NAME_REF) => parent.into(), (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) { if let Some(token) = element.as_token().cloned().and_then(ast::RawString::cast) {
let expanded = element_to_highlight.as_token().unwrap().clone(); 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; continue;
} }
} }
let is_format_string = format_string.as_ref() == Some(&element_to_highlight);
if let Some((highlight, binding_hash)) = 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"); stack.flattened()
let mut res = res.pop().unwrap(); }
res.sort_by_key(|range| range.range.start());
// Check that ranges are sorted and disjoint fn highlight_format_specifier(kind: FormatSpecifier) -> Option<HighlightTag> {
assert!(res Some(match kind {
.iter() FormatSpecifier::Open
.zip(res.iter().skip(1)) | FormatSpecifier::Close
.all(|(left, right)| left.range.end() <= right.range.start())); | FormatSpecifier::Colon
res | 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> { 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( fn highlight_injection(
acc: &mut Vec<HighlightedRange>, acc: &mut HighlightedRangeStack,
sema: &Semantics<RootDatabase>, sema: &Semantics<RootDatabase>,
literal: ast::RawString, literal: ast::RawString,
expanded: SyntaxToken, expanded: SyntaxToken,
@ -372,7 +484,7 @@ fn highlight_injection(
let (analysis, tmp_file_id) = Analysis::from_single_file(value); let (analysis, tmp_file_id) = Analysis::from_single_file(value);
if let Some(range) = literal.open_quote_text_range() { if let Some(range) = literal.open_quote_text_range() {
acc.push(HighlightedRange { acc.add(HighlightedRange {
range, range,
highlight: HighlightTag::StringLiteral.into(), highlight: HighlightTag::StringLiteral.into(),
binding_hash: None, binding_hash: None,
@ -382,12 +494,12 @@ fn highlight_injection(
for mut h in analysis.highlight(tmp_file_id).unwrap() { for mut h in analysis.highlight(tmp_file_id).unwrap() {
if let Some(r) = literal.map_range_up(h.range) { if let Some(r) = literal.map_range_up(h.range) {
h.range = r; h.range = r;
acc.push(h) acc.add(h)
} }
} }
if let Some(range) = literal.close_quote_text_range() { if let Some(range) = literal.close_quote_text_range() {
acc.push(HighlightedRange { acc.add(HighlightedRange {
range, range,
highlight: HighlightTag::StringLiteral.into(), highlight: HighlightTag::StringLiteral.into(),
binding_hash: None, binding_hash: None,

View file

@ -168,3 +168,73 @@ macro_rules! test {}
); );
let _ = analysis.highlight(file_id).unwrap(); 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);
}

View file

@ -293,11 +293,20 @@ pub fn fn_def(
ast_from_text(&format!("fn {}{}{} {}", fn_name, type_params, params, body)) 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); let newlines = "\n".repeat(amount_of_newlines);
ast_from_text(&format!("{}{}", newlines, t.syntax())) 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 { fn ast_from_text<N: AstNode>(text: &str) -> N {
let parse = SourceFile::parse(text); let parse = SourceFile::parse(text);
let node = parse.tree().syntax().descendants().find_map(N::cast).unwrap(); let node = parse.tree().syntax().descendants().find_map(N::cast).unwrap();

View file

@ -172,3 +172,362 @@ impl RawString {
Some(range + contents_range.start()) 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)
}
}

View file

@ -37,11 +37,13 @@ export async function applySourceChange(ctx: Ctx, change: ra.SourceChange) {
toReveal.position, toReveal.position,
); );
const editor = vscode.window.activeTextEditor; const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.uri.toString() !== uri.toString()) { if (!editor || !editor.selection.isEmpty) {
return; 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.selection = new vscode.Selection(position, position);
editor.revealRange( editor.revealRange(