From 29a846464b63259f5152d61a5520bffcc2cb8703 Mon Sep 17 00:00:00 2001 From: Leander Tentrup Date: Thu, 16 Apr 2020 14:29:58 +0200 Subject: Refactor flattening logic for highlighted syntax ranges --- crates/ra_ide/src/syntax_highlighting.rs | 139 +++++++++++++++++++------------ 1 file changed, 85 insertions(+), 54 deletions(-) (limited to 'crates') diff --git a/crates/ra_ide/src/syntax_highlighting.rs b/crates/ra_ide/src/syntax_highlighting.rs index 7b15b82bd..e7d9bf696 100644 --- a/crates/ra_ide/src/syntax_highlighting.rs +++ b/crates/ra_ide/src/syntax_highlighting.rs @@ -31,6 +31,80 @@ pub struct HighlightedRange { pub binding_hash: Option, } +#[derive(Debug)] +struct HighlightedRangeStack { + stack: Vec>, +} + +/// 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 { + assert_eq!( + self.stack.len(), + 1, + "after DFS traversal, the stack should only contain a single element" + ); + let res = self.stack.pop().unwrap(); + // 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,7 +131,7 @@ pub(crate) fn highlight( let mut bindings_shadow_count: FxHashMap = 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![Vec::new()]; + let mut stack = HighlightedRangeStack::new(); let mut current_macro_call: Option = None; @@ -65,44 +139,9 @@ pub(crate) fn highlight( // 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 +158,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, @@ -161,7 +200,7 @@ 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; } } @@ -169,19 +208,11 @@ pub(crate) fn highlight( if let Some((highlight, binding_hash)) = highlight_element(&sema, &mut bindings_shadow_count, element_to_highlight) { - current.push(HighlightedRange { range, highlight, binding_hash }); + stack.add(HighlightedRange { range, highlight, binding_hash }); } } - 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 macro_call_range(macro_call: &ast::MacroCall) -> Option { @@ -358,7 +389,7 @@ fn highlight_name_by_syntax(name: ast::Name) -> Highlight { } fn highlight_injection( - acc: &mut Vec, + acc: &mut HighlightedRangeStack, sema: &Semantics, literal: ast::RawString, expanded: SyntaxToken, @@ -373,7 +404,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, @@ -383,12 +414,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, -- cgit v1.2.3 From ac798e1f7cfbc6d27c87bb28e3f1d5b6801796aa Mon Sep 17 00:00:00 2001 From: Leander Tentrup Date: Fri, 17 Apr 2020 09:37:18 +0200 Subject: Implement syntax highlighting for format strings Detailed changes: 1) Implement a lexer for string literals that divides the string in format specifier `{}` including the format specifier modifier. 2) Adapt syntax highlighting to add ranges for the detected sequences. 3) Add a test case for the format string syntax highlighting. --- crates/ra_ide/src/snapshots/highlight_strings.html | 77 +++++ crates/ra_ide/src/syntax_highlighting.rs | 69 ++++- crates/ra_ide/src/syntax_highlighting/tests.rs | 65 +++++ crates/ra_syntax/src/ast/tokens.rs | 324 +++++++++++++++++++++ 4 files changed, 532 insertions(+), 3 deletions(-) create mode 100644 crates/ra_ide/src/snapshots/highlight_strings.html (limited to 'crates') diff --git a/crates/ra_ide/src/snapshots/highlight_strings.html b/crates/ra_ide/src/snapshots/highlight_strings.html new file mode 100644 index 000000000..d70627da0 --- /dev/null +++ b/crates/ra_ide/src/snapshots/highlight_strings.html @@ -0,0 +1,77 @@ + + +
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");
+}
\ No newline at end of file diff --git a/crates/ra_ide/src/syntax_highlighting.rs b/crates/ra_ide/src/syntax_highlighting.rs index e7d9bf696..e342ca9df 100644 --- a/crates/ra_ide/src/syntax_highlighting.rs +++ b/crates/ra_ide/src/syntax_highlighting.rs @@ -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::call_info_for_token, Analysis, FileId}; +use ast::FormatSpecifier; pub(crate) use html::highlight_as_html; pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag}; @@ -95,7 +96,8 @@ impl HighlightedRangeStack { 1, "after DFS traversal, the stack should only contain a single element" ); - let res = self.stack.pop().unwrap(); + 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() @@ -134,6 +136,7 @@ pub(crate) fn highlight( let mut stack = HighlightedRangeStack::new(); let mut current_macro_call: Option = None; + let mut format_string: Option = 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. @@ -169,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; } _ => (), @@ -189,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 remeber 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(), @@ -205,10 +233,45 @@ pub(crate) fn highlight( } } + let is_format_string = + format_string.as_ref().map(|fs| fs == &element_to_highlight).unwrap_or_default(); + 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()) { 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(&mut |piece_range, kind| { + let highlight = 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, + }; + stack.add(HighlightedRange { + range: piece_range + range.start(), + highlight: highlight.into(), + binding_hash: None, + }); + }); + } + stack.pop(); + } } } diff --git a/crates/ra_ide/src/syntax_highlighting/tests.rs b/crates/ra_ide/src/syntax_highlighting/tests.rs index 73611e23a..f198767ce 100644 --- a/crates/ra_ide/src/syntax_highlighting/tests.rs +++ b/crates/ra_ide/src/syntax_highlighting/tests.rs @@ -168,3 +168,68 @@ 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"); +}"# + .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); +} diff --git a/crates/ra_syntax/src/ast/tokens.rs b/crates/ra_syntax/src/ast/tokens.rs index e8320b57e..ec3b4e553 100644 --- a/crates/ra_syntax/src/ast/tokens.rs +++ b/crates/ra_syntax/src/ast/tokens.rs @@ -172,3 +172,327 @@ 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 lex_format_specifier(&self, callback: &mut F) + where + F: FnMut(TextRange, FormatSpecifier), + { + let src = self.text().as_str(); + let initial_len = src.len(); + let mut chars = src.chars(); + + while let Some(first_char) = chars.next() { + match first_char { + '{' => { + // Format specifier, see syntax at https://doc.rust-lang.org/std/fmt/index.html#syntax + if chars.clone().next() == Some('{') { + // Escaped format specifier, `{{` + chars.next(); + continue; + } + + let start = initial_len - chars.as_str().len() - first_char.len_utf8(); + let end = initial_len - chars.as_str().len(); + callback( + TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), + FormatSpecifier::Open, + ); + + let next_char = if let Some(c) = chars.clone().next() { + c + } else { + break; + }; + + // check for integer/identifier + match next_char { + '0'..='9' => { + // integer + read_integer(&mut chars, initial_len, callback); + } + 'a'..='z' | 'A'..='Z' | '_' => { + // identifier + read_identifier(&mut chars, initial_len, callback); + } + _ => {} + } + + if chars.clone().next() == Some(':') { + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::Colon, + callback, + ); + + // check for fill/align + let mut cloned = chars.clone().take(2); + let first = cloned.next().unwrap_or_default(); + let second = cloned.next().unwrap_or_default(); + match second { + '<' | '^' | '>' => { + // alignment specifier, first char specifies fillment + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::Fill, + callback, + ); + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::Align, + callback, + ); + } + _ => match first { + '<' | '^' | '>' => { + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::Align, + callback, + ); + } + _ => {} + }, + } + + // check for sign + match chars.clone().next().unwrap_or_default() { + '+' | '-' => { + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::Sign, + callback, + ); + } + _ => {} + } + + // check for `#` + if let Some('#') = chars.clone().next() { + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::NumberSign, + callback, + ); + } + + // check for `0` + let mut cloned = chars.clone().take(2); + let first = cloned.next(); + let second = cloned.next(); + + if first == Some('0') && second != Some('$') { + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::Zero, + callback, + ); + } + + // width + match chars.clone().next().unwrap_or_default() { + '0'..='9' => { + read_integer(&mut chars, initial_len, callback); + if chars.clone().next() == Some('$') { + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::DollarSign, + callback, + ); + } + } + 'a'..='z' | 'A'..='Z' | '_' => { + read_identifier(&mut chars, initial_len, callback); + if chars.clone().next() != Some('$') { + continue; + } + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::DollarSign, + callback, + ); + } + _ => {} + } + + // precision + if chars.clone().next() == Some('.') { + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::Dot, + callback, + ); + + match chars.clone().next().unwrap_or_default() { + '*' => { + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::Asterisk, + callback, + ); + } + '0'..='9' => { + read_integer(&mut chars, initial_len, callback); + if chars.clone().next() == Some('$') { + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::DollarSign, + callback, + ); + } + } + 'a'..='z' | 'A'..='Z' | '_' => { + read_identifier(&mut chars, initial_len, callback); + if chars.clone().next() != Some('$') { + continue; + } + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::DollarSign, + callback, + ); + } + _ => { + continue; + } + } + } + + // type + match chars.clone().next().unwrap_or_default() { + '?' => { + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::QuestionMark, + callback, + ); + } + 'a'..='z' | 'A'..='Z' | '_' => { + read_identifier(&mut chars, initial_len, callback); + } + _ => {} + } + } + + let mut cloned = chars.clone().take(2); + let first = cloned.next(); + let second = cloned.next(); + if first != Some('}') { + continue; + } + if second == Some('}') { + // Escaped format end specifier, `}}` + continue; + } + skip_char_and_emit(&mut chars, initial_len, FormatSpecifier::Close, callback); + } + _ => { + while let Some(next_char) = chars.clone().next() { + match next_char { + '{' => break, + _ => {} + } + chars.next(); + } + } + }; + } + + fn skip_char_and_emit( + chars: &mut std::str::Chars, + initial_len: usize, + emit: FormatSpecifier, + callback: &mut F, + ) where + F: FnMut(TextRange, FormatSpecifier), + { + let start = initial_len - chars.as_str().len(); + chars.next(); + let end = initial_len - chars.as_str().len(); + callback( + TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), + emit, + ); + } + + fn read_integer(chars: &mut std::str::Chars, initial_len: usize, callback: &mut F) + where + F: FnMut(TextRange, FormatSpecifier), + { + let start = initial_len - chars.as_str().len(); + chars.next(); + while let Some(next_char) = chars.clone().next() { + match next_char { + '0'..='9' => { + chars.next(); + } + _ => { + break; + } + } + } + let end = initial_len - chars.as_str().len(); + callback( + TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), + FormatSpecifier::Integer, + ); + } + fn read_identifier(chars: &mut std::str::Chars, initial_len: usize, callback: &mut F) + where + F: FnMut(TextRange, FormatSpecifier), + { + let start = initial_len - chars.as_str().len(); + chars.next(); + while let Some(next_char) = chars.clone().next() { + match next_char { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => { + chars.next(); + } + _ => { + break; + } + } + } + let end = initial_len - chars.as_str().len(); + callback( + TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), + FormatSpecifier::Identifier, + ); + } + } +} + +impl HasFormatSpecifier for String {} +impl HasFormatSpecifier for RawString {} -- cgit v1.2.3 From 317fc650d5c1267a8c192041efe6b591d900808f Mon Sep 17 00:00:00 2001 From: Timo Freiberg Date: Thu, 16 Apr 2020 23:53:25 +0200 Subject: Make add_function generate functions in other modules via qualified path --- crates/ra_assists/src/doc_tests/generated.rs | 4 +- crates/ra_assists/src/handlers/add_function.rs | 219 ++++++++++++++++++++++--- crates/ra_syntax/src/ast/make.rs | 7 +- 3 files changed, 203 insertions(+), 27 deletions(-) (limited to 'crates') diff --git a/crates/ra_assists/src/doc_tests/generated.rs b/crates/ra_assists/src/doc_tests/generated.rs index b39e60870..e4fa9ee36 100644 --- a/crates/ra_assists/src/doc_tests/generated.rs +++ b/crates/ra_assists/src/doc_tests/generated.rs @@ -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) { diff --git a/crates/ra_assists/src/handlers/add_function.rs b/crates/ra_assists/src/handlers/add_function.rs index ad4ab66ed..a1261fe15 100644 --- a/crates/ra_assists/src/handlers/add_function.rs +++ b/crates/ra_assists/src/handlers/add_function.rs @@ -4,7 +4,7 @@ use ra_syntax::{ }; use crate::{Assist, AssistCtx, AssistId}; -use ast::{edit::IndentLevel, ArgListOwner, CallExpr, Expr}; +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,16 +38,24 @@ pub(crate) fn add_function(ctx: AssistCtx) -> Option { 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(resolved))) = + ctx.sema.resolve_path(&qualifier) + { + Some(resolved.definition_source(ctx.sema.db).value) + } 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()); @@ -66,26 +74,54 @@ struct FunctionTemplate { } struct FunctionBuilder { - append_fn_at: SyntaxNode, + target: GeneratedFunctionTarget, fn_name: ast::Name, type_params: Option, params: ast::ParamList, } impl FunctionBuilder { - fn from_call(ctx: &AssistCtx, call: &ast::CallExpr) -> Option { - 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, + generate_in: Option, + ) -> Option { + let target = if let Some(generate_in_module) = generate_in { + next_space_for_fn_in_module(generate_in_module)? + } 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 }) } fn render(self) -> Option { 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 (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 with_leading_newline = ast::make::add_leading_newlines(1, fn_def); + let indent = IndentLevel::from_node(it.syntax()).indented(); + let mut indented = indent.increase_indent(with_leading_newline); + if !item_list_has_whitespace(&it) { + // In this case we want to make sure there's a newline between the closing + // function brace and the closing module brace (so it doesn't end in `}}`). + indented = ast::make::add_trailing_newlines(1, indented); + } + (indented, it.syntax().text_range().start() + TextUnit::from_usize(1)) + } + }; + let cursor_offset_from_fn_start = fn_def .syntax() .descendants() @@ -98,15 +134,25 @@ impl FunctionBuilder { } } -fn fn_name(call: &CallExpr) -> Option { - let name = call.expr()?.syntax().to_string(); +/// Returns true if the given ItemList contains whitespace. +fn item_list_has_whitespace(it: &ast::ItemList) -> bool { + it.syntax().descendants_with_tokens().find(|it| it.kind() == SyntaxKind::WHITESPACE).is_some() +} + +enum GeneratedFunctionTarget { + BehindItem(SyntaxNode), + InEmptyItemList(ast::ItemList), +} + +fn fn_name(call: &ast::Path) -> Option { + 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::ParamList)> { let mut arg_names = Vec::new(); let mut arg_types = Vec::new(); @@ -158,9 +204,9 @@ fn deduplicate_arg_names(arg_names: &mut Vec) { } } -fn fn_arg_name(fn_arg: &Expr) -> Option { +fn fn_arg_name(fn_arg: &ast::Expr) -> Option { 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 +218,7 @@ fn fn_arg_name(fn_arg: &Expr) -> Option { } } -fn fn_arg_type(ctx: &AssistCtx, fn_arg: &Expr) -> Option { +fn fn_arg_type(ctx: &AssistCtx, fn_arg: &ast::Expr) -> Option { let ty = ctx.sema.type_of_expr(fn_arg)?; if ty.is_unknown() { return None; @@ -184,7 +230,7 @@ fn fn_arg_type(ctx: &AssistCtx, fn_arg: &Expr) -> Option { /// 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 { +fn next_space_for_fn_after_call_site(expr: &ast::CallExpr) -> Option { let mut ancestors = expr.syntax().ancestors().peekable(); let mut last_ancestor: Option = None; while let Some(next_ancestor) = ancestors.next() { @@ -201,7 +247,26 @@ fn next_space_for_fn(expr: &CallExpr) -> Option { } last_ancestor = Some(next_ancestor); } - last_ancestor + last_ancestor.map(GeneratedFunctionTarget::BehindItem) +} + +fn next_space_for_fn_in_module(module: hir::ModuleSource) -> Option { + match module { + hir::ModuleSource::SourceFile(it) => { + if let Some(last_item) = it.items().last() { + Some(GeneratedFunctionTarget::BehindItem(last_item.syntax().clone())) + } else { + Some(GeneratedFunctionTarget::BehindItem(it.syntax().clone())) + } + } + hir::ModuleSource::Module(it) => { + if let Some(last_item) = it.item_list().and_then(|it| it.items().last()) { + Some(GeneratedFunctionTarget::BehindItem(last_item.syntax().clone())) + } else { + it.item_list().map(GeneratedFunctionTarget::InEmptyItemList) + } + } + } } #[cfg(test)] @@ -713,6 +778,112 @@ 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 { + fn my_fn() { + <|>todo!() + } +} + +fn foo() { + bar::my_fn() +} +", + ); + check_assist( + add_function, + r" +mod bar { +} + +fn foo() { + bar::my_fn<|>() +} +", + r" +mod bar { + 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() {} + + 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 { + fn my_fn() { + <|>todo!() + } + } +} + +fn foo() { + bar::baz::my_fn() +} +", + ) + } + #[test] fn add_function_not_applicable_if_function_already_exists() { check_assist_not_applicable( diff --git a/crates/ra_syntax/src/ast/make.rs b/crates/ra_syntax/src/ast/make.rs index 0f4a50be4..b0f4803f3 100644 --- a/crates/ra_syntax/src/ast/make.rs +++ b/crates/ra_syntax/src/ast/make.rs @@ -293,11 +293,16 @@ 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)) +} + fn ast_from_text(text: &str) -> N { let parse = SourceFile::parse(text); let node = parse.tree().syntax().descendants().find_map(N::cast).unwrap(); -- cgit v1.2.3 From ba8faf3efc7a3a373571f98569699bbe684779b3 Mon Sep 17 00:00:00 2001 From: Timo Freiberg Date: Sat, 18 Apr 2020 16:12:21 +0200 Subject: Add target file information to AssistAction --- crates/ra_assists/src/assist_ctx.rs | 8 ++- crates/ra_assists/src/handlers/add_function.rs | 71 +++++++++++++++++++++----- crates/ra_assists/src/lib.rs | 15 +++++- crates/ra_ide/src/assists.rs | 4 ++ 4 files changed, 82 insertions(+), 16 deletions(-) (limited to 'crates') diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs index c3e653299..279163257 100644 --- a/crates/ra_assists/src/assist_ctx.rs +++ b/crates/ra_assists/src/assist_ctx.rs @@ -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, target: Option, + 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, } } } diff --git a/crates/ra_assists/src/handlers/add_function.rs b/crates/ra_assists/src/handlers/add_function.rs index a1261fe15..b65ded871 100644 --- a/crates/ra_assists/src/handlers/add_function.rs +++ b/crates/ra_assists/src/handlers/add_function.rs @@ -3,7 +3,7 @@ use ra_syntax::{ SyntaxKind, SyntaxNode, TextUnit, }; -use crate::{Assist, AssistCtx, AssistId}; +use crate::{Assist, AssistCtx, AssistFile, AssistId}; use ast::{edit::IndentLevel, ArgListOwner, ModuleItemOwner}; use hir::HirDisplay; use rustc_hash::{FxHashMap, FxHashSet}; @@ -44,10 +44,10 @@ pub(crate) fn add_function(ctx: AssistCtx) -> Option { } let target_module = if let Some(qualifier) = path.qualifier() { - if let Some(hir::PathResolution::Def(hir::ModuleDef::Module(resolved))) = + if let Some(hir::PathResolution::Def(hir::ModuleDef::Module(module))) = ctx.sema.resolve_path(&qualifier) { - Some(resolved.definition_source(ctx.sema.db).value) + Some(module.definition_source(ctx.sema.db)) } else { return None; } @@ -61,6 +61,7 @@ pub(crate) fn add_function(ctx: AssistCtx) -> Option { 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()); } @@ -71,6 +72,7 @@ struct FunctionTemplate { insert_offset: TextUnit, cursor_offset: TextUnit, fn_def: ast::SourceFile, + file: AssistFile, } struct FunctionBuilder { @@ -78,6 +80,7 @@ struct FunctionBuilder { fn_name: ast::Name, type_params: Option, params: ast::ParamList, + file: AssistFile, } impl FunctionBuilder { @@ -87,16 +90,19 @@ impl FunctionBuilder { ctx: &AssistCtx, call: &ast::CallExpr, path: &ast::Path, - generate_in: Option, + generate_in: Option>, ) -> Option { + let mut file = AssistFile::default(); let target = if let Some(generate_in_module) = generate_in { - next_space_for_fn_in_module(generate_in_module)? + let (in_file, target) = next_space_for_fn_in_module(ctx.sema.db, generate_in_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 { target, fn_name, type_params, params }) + Some(Self { target, fn_name, type_params, params, file }) } fn render(self) -> Option { let placeholder_expr = ast::make::expr_todo(); @@ -130,7 +136,7 @@ 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 }) } } @@ -250,23 +256,29 @@ fn next_space_for_fn_after_call_site(expr: &ast::CallExpr) -> Option Option { - match module { +fn next_space_for_fn_in_module( + db: &dyn hir::db::AstDatabase, + module: hir::InFile, +) -> 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() { - Some(GeneratedFunctionTarget::BehindItem(last_item.syntax().clone())) + GeneratedFunctionTarget::BehindItem(last_item.syntax().clone()) } else { - Some(GeneratedFunctionTarget::BehindItem(it.syntax().clone())) + GeneratedFunctionTarget::BehindItem(it.syntax().clone()) } } hir::ModuleSource::Module(it) => { if let Some(last_item) = it.item_list().and_then(|it| it.items().last()) { - Some(GeneratedFunctionTarget::BehindItem(last_item.syntax().clone())) + GeneratedFunctionTarget::BehindItem(last_item.syntax().clone()) } else { - it.item_list().map(GeneratedFunctionTarget::InEmptyItemList) + GeneratedFunctionTarget::InEmptyItemList(it.item_list()?) } } - } + }; + Some((assist_file, assist_item)) } #[cfg(test)] @@ -884,6 +896,37 @@ fn foo() { ) } + #[test] + fn add_function_in_another_file() { + check_assist( + add_function, + r" +//- /main.rs +mod foo; + +fn main() { + foo::bar<|>() +} + +//- /foo.rs + +", + r" +//- /main.rs +mod foo; + +fn main() { + foo::bar() +} + +//- /foo.rs +fn bar() { + <|>todo!() +} +", + ) + } + #[test] fn add_function_not_applicable_if_function_already_exists() { check_assist_not_applicable( diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index a00136da1..fb57486cb 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -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, // FIXME: This belongs to `AssistLabel` pub target: Option, + 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 diff --git a/crates/ra_ide/src/assists.rs b/crates/ra_ide/src/assists.rs index 40d56a4f7..2b5d11681 100644 --- a/crates/ra_ide/src/assists.rs +++ b/crates/ra_ide/src/assists.rs @@ -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 })) -- cgit v1.2.3 From 74780a15f65916d08942eb53c43b8e8c0b62cb48 Mon Sep 17 00:00:00 2001 From: Timo Freiberg Date: Mon, 20 Apr 2020 18:02:36 +0200 Subject: Jump to sourceChanges in other files --- crates/ra_assists/src/handlers/add_function.rs | 13 ++----------- crates/ra_assists/src/lib.rs | 10 ++++++++-- 2 files changed, 10 insertions(+), 13 deletions(-) (limited to 'crates') diff --git a/crates/ra_assists/src/handlers/add_function.rs b/crates/ra_assists/src/handlers/add_function.rs index b65ded871..9bd46f5dc 100644 --- a/crates/ra_assists/src/handlers/add_function.rs +++ b/crates/ra_assists/src/handlers/add_function.rs @@ -907,23 +907,14 @@ mod foo; fn main() { foo::bar<|>() } - //- /foo.rs - ", r" -//- /main.rs -mod foo; -fn main() { - foo::bar() -} -//- /foo.rs -fn bar() { +pub(crate) fn bar() { <|>todo!() -} -", +}", ) } diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index fb57486cb..ccc95735f 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -197,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) { @@ -259,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 { -- cgit v1.2.3 From f2f882bc44a85eb13276a8fbda7533d94e92e3af Mon Sep 17 00:00:00 2001 From: Timo Freiberg Date: Mon, 20 Apr 2020 18:36:12 +0200 Subject: Add `pub(crate)` to functions generated in other module --- crates/ra_assists/src/handlers/add_function.rs | 69 +++++++++----------------- crates/ra_syntax/src/ast/make.rs | 4 ++ 2 files changed, 27 insertions(+), 46 deletions(-) (limited to 'crates') diff --git a/crates/ra_assists/src/handlers/add_function.rs b/crates/ra_assists/src/handlers/add_function.rs index 9bd46f5dc..f185cffdb 100644 --- a/crates/ra_assists/src/handlers/add_function.rs +++ b/crates/ra_assists/src/handlers/add_function.rs @@ -81,6 +81,7 @@ struct FunctionBuilder { type_params: Option, params: ast::ParamList, file: AssistFile, + needs_pub: bool, } impl FunctionBuilder { @@ -90,11 +91,12 @@ impl FunctionBuilder { ctx: &AssistCtx, call: &ast::CallExpr, path: &ast::Path, - generate_in: Option>, + target_module: Option>, ) -> Option { + let needs_pub = target_module.is_some(); let mut file = AssistFile::default(); - let target = if let Some(generate_in_module) = generate_in { - let (in_file, target) = next_space_for_fn_in_module(ctx.sema.db, generate_in_module)?; + 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 { @@ -102,12 +104,16 @@ impl FunctionBuilder { }; let fn_name = fn_name(&path)?; let (type_params, params) = fn_args(ctx, &call)?; - Some(Self { target, fn_name, type_params, params, file }) + Some(Self { target, fn_name, type_params, params, file, needs_pub }) } + fn render(self) -> Option { 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 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) => { @@ -116,15 +122,14 @@ impl FunctionBuilder { (indented, it.text_range().end()) } GeneratedFunctionTarget::InEmptyItemList(it) => { - let with_leading_newline = ast::make::add_leading_newlines(1, fn_def); - let indent = IndentLevel::from_node(it.syntax()).indented(); - let mut indented = indent.increase_indent(with_leading_newline); - if !item_list_has_whitespace(&it) { - // In this case we want to make sure there's a newline between the closing - // function brace and the closing module brace (so it doesn't end in `}}`). - indented = ast::make::add_trailing_newlines(1, indented); - } - (indented, it.syntax().text_range().start() + TextUnit::from_usize(1)) + 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)) } }; @@ -140,11 +145,6 @@ impl FunctionBuilder { } } -/// Returns true if the given ItemList contains whitespace. -fn item_list_has_whitespace(it: &ast::ItemList) -> bool { - it.syntax().descendants_with_tokens().find(|it| it.kind() == SyntaxKind::WHITESPACE).is_some() -} - enum GeneratedFunctionTarget { BehindItem(SyntaxNode), InEmptyItemList(ast::ItemList), @@ -803,29 +803,7 @@ fn foo() { ", r" mod bar { - fn my_fn() { - <|>todo!() - } -} - -fn foo() { - bar::my_fn() -} -", - ); - check_assist( - add_function, - r" -mod bar { -} - -fn foo() { - bar::my_fn<|>() -} -", - r" -mod bar { - fn my_fn() { + pub(crate) fn my_fn() { <|>todo!() } } @@ -854,7 +832,7 @@ fn foo() { mod bar { fn something_else() {} - fn my_fn() { + pub(crate) fn my_fn() { <|>todo!() } } @@ -872,8 +850,7 @@ fn foo() { add_function, r" mod bar { - mod baz { - } + mod baz {} } fn foo() { @@ -883,7 +860,7 @@ fn foo() { r" mod bar { mod baz { - fn my_fn() { + pub(crate) fn my_fn() { <|>todo!() } } diff --git a/crates/ra_syntax/src/ast/make.rs b/crates/ra_syntax/src/ast/make.rs index b0f4803f3..ee0f5cc40 100644 --- a/crates/ra_syntax/src/ast/make.rs +++ b/crates/ra_syntax/src/ast/make.rs @@ -303,6 +303,10 @@ pub fn add_trailing_newlines(amount_of_newlines: usize, t: impl AstNode) -> ast: 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(text: &str) -> N { let parse = SourceFile::parse(text); let node = parse.tree().syntax().descendants().find_map(N::cast).unwrap(); -- cgit v1.2.3 From b2829a52161bc414f3b361c06b66633a234bba16 Mon Sep 17 00:00:00 2001 From: Leander Tentrup Date: Wed, 22 Apr 2020 10:08:46 +0200 Subject: Apply suggestions from code review Co-Authored-By: bjorn3 --- crates/ra_ide/src/syntax_highlighting.rs | 7 ++-- crates/ra_syntax/src/ast/tokens.rs | 59 ++++++++++++++++---------------- 2 files changed, 32 insertions(+), 34 deletions(-) (limited to 'crates') diff --git a/crates/ra_ide/src/syntax_highlighting.rs b/crates/ra_ide/src/syntax_highlighting.rs index e342ca9df..8ee3a78c6 100644 --- a/crates/ra_ide/src/syntax_highlighting.rs +++ b/crates/ra_ide/src/syntax_highlighting.rs @@ -194,7 +194,7 @@ pub(crate) fn highlight( let token = sema.descend_into_macros(token.clone()); let parent = token.parent(); - // Check if macro takes a format string and remeber it for highlighting later. + // 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) { @@ -233,8 +233,7 @@ pub(crate) fn highlight( } } - let is_format_string = - format_string.as_ref().map(|fs| fs == &element_to_highlight).unwrap_or_default(); + 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.clone()) @@ -245,7 +244,7 @@ pub(crate) fn highlight( { stack.push(); if is_format_string { - string.lex_format_specifier(&mut |piece_range, kind| { + string.lex_format_specifier(|piece_range, kind| { let highlight = match kind { FormatSpecifier::Open | FormatSpecifier::Close diff --git a/crates/ra_syntax/src/ast/tokens.rs b/crates/ra_syntax/src/ast/tokens.rs index ec3b4e553..3e5c56b19 100644 --- a/crates/ra_syntax/src/ast/tokens.rs +++ b/crates/ra_syntax/src/ast/tokens.rs @@ -192,7 +192,7 @@ pub enum FormatSpecifier { } pub trait HasFormatSpecifier: AstToken { - fn lex_format_specifier(&self, callback: &mut F) + fn lex_format_specifier(&self, mut callback: F) where F: FnMut(TextRange, FormatSpecifier), { @@ -217,21 +217,15 @@ pub trait HasFormatSpecifier: AstToken { FormatSpecifier::Open, ); - let next_char = if let Some(c) = chars.clone().next() { - c - } else { - break; - }; - // check for integer/identifier - match next_char { + match chars.clone().next().unwrap_or_default() { '0'..='9' => { // integer - read_integer(&mut chars, initial_len, callback); + read_integer(&mut chars, initial_len, &mut callback); } 'a'..='z' | 'A'..='Z' | '_' => { // identifier - read_identifier(&mut chars, initial_len, callback); + read_identifier(&mut chars, initial_len, &mut callback); } _ => {} } @@ -241,7 +235,7 @@ pub trait HasFormatSpecifier: AstToken { &mut chars, initial_len, FormatSpecifier::Colon, - callback, + &mut callback, ); // check for fill/align @@ -255,13 +249,13 @@ pub trait HasFormatSpecifier: AstToken { &mut chars, initial_len, FormatSpecifier::Fill, - callback, + &mut callback, ); skip_char_and_emit( &mut chars, initial_len, FormatSpecifier::Align, - callback, + &mut callback, ); } _ => match first { @@ -270,7 +264,7 @@ pub trait HasFormatSpecifier: AstToken { &mut chars, initial_len, FormatSpecifier::Align, - callback, + &mut callback, ); } _ => {} @@ -284,7 +278,7 @@ pub trait HasFormatSpecifier: AstToken { &mut chars, initial_len, FormatSpecifier::Sign, - callback, + &mut callback, ); } _ => {} @@ -296,7 +290,7 @@ pub trait HasFormatSpecifier: AstToken { &mut chars, initial_len, FormatSpecifier::NumberSign, - callback, + &mut callback, ); } @@ -310,25 +304,25 @@ pub trait HasFormatSpecifier: AstToken { &mut chars, initial_len, FormatSpecifier::Zero, - callback, + &mut callback, ); } // width match chars.clone().next().unwrap_or_default() { '0'..='9' => { - read_integer(&mut chars, initial_len, callback); + read_integer(&mut chars, initial_len, &mut callback); if chars.clone().next() == Some('$') { skip_char_and_emit( &mut chars, initial_len, FormatSpecifier::DollarSign, - callback, + &mut callback, ); } } 'a'..='z' | 'A'..='Z' | '_' => { - read_identifier(&mut chars, initial_len, callback); + read_identifier(&mut chars, initial_len, &mut callback); if chars.clone().next() != Some('$') { continue; } @@ -336,7 +330,7 @@ pub trait HasFormatSpecifier: AstToken { &mut chars, initial_len, FormatSpecifier::DollarSign, - callback, + &mut callback, ); } _ => {} @@ -348,7 +342,7 @@ pub trait HasFormatSpecifier: AstToken { &mut chars, initial_len, FormatSpecifier::Dot, - callback, + &mut callback, ); match chars.clone().next().unwrap_or_default() { @@ -357,22 +351,22 @@ pub trait HasFormatSpecifier: AstToken { &mut chars, initial_len, FormatSpecifier::Asterisk, - callback, + &mut callback, ); } '0'..='9' => { - read_integer(&mut chars, initial_len, callback); + read_integer(&mut chars, initial_len, &mut callback); if chars.clone().next() == Some('$') { skip_char_and_emit( &mut chars, initial_len, FormatSpecifier::DollarSign, - callback, + &mut callback, ); } } 'a'..='z' | 'A'..='Z' | '_' => { - read_identifier(&mut chars, initial_len, callback); + read_identifier(&mut chars, initial_len, &mut callback); if chars.clone().next() != Some('$') { continue; } @@ -380,7 +374,7 @@ pub trait HasFormatSpecifier: AstToken { &mut chars, initial_len, FormatSpecifier::DollarSign, - callback, + &mut callback, ); } _ => { @@ -396,11 +390,11 @@ pub trait HasFormatSpecifier: AstToken { &mut chars, initial_len, FormatSpecifier::QuestionMark, - callback, + &mut callback, ); } 'a'..='z' | 'A'..='Z' | '_' => { - read_identifier(&mut chars, initial_len, callback); + read_identifier(&mut chars, initial_len, &mut callback); } _ => {} } @@ -416,7 +410,12 @@ pub trait HasFormatSpecifier: AstToken { // Escaped format end specifier, `}}` continue; } - skip_char_and_emit(&mut chars, initial_len, FormatSpecifier::Close, callback); + skip_char_and_emit( + &mut chars, + initial_len, + FormatSpecifier::Close, + &mut callback, + ); } _ => { while let Some(next_char) = chars.clone().next() { -- cgit v1.2.3 From 445052f6d426043b543033f3fa4594fc1a09d7fa Mon Sep 17 00:00:00 2001 From: Leander Tentrup Date: Wed, 22 Apr 2020 15:28:35 +0200 Subject: Adapt format specifier highlighting to support escaped squences and unicode identifiers --- crates/ra_ide/src/snapshots/highlight_strings.html | 5 + crates/ra_ide/src/syntax_highlighting.rs | 63 +++-- crates/ra_ide/src/syntax_highlighting/tests.rs | 5 + crates/ra_syntax/src/ast/tokens.rs | 280 ++++++++++++--------- 4 files changed, 209 insertions(+), 144 deletions(-) (limited to 'crates') diff --git a/crates/ra_ide/src/snapshots/highlight_strings.html b/crates/ra_ide/src/snapshots/highlight_strings.html index d70627da0..433f2e0c5 100644 --- a/crates/ra_ide/src/snapshots/highlight_strings.html +++ b/crates/ra_ide/src/snapshots/highlight_strings.html @@ -74,4 +74,9 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd 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); } \ No newline at end of file diff --git a/crates/ra_ide/src/syntax_highlighting.rs b/crates/ra_ide/src/syntax_highlighting.rs index 8ee3a78c6..b5fd3390f 100644 --- a/crates/ra_ide/src/syntax_highlighting.rs +++ b/crates/ra_ide/src/syntax_highlighting.rs @@ -245,28 +245,29 @@ pub(crate) fn highlight( stack.push(); if is_format_string { string.lex_format_specifier(|piece_range, kind| { - let highlight = 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, - }; - stack.add(HighlightedRange { - range: piece_range + range.start(), - highlight: highlight.into(), - binding_hash: None, - }); + 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(); @@ -277,6 +278,24 @@ pub(crate) fn highlight( stack.flattened() } +fn highlight_format_specifier(kind: FormatSpecifier) -> Option { + 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 { let path = macro_call.path()?; let name_ref = path.segment()?.name_ref()?; diff --git a/crates/ra_ide/src/syntax_highlighting/tests.rs b/crates/ra_ide/src/syntax_highlighting/tests.rs index f198767ce..a9aae957f 100644 --- a/crates/ra_ide/src/syntax_highlighting/tests.rs +++ b/crates/ra_ide/src/syntax_highlighting/tests.rs @@ -223,6 +223,11 @@ fn main() { 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(), ); diff --git a/crates/ra_syntax/src/ast/tokens.rs b/crates/ra_syntax/src/ast/tokens.rs index 3e5c56b19..aa34b682d 100644 --- a/crates/ra_syntax/src/ast/tokens.rs +++ b/crates/ra_syntax/src/ast/tokens.rs @@ -192,68 +192,76 @@ pub enum FormatSpecifier { } pub trait HasFormatSpecifier: AstToken { + fn char_ranges( + &self, + ) -> Option)>>; + fn lex_format_specifier(&self, mut callback: F) where F: FnMut(TextRange, FormatSpecifier), { - let src = self.text().as_str(); - let initial_len = src.len(); - let mut chars = src.chars(); + 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(first_char) = chars.next() { + 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 chars.clone().next() == Some('{') { + if let Some((_, Ok('{'))) = chars.peek() { // Escaped format specifier, `{{` chars.next(); continue; } - let start = initial_len - chars.as_str().len() - first_char.len_utf8(); - let end = initial_len - chars.as_str().len(); - callback( - TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), - FormatSpecifier::Open, - ); + callback(*range, FormatSpecifier::Open); // check for integer/identifier - match chars.clone().next().unwrap_or_default() { + match chars + .peek() + .and_then(|next| next.1.as_ref().ok()) + .copied() + .unwrap_or_default() + { '0'..='9' => { // integer - read_integer(&mut chars, initial_len, &mut callback); + read_integer(&mut chars, &mut callback); } - 'a'..='z' | 'A'..='Z' | '_' => { + c if c == '_' || c.is_alphabetic() => { // identifier - read_identifier(&mut chars, initial_len, &mut callback); + read_identifier(&mut chars, &mut callback); } _ => {} } - if chars.clone().next() == Some(':') { - skip_char_and_emit( - &mut chars, - initial_len, - FormatSpecifier::Colon, - &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().unwrap_or_default(); - let second = cloned.next().unwrap_or_default(); + 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, - initial_len, FormatSpecifier::Fill, &mut callback, ); skip_char_and_emit( &mut chars, - initial_len, FormatSpecifier::Align, &mut callback, ); @@ -262,7 +270,6 @@ pub trait HasFormatSpecifier: AstToken { '<' | '^' | '>' => { skip_char_and_emit( &mut chars, - initial_len, FormatSpecifier::Align, &mut callback, ); @@ -272,11 +279,15 @@ pub trait HasFormatSpecifier: AstToken { } // check for sign - match chars.clone().next().unwrap_or_default() { + match chars + .peek() + .and_then(|next| next.1.as_ref().ok()) + .copied() + .unwrap_or_default() + { '+' | '-' => { skip_char_and_emit( &mut chars, - initial_len, FormatSpecifier::Sign, &mut callback, ); @@ -285,10 +296,9 @@ pub trait HasFormatSpecifier: AstToken { } // check for `#` - if let Some('#') = chars.clone().next() { + if let Some((_, Ok('#'))) = chars.peek() { skip_char_and_emit( &mut chars, - initial_len, FormatSpecifier::NumberSign, &mut callback, ); @@ -296,39 +306,39 @@ pub trait HasFormatSpecifier: AstToken { // check for `0` let mut cloned = chars.clone().take(2); - let first = cloned.next(); - let second = cloned.next(); + 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, - initial_len, - FormatSpecifier::Zero, - &mut callback, - ); + skip_char_and_emit(&mut chars, FormatSpecifier::Zero, &mut callback); } // width - match chars.clone().next().unwrap_or_default() { + match chars + .peek() + .and_then(|next| next.1.as_ref().ok()) + .copied() + .unwrap_or_default() + { '0'..='9' => { - read_integer(&mut chars, initial_len, &mut callback); - if chars.clone().next() == Some('$') { + read_integer(&mut chars, &mut callback); + if let Some((_, Ok('$'))) = chars.peek() { skip_char_and_emit( &mut chars, - initial_len, FormatSpecifier::DollarSign, &mut callback, ); } } - 'a'..='z' | 'A'..='Z' | '_' => { - read_identifier(&mut chars, initial_len, &mut callback); - if chars.clone().next() != Some('$') { + 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, - initial_len, FormatSpecifier::DollarSign, &mut callback, ); @@ -337,42 +347,41 @@ pub trait HasFormatSpecifier: AstToken { } // precision - if chars.clone().next() == Some('.') { - skip_char_and_emit( - &mut chars, - initial_len, - FormatSpecifier::Dot, - &mut callback, - ); - - match chars.clone().next().unwrap_or_default() { + 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, - initial_len, FormatSpecifier::Asterisk, &mut callback, ); } '0'..='9' => { - read_integer(&mut chars, initial_len, &mut callback); - if chars.clone().next() == Some('$') { + read_integer(&mut chars, &mut callback); + if let Some((_, Ok('$'))) = chars.peek() { skip_char_and_emit( &mut chars, - initial_len, FormatSpecifier::DollarSign, &mut callback, ); } } - 'a'..='z' | 'A'..='Z' | '_' => { - read_identifier(&mut chars, initial_len, &mut callback); - if chars.clone().next() != Some('$') { + 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, - initial_len, FormatSpecifier::DollarSign, &mut callback, ); @@ -384,25 +393,29 @@ pub trait HasFormatSpecifier: AstToken { } // type - match chars.clone().next().unwrap_or_default() { + match chars + .peek() + .and_then(|next| next.1.as_ref().ok()) + .copied() + .unwrap_or_default() + { '?' => { skip_char_and_emit( &mut chars, - initial_len, FormatSpecifier::QuestionMark, &mut callback, ); } - 'a'..='z' | 'A'..='Z' | '_' => { - read_identifier(&mut chars, initial_len, &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(); - let second = cloned.next(); + 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; } @@ -410,15 +423,10 @@ pub trait HasFormatSpecifier: AstToken { // Escaped format end specifier, `}}` continue; } - skip_char_and_emit( - &mut chars, - initial_len, - FormatSpecifier::Close, - &mut callback, - ); + skip_char_and_emit(&mut chars, FormatSpecifier::Close, &mut callback); } _ => { - while let Some(next_char) = chars.clone().next() { + while let Some((_, Ok(next_char))) = chars.peek() { match next_char { '{' => break, _ => {} @@ -429,69 +437,97 @@ pub trait HasFormatSpecifier: AstToken { }; } - fn skip_char_and_emit( - chars: &mut std::str::Chars, - initial_len: usize, + fn skip_char_and_emit<'a, I, F>( + chars: &mut std::iter::Peekable, emit: FormatSpecifier, callback: &mut F, ) where + I: Iterator)>, F: FnMut(TextRange, FormatSpecifier), { - let start = initial_len - chars.as_str().len(); - chars.next(); - let end = initial_len - chars.as_str().len(); - callback( - TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), - emit, - ); + let (range, _) = chars.next().unwrap(); + callback(*range, emit); } - fn read_integer(chars: &mut std::str::Chars, initial_len: usize, callback: &mut F) + fn read_integer<'a, I, F>(chars: &mut std::iter::Peekable, callback: &mut F) where + I: Iterator)>, F: FnMut(TextRange, FormatSpecifier), { - let start = initial_len - chars.as_str().len(); - chars.next(); - while let Some(next_char) = chars.clone().next() { - match next_char { - '0'..='9' => { - chars.next(); - } - _ => { - break; - } + 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; } } - let end = initial_len - chars.as_str().len(); - callback( - TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), - FormatSpecifier::Integer, - ); + callback(range, FormatSpecifier::Integer); } - fn read_identifier(chars: &mut std::str::Chars, initial_len: usize, callback: &mut F) + + fn read_identifier<'a, I, F>(chars: &mut std::iter::Peekable, callback: &mut F) where + I: Iterator)>, F: FnMut(TextRange, FormatSpecifier), { - let start = initial_len - chars.as_str().len(); - chars.next(); - while let Some(next_char) = chars.clone().next() { - match next_char { - 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => { - chars.next(); - } - _ => { - break; - } + 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; } } - let end = initial_len - chars.as_str().len(); - callback( - TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), - FormatSpecifier::Identifier, - ); + callback(range, FormatSpecifier::Identifier); } } } -impl HasFormatSpecifier for String {} -impl HasFormatSpecifier for RawString {} +impl HasFormatSpecifier for String { + fn char_ranges( + &self, + ) -> Option)>> { + 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)>> { + 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) + } +} -- cgit v1.2.3