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 000000000000..433f2e0c5e91
--- /dev/null
+++ b/crates/ra_ide/src/snapshots/highlight_strings.html
@@ -0,0 +1,82 @@
+
+
+
macro_rules! println {
+ ($($arg:tt)*) => ({
+ $crate::io::_print($crate::format_args_nl!($($arg)*));
+ })
+}
+#[rustc_builtin_macro]
+macro_rules! format_args_nl {
+ ($fmt:expr) => {{ }};
+ ($fmt:expr, $($args:tt)*) => {{ }};
+}
+
+fn main() {
+
+ println!("Hello");
+ println!("Hello, {}!", "world");
+ println!("The number is {}", 1);
+ println!("{:?}", (3, 4));
+ println!("{value}", value=4);
+ println!("{} {}", 1, 2);
+ println!("{:04}", 42);
+ println!("{1} {} {0} {}", 1, 2);
+ println!("{argument}", argument = "test");
+ println!("{name} {}", 1, name = 2);
+ println!("{a} {c} {b}", a="a", b='b', c=3);
+ 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);
+}
\ 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 7b15b82bdb6d..b5fd3390f20c 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};
@@ -31,6 +32,81 @@ 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 mut res = self.stack.pop().unwrap();
+ res.sort_by_key(|range| range.range.start());
+ // Check that ranges are sorted and disjoint
+ assert!(res
+ .iter()
+ .zip(res.iter().skip(1))
+ .all(|(left, right)| left.range.end() <= right.range.start()));
+ res
+ }
+}
+
pub(crate) fn highlight(
db: &RootDatabase,
file_id: FileId,
@@ -57,52 +133,18 @@ pub(crate) fn highlight(
let mut bindings_shadow_count: FxHashMap = 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;
+ 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.
for event in root.preorder_with_tokens() {
match &event {
- WalkEvent::Enter(_) => res.push(Vec::new()),
- WalkEvent::Leave(_) => {
- /* Flattens the highlighted ranges.
- *
- * For example `#[cfg(feature = "foo")]` contains the nested ranges:
- * 1) parent-range: Attribute [0, 23)
- * 2) child-range: String [16, 21)
- *
- * The following code implements the flattening, for our example this results to:
- * `[Attribute [0, 16), String [16, 21), Attribute [21, 23)]`
- */
- let children = res.pop().unwrap();
- let prev = res.last_mut().unwrap();
- let needs_flattening = !children.is_empty()
- && !prev.is_empty()
- && children.first().unwrap().range.is_subrange(&prev.last().unwrap().range);
- if !needs_flattening {
- prev.extend(children);
- } else {
- let mut parent = prev.pop().unwrap();
- for ele in children {
- assert!(ele.range.is_subrange(&parent.range));
- let mut cloned = parent.clone();
- parent.range = TextRange::from_to(parent.range.start(), ele.range.start());
- cloned.range = TextRange::from_to(ele.range.end(), cloned.range.end());
- if !parent.range.is_empty() {
- prev.push(parent);
- }
- prev.push(ele);
- parent = cloned;
- }
- if !parent.range.is_empty() {
- prev.push(parent);
- }
- }
- }
+ WalkEvent::Enter(_) => stack.push(),
+ WalkEvent::Leave(_) => stack.pop(),
};
- let current = res.last_mut().expect("during DFS traversal, the stack must not be empty");
let event_range = match &event {
WalkEvent::Enter(it) => it.text_range(),
@@ -119,7 +161,7 @@ pub(crate) fn highlight(
WalkEvent::Enter(Some(mc)) => {
current_macro_call = Some(mc.clone());
if let Some(range) = macro_call_range(&mc) {
- current.push(HighlightedRange {
+ stack.add(HighlightedRange {
range,
highlight: HighlightTag::Macro.into(),
binding_hash: None,
@@ -130,6 +172,7 @@ pub(crate) fn highlight(
WalkEvent::Leave(Some(mc)) => {
assert!(current_macro_call == Some(mc));
current_macro_call = None;
+ format_string = None;
continue;
}
_ => (),
@@ -150,6 +193,30 @@ pub(crate) fn highlight(
};
let token = sema.descend_into_macros(token.clone());
let parent = token.parent();
+
+ // Check if macro takes a format string and remember it for highlighting later.
+ // The macros that accept a format string expand to a compiler builtin macros
+ // `format_args` and `format_args_nl`.
+ if let Some(fmt_macro_call) = parent.parent().and_then(ast::MacroCall::cast) {
+ if let Some(name) =
+ fmt_macro_call.path().and_then(|p| p.segment()).and_then(|s| s.name_ref())
+ {
+ match name.text().as_str() {
+ "format_args" | "format_args_nl" => {
+ format_string = parent
+ .children_with_tokens()
+ .filter(|t| t.kind() != WHITESPACE)
+ .nth(1)
+ .filter(|e| {
+ ast::String::can_cast(e.kind())
+ || ast::RawString::can_cast(e.kind())
+ })
+ }
+ _ => {}
+ }
+ }
+ }
+
// We only care Name and Name_ref
match (token.kind(), parent.kind()) {
(IDENT, NAME) | (IDENT, NAME_REF) => parent.into(),
@@ -161,27 +228,72 @@ pub(crate) fn highlight(
if let Some(token) = element.as_token().cloned().and_then(ast::RawString::cast) {
let expanded = element_to_highlight.as_token().unwrap().clone();
- if highlight_injection(current, &sema, token, expanded).is_some() {
+ if highlight_injection(&mut stack, &sema, token, expanded).is_some() {
continue;
}
}
+ let is_format_string = format_string.as_ref() == Some(&element_to_highlight);
+
if let Some((highlight, binding_hash)) =
- highlight_element(&sema, &mut bindings_shadow_count, element_to_highlight)
+ highlight_element(&sema, &mut bindings_shadow_count, element_to_highlight.clone())
{
- current.push(HighlightedRange { range, highlight, binding_hash });
+ stack.add(HighlightedRange { range, highlight, binding_hash });
+ if let Some(string) =
+ element_to_highlight.as_token().cloned().and_then(ast::String::cast)
+ {
+ stack.push();
+ if is_format_string {
+ string.lex_format_specifier(|piece_range, kind| {
+ if let Some(highlight) = highlight_format_specifier(kind) {
+ stack.add(HighlightedRange {
+ range: piece_range + range.start(),
+ highlight: highlight.into(),
+ binding_hash: None,
+ });
+ }
+ });
+ }
+ stack.pop();
+ } else if let Some(string) =
+ element_to_highlight.as_token().cloned().and_then(ast::RawString::cast)
+ {
+ stack.push();
+ if is_format_string {
+ string.lex_format_specifier(|piece_range, kind| {
+ if let Some(highlight) = highlight_format_specifier(kind) {
+ stack.add(HighlightedRange {
+ range: piece_range + range.start(),
+ highlight: highlight.into(),
+ binding_hash: None,
+ });
+ }
+ });
+ }
+ stack.pop();
+ }
}
}
- assert_eq!(res.len(), 1, "after DFS traversal, the stack should only contain a single element");
- let mut res = res.pop().unwrap();
- res.sort_by_key(|range| range.range.start());
- // Check that ranges are sorted and disjoint
- assert!(res
- .iter()
- .zip(res.iter().skip(1))
- .all(|(left, right)| left.range.end() <= right.range.start()));
- res
+ stack.flattened()
+}
+
+fn highlight_format_specifier(kind: FormatSpecifier) -> Option {
+ 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 {
@@ -358,7 +470,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 +485,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 +495,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,
diff --git a/crates/ra_ide/src/syntax_highlighting/tests.rs b/crates/ra_ide/src/syntax_highlighting/tests.rs
index 73611e23a5a5..a9aae957f013 100644
--- a/crates/ra_ide/src/syntax_highlighting/tests.rs
+++ b/crates/ra_ide/src/syntax_highlighting/tests.rs
@@ -168,3 +168,73 @@ macro_rules! test {}
);
let _ = analysis.highlight(file_id).unwrap();
}
+
+#[test]
+fn test_string_highlighting() {
+ // The format string detection is based on macro-expansion,
+ // thus, we have to copy the macro definition from `std`
+ let (analysis, file_id) = single_file(
+ r#"
+macro_rules! println {
+ ($($arg:tt)*) => ({
+ $crate::io::_print($crate::format_args_nl!($($arg)*));
+ })
+}
+#[rustc_builtin_macro]
+macro_rules! format_args_nl {
+ ($fmt:expr) => {{ /* compiler built-in */ }};
+ ($fmt:expr, $($args:tt)*) => {{ /* compiler built-in */ }};
+}
+
+fn main() {
+ // from https://doc.rust-lang.org/std/fmt/index.html
+ println!("Hello"); // => "Hello"
+ println!("Hello, {}!", "world"); // => "Hello, world!"
+ println!("The number is {}", 1); // => "The number is 1"
+ println!("{:?}", (3, 4)); // => "(3, 4)"
+ println!("{value}", value=4); // => "4"
+ println!("{} {}", 1, 2); // => "1 2"
+ println!("{:04}", 42); // => "0042" with leading zerosV
+ println!("{1} {} {0} {}", 1, 2); // => "2 1 1 2"
+ println!("{argument}", argument = "test"); // => "test"
+ println!("{name} {}", 1, name = 2); // => "2 1"
+ println!("{a} {c} {b}", a="a", b='b', c=3); // => "a 3 b"
+ println!("Hello {:5}!", "x");
+ println!("Hello {:1$}!", "x", 5);
+ println!("Hello {1:0$}!", 5, "x");
+ println!("Hello {:width$}!", "x", width = 5);
+ println!("Hello {:<5}!", "x");
+ println!("Hello {:-<5}!", "x");
+ println!("Hello {:^5}!", "x");
+ println!("Hello {:>5}!", "x");
+ println!("Hello {:+}!", 5);
+ println!("{:#x}!", 27);
+ println!("Hello {:05}!", 5);
+ println!("Hello {:05}!", -5);
+ println!("{:#010x}!", 27);
+ println!("Hello {0} is {1:.5}", "x", 0.01);
+ println!("Hello {1} is {2:.0$}", 5, "x", 0.01);
+ println!("Hello {0} is {2:.1$}", "x", 5, 0.01);
+ println!("Hello {} is {:.*}", "x", 5, 0.01);
+ println!("Hello {} is {2:.*}", "x", 5, 0.01);
+ println!("Hello {} is {number:.prec$}", "x", prec = 5, number = 0.01);
+ println!("{}, `{name:.*}` has 3 fractional digits", "Hello", 3, name=1234.56);
+ println!("{}, `{name:.*}` has 3 characters", "Hello", 3, name="1234.56");
+ println!("{}, `{name:>8.*}` has 3 right-aligned characters", "Hello", 3, name="1234.56");
+ println!("Hello {{}}");
+ println!("{{ Hello");
+
+ println!(r"Hello, {}!", "world");
+
+ println!("{\x41}", A = 92);
+ println!("{ничоси}", ничоси = 92);
+}"#
+ .trim(),
+ );
+
+ let dst_file = project_dir().join("crates/ra_ide/src/snapshots/highlight_strings.html");
+ let actual_html = &analysis.highlight_as_html(file_id, false).unwrap();
+ let expected_html = &read_text(&dst_file);
+ fs::write(dst_file, &actual_html).unwrap();
+ assert_eq_text!(expected_html, actual_html);
+}
diff --git a/crates/ra_syntax/src/ast/tokens.rs b/crates/ra_syntax/src/ast/tokens.rs
index e8320b57ed45..aa34b682d92e 100644
--- a/crates/ra_syntax/src/ast/tokens.rs
+++ b/crates/ra_syntax/src/ast/tokens.rs
@@ -172,3 +172,362 @@ impl RawString {
Some(range + contents_range.start())
}
}
+
+#[derive(Debug)]
+pub enum FormatSpecifier {
+ Open,
+ Close,
+ Integer,
+ Identifier,
+ Colon,
+ Fill,
+ Align,
+ Sign,
+ NumberSign,
+ Zero,
+ DollarSign,
+ Dot,
+ Asterisk,
+ QuestionMark,
+}
+
+pub trait HasFormatSpecifier: AstToken {
+ fn char_ranges(
+ &self,
+ ) -> Option)>>;
+
+ fn lex_format_specifier(&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,
+ emit: FormatSpecifier,
+ callback: &mut F,
+ ) where
+ I: Iterator- )>,
+ F: FnMut(TextRange, FormatSpecifier),
+ {
+ let (range, _) = chars.next().unwrap();
+ callback(*range, emit);
+ }
+
+ fn read_integer<'a, I, F>(chars: &mut std::iter::Peekable, callback: &mut F)
+ where
+ I: Iterator
- )>,
+ 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, callback: &mut F)
+ where
+ I: Iterator
- )>,
+ 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)>> {
+ 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)
+ }
+}