diff options
author | Leander Tentrup <[email protected]> | 2020-04-17 08:37:18 +0100 |
---|---|---|
committer | Leander Tentrup <[email protected]> | 2020-04-20 10:19:15 +0100 |
commit | ac798e1f7cfbc6d27c87bb28e3f1d5b6801796aa (patch) | |
tree | 958ccd22ae5e75047b8189f69cf5878eeb9611ea /crates | |
parent | 29a846464b63259f5152d61a5520bffcc2cb8703 (diff) |
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.
Diffstat (limited to 'crates')
-rw-r--r-- | crates/ra_ide/src/snapshots/highlight_strings.html | 77 | ||||
-rw-r--r-- | crates/ra_ide/src/syntax_highlighting.rs | 69 | ||||
-rw-r--r-- | crates/ra_ide/src/syntax_highlighting/tests.rs | 65 | ||||
-rw-r--r-- | crates/ra_syntax/src/ast/tokens.rs | 324 |
4 files changed, 532 insertions, 3 deletions
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 @@ | |||
1 | |||
2 | <style> | ||
3 | body { margin: 0; } | ||
4 | pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } | ||
5 | |||
6 | .lifetime { color: #DFAF8F; font-style: italic; } | ||
7 | .comment { color: #7F9F7F; } | ||
8 | .struct, .enum { color: #7CB8BB; } | ||
9 | .enum_variant { color: #BDE0F3; } | ||
10 | .string_literal { color: #CC9393; } | ||
11 | .field { color: #94BFF3; } | ||
12 | .function { color: #93E0E3; } | ||
13 | .parameter { color: #94BFF3; } | ||
14 | .text { color: #DCDCCC; } | ||
15 | .type { color: #7CB8BB; } | ||
16 | .builtin_type { color: #8CD0D3; } | ||
17 | .type_param { color: #DFAF8F; } | ||
18 | .attribute { color: #94BFF3; } | ||
19 | .numeric_literal { color: #BFEBBF; } | ||
20 | .macro { color: #94BFF3; } | ||
21 | .module { color: #AFD8AF; } | ||
22 | .variable { color: #DCDCCC; } | ||
23 | .mutable { text-decoration: underline; } | ||
24 | |||
25 | .keyword { color: #F0DFAF; font-weight: bold; } | ||
26 | .keyword.unsafe { color: #BC8383; font-weight: bold; } | ||
27 | .control { font-style: italic; } | ||
28 | </style> | ||
29 | <pre><code><span class="macro">macro_rules!</span> println { | ||
30 | ($($arg:tt)*) => ({ | ||
31 | $<span class="keyword">crate</span>::io::_print($<span class="keyword">crate</span>::format_args_nl!($($arg)*)); | ||
32 | }) | ||
33 | } | ||
34 | #[rustc_builtin_macro] | ||
35 | <span class="macro">macro_rules!</span> format_args_nl { | ||
36 | ($fmt:expr) => {{ <span class="comment">/* compiler built-in */</span> }}; | ||
37 | ($fmt:expr, $($args:tt)*) => {{ <span class="comment">/* compiler built-in */</span> }}; | ||
38 | } | ||
39 | |||
40 | <span class="keyword">fn</span> <span class="function declaration">main</span>() { | ||
41 | <span class="comment">// from https://doc.rust-lang.org/std/fmt/index.html</span> | ||
42 | <span class="macro">println!</span>(<span class="string_literal">"Hello"</span>); <span class="comment">// => "Hello"</span> | ||
43 | <span class="macro">println!</span>(<span class="string_literal">"Hello, </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="string_literal">"world"</span>); <span class="comment">// => "Hello, world!"</span> | ||
44 | <span class="macro">println!</span>(<span class="string_literal">"The number is </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="numeric_literal">1</span>); <span class="comment">// => "The number is 1"</span> | ||
45 | <span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="attribute">:</span><span class="attribute">?</span><span class="attribute">}</span><span class="string_literal">"</span>, (<span class="numeric_literal">3</span>, <span class="numeric_literal">4</span>)); <span class="comment">// => "(3, 4)"</span> | ||
46 | <span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="variable">value</span><span class="attribute">}</span><span class="string_literal">"</span>, value=<span class="numeric_literal">4</span>); <span class="comment">// => "4"</span> | ||
47 | <span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="numeric_literal">1</span>, <span class="numeric_literal">2</span>); <span class="comment">// => "1 2"</span> | ||
48 | <span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="attribute">:</span><span class="numeric_literal">0</span><span class="numeric_literal">4</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="numeric_literal">42</span>); <span class="comment">// => "0042" with leading zerosV</span> | ||
49 | <span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="numeric_literal">1</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="numeric_literal">0</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="numeric_literal">1</span>, <span class="numeric_literal">2</span>); <span class="comment">// => "2 1 1 2"</span> | ||
50 | <span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="variable">argument</span><span class="attribute">}</span><span class="string_literal">"</span>, argument = <span class="string_literal">"test"</span>); <span class="comment">// => "test"</span> | ||
51 | <span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="variable">name</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">"</span>, <span class="numeric_literal">1</span>, name = <span class="numeric_literal">2</span>); <span class="comment">// => "2 1"</span> | ||
52 | <span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="variable">a</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="variable">c</span><span class="attribute">}</span><span class="string_literal"> </span><span class="attribute">{</span><span class="variable">b</span><span class="attribute">}</span><span class="string_literal">"</span>, a=<span class="string_literal">"a"</span>, b=<span class="char_literal">'b'</span>, c=<span class="numeric_literal">3</span>); <span class="comment">// => "a 3 b"</span> | ||
53 | <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>); | ||
54 | <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>); | ||
55 | <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>); | ||
56 | <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>); | ||
57 | <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>); | ||
58 | <span class="macro">println!</span>(<span class="string_literal">"Hello </span><span class="attribute">{</span><span class="attribute">:</span><span class="attribute">-</span><span class="attribute"><</span><span class="numeric_literal">5</span><span class="attribute">}</span><span class="string_literal">!"</span>, <span class="string_literal">"x"</span>); | ||
59 | <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>); | ||
60 | <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>); | ||
61 | <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>); | ||
62 | <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>); | ||
63 | <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>); | ||
64 | <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>); | ||
65 | <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>); | ||
66 | <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>); | ||
67 | <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>); | ||
68 | <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>); | ||
69 | <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>); | ||
70 | <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>); | ||
71 | <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>); | ||
72 | <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>); | ||
73 | <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>); | ||
74 | <span class="macro">println!</span>(<span class="string_literal">"</span><span class="attribute">{</span><span class="attribute">}</span><span class="string_literal">, `</span><span class="attribute">{</span><span class="variable">name</span><span class="attribute">:</span><span class="attribute">></span><span class="numeric_literal">8</span><span class="attribute">.</span><span class="attribute">*</span><span class="attribute">}</span><span class="string_literal">` has 3 right-aligned characters"</span>, <span class="string_literal">"Hello"</span>, <span class="numeric_literal">3</span>, name=<span class="string_literal">"1234.56"</span>); | ||
75 | <span class="macro">println!</span>(<span class="string_literal">"Hello {{}}"</span>); | ||
76 | <span class="macro">println!</span>(<span class="string_literal">"{{ Hello"</span>); | ||
77 | }</code></pre> \ 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::{ | |||
12 | }; | 12 | }; |
13 | use ra_prof::profile; | 13 | use ra_prof::profile; |
14 | use ra_syntax::{ | 14 | use ra_syntax::{ |
15 | ast::{self, HasQuotes, HasStringValue}, | 15 | ast::{self, HasFormatSpecifier, HasQuotes, HasStringValue}, |
16 | AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, | 16 | AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, |
17 | SyntaxKind::*, | 17 | SyntaxKind::*, |
18 | SyntaxToken, TextRange, WalkEvent, T, | 18 | SyntaxToken, TextRange, WalkEvent, T, |
@@ -21,6 +21,7 @@ use rustc_hash::FxHashMap; | |||
21 | 21 | ||
22 | use crate::{call_info::call_info_for_token, Analysis, FileId}; | 22 | use crate::{call_info::call_info_for_token, Analysis, FileId}; |
23 | 23 | ||
24 | use ast::FormatSpecifier; | ||
24 | pub(crate) use html::highlight_as_html; | 25 | pub(crate) use html::highlight_as_html; |
25 | pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag}; | 26 | pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag}; |
26 | 27 | ||
@@ -95,7 +96,8 @@ impl HighlightedRangeStack { | |||
95 | 1, | 96 | 1, |
96 | "after DFS traversal, the stack should only contain a single element" | 97 | "after DFS traversal, the stack should only contain a single element" |
97 | ); | 98 | ); |
98 | let res = self.stack.pop().unwrap(); | 99 | let mut res = self.stack.pop().unwrap(); |
100 | res.sort_by_key(|range| range.range.start()); | ||
99 | // Check that ranges are sorted and disjoint | 101 | // Check that ranges are sorted and disjoint |
100 | assert!(res | 102 | assert!(res |
101 | .iter() | 103 | .iter() |
@@ -134,6 +136,7 @@ pub(crate) fn highlight( | |||
134 | let mut stack = HighlightedRangeStack::new(); | 136 | let mut stack = HighlightedRangeStack::new(); |
135 | 137 | ||
136 | let mut current_macro_call: Option<ast::MacroCall> = None; | 138 | let mut current_macro_call: Option<ast::MacroCall> = None; |
139 | let mut format_string: Option<SyntaxElement> = None; | ||
137 | 140 | ||
138 | // Walk all nodes, keeping track of whether we are inside a macro or not. | 141 | // Walk all nodes, keeping track of whether we are inside a macro or not. |
139 | // If in macro, expand it first and highlight the expanded code. | 142 | // If in macro, expand it first and highlight the expanded code. |
@@ -169,6 +172,7 @@ pub(crate) fn highlight( | |||
169 | WalkEvent::Leave(Some(mc)) => { | 172 | WalkEvent::Leave(Some(mc)) => { |
170 | assert!(current_macro_call == Some(mc)); | 173 | assert!(current_macro_call == Some(mc)); |
171 | current_macro_call = None; | 174 | current_macro_call = None; |
175 | format_string = None; | ||
172 | continue; | 176 | continue; |
173 | } | 177 | } |
174 | _ => (), | 178 | _ => (), |
@@ -189,6 +193,30 @@ pub(crate) fn highlight( | |||
189 | }; | 193 | }; |
190 | let token = sema.descend_into_macros(token.clone()); | 194 | let token = sema.descend_into_macros(token.clone()); |
191 | let parent = token.parent(); | 195 | let parent = token.parent(); |
196 | |||
197 | // Check if macro takes a format string and remeber it for highlighting later. | ||
198 | // The macros that accept a format string expand to a compiler builtin macros | ||
199 | // `format_args` and `format_args_nl`. | ||
200 | if let Some(fmt_macro_call) = parent.parent().and_then(ast::MacroCall::cast) { | ||
201 | if let Some(name) = | ||
202 | fmt_macro_call.path().and_then(|p| p.segment()).and_then(|s| s.name_ref()) | ||
203 | { | ||
204 | match name.text().as_str() { | ||
205 | "format_args" | "format_args_nl" => { | ||
206 | format_string = parent | ||
207 | .children_with_tokens() | ||
208 | .filter(|t| t.kind() != WHITESPACE) | ||
209 | .nth(1) | ||
210 | .filter(|e| { | ||
211 | ast::String::can_cast(e.kind()) | ||
212 | || ast::RawString::can_cast(e.kind()) | ||
213 | }) | ||
214 | } | ||
215 | _ => {} | ||
216 | } | ||
217 | } | ||
218 | } | ||
219 | |||
192 | // We only care Name and Name_ref | 220 | // We only care Name and Name_ref |
193 | match (token.kind(), parent.kind()) { | 221 | match (token.kind(), parent.kind()) { |
194 | (IDENT, NAME) | (IDENT, NAME_REF) => parent.into(), | 222 | (IDENT, NAME) | (IDENT, NAME_REF) => parent.into(), |
@@ -205,10 +233,45 @@ pub(crate) fn highlight( | |||
205 | } | 233 | } |
206 | } | 234 | } |
207 | 235 | ||
236 | let is_format_string = | ||
237 | format_string.as_ref().map(|fs| fs == &element_to_highlight).unwrap_or_default(); | ||
238 | |||
208 | if let Some((highlight, binding_hash)) = | 239 | if let Some((highlight, binding_hash)) = |
209 | highlight_element(&sema, &mut bindings_shadow_count, element_to_highlight) | 240 | highlight_element(&sema, &mut bindings_shadow_count, element_to_highlight.clone()) |
210 | { | 241 | { |
211 | stack.add(HighlightedRange { range, highlight, binding_hash }); | 242 | stack.add(HighlightedRange { range, highlight, binding_hash }); |
243 | if let Some(string) = | ||
244 | element_to_highlight.as_token().cloned().and_then(ast::String::cast) | ||
245 | { | ||
246 | stack.push(); | ||
247 | if is_format_string { | ||
248 | string.lex_format_specifier(&mut |piece_range, kind| { | ||
249 | let highlight = match kind { | ||
250 | FormatSpecifier::Open | ||
251 | | FormatSpecifier::Close | ||
252 | | FormatSpecifier::Colon | ||
253 | | FormatSpecifier::Fill | ||
254 | | FormatSpecifier::Align | ||
255 | | FormatSpecifier::Sign | ||
256 | | FormatSpecifier::NumberSign | ||
257 | | FormatSpecifier::DollarSign | ||
258 | | FormatSpecifier::Dot | ||
259 | | FormatSpecifier::Asterisk | ||
260 | | FormatSpecifier::QuestionMark => HighlightTag::Attribute, | ||
261 | FormatSpecifier::Integer | FormatSpecifier::Zero => { | ||
262 | HighlightTag::NumericLiteral | ||
263 | } | ||
264 | FormatSpecifier::Identifier => HighlightTag::Local, | ||
265 | }; | ||
266 | stack.add(HighlightedRange { | ||
267 | range: piece_range + range.start(), | ||
268 | highlight: highlight.into(), | ||
269 | binding_hash: None, | ||
270 | }); | ||
271 | }); | ||
272 | } | ||
273 | stack.pop(); | ||
274 | } | ||
212 | } | 275 | } |
213 | } | 276 | } |
214 | 277 | ||
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 {} | |||
168 | ); | 168 | ); |
169 | let _ = analysis.highlight(file_id).unwrap(); | 169 | let _ = analysis.highlight(file_id).unwrap(); |
170 | } | 170 | } |
171 | |||
172 | #[test] | ||
173 | fn test_string_highlighting() { | ||
174 | // The format string detection is based on macro-expansion, | ||
175 | // thus, we have to copy the macro definition from `std` | ||
176 | let (analysis, file_id) = single_file( | ||
177 | r#" | ||
178 | macro_rules! println { | ||
179 | ($($arg:tt)*) => ({ | ||
180 | $crate::io::_print($crate::format_args_nl!($($arg)*)); | ||
181 | }) | ||
182 | } | ||
183 | #[rustc_builtin_macro] | ||
184 | macro_rules! format_args_nl { | ||
185 | ($fmt:expr) => {{ /* compiler built-in */ }}; | ||
186 | ($fmt:expr, $($args:tt)*) => {{ /* compiler built-in */ }}; | ||
187 | } | ||
188 | |||
189 | fn main() { | ||
190 | // from https://doc.rust-lang.org/std/fmt/index.html | ||
191 | println!("Hello"); // => "Hello" | ||
192 | println!("Hello, {}!", "world"); // => "Hello, world!" | ||
193 | println!("The number is {}", 1); // => "The number is 1" | ||
194 | println!("{:?}", (3, 4)); // => "(3, 4)" | ||
195 | println!("{value}", value=4); // => "4" | ||
196 | println!("{} {}", 1, 2); // => "1 2" | ||
197 | println!("{:04}", 42); // => "0042" with leading zerosV | ||
198 | println!("{1} {} {0} {}", 1, 2); // => "2 1 1 2" | ||
199 | println!("{argument}", argument = "test"); // => "test" | ||
200 | println!("{name} {}", 1, name = 2); // => "2 1" | ||
201 | println!("{a} {c} {b}", a="a", b='b', c=3); // => "a 3 b" | ||
202 | println!("Hello {:5}!", "x"); | ||
203 | println!("Hello {:1$}!", "x", 5); | ||
204 | println!("Hello {1:0$}!", 5, "x"); | ||
205 | println!("Hello {:width$}!", "x", width = 5); | ||
206 | println!("Hello {:<5}!", "x"); | ||
207 | println!("Hello {:-<5}!", "x"); | ||
208 | println!("Hello {:^5}!", "x"); | ||
209 | println!("Hello {:>5}!", "x"); | ||
210 | println!("Hello {:+}!", 5); | ||
211 | println!("{:#x}!", 27); | ||
212 | println!("Hello {:05}!", 5); | ||
213 | println!("Hello {:05}!", -5); | ||
214 | println!("{:#010x}!", 27); | ||
215 | println!("Hello {0} is {1:.5}", "x", 0.01); | ||
216 | println!("Hello {1} is {2:.0$}", 5, "x", 0.01); | ||
217 | println!("Hello {0} is {2:.1$}", "x", 5, 0.01); | ||
218 | println!("Hello {} is {:.*}", "x", 5, 0.01); | ||
219 | println!("Hello {} is {2:.*}", "x", 5, 0.01); | ||
220 | println!("Hello {} is {number:.prec$}", "x", prec = 5, number = 0.01); | ||
221 | println!("{}, `{name:.*}` has 3 fractional digits", "Hello", 3, name=1234.56); | ||
222 | println!("{}, `{name:.*}` has 3 characters", "Hello", 3, name="1234.56"); | ||
223 | println!("{}, `{name:>8.*}` has 3 right-aligned characters", "Hello", 3, name="1234.56"); | ||
224 | println!("Hello {{}}"); | ||
225 | println!("{{ Hello"); | ||
226 | }"# | ||
227 | .trim(), | ||
228 | ); | ||
229 | |||
230 | let dst_file = project_dir().join("crates/ra_ide/src/snapshots/highlight_strings.html"); | ||
231 | let actual_html = &analysis.highlight_as_html(file_id, false).unwrap(); | ||
232 | let expected_html = &read_text(&dst_file); | ||
233 | fs::write(dst_file, &actual_html).unwrap(); | ||
234 | assert_eq_text!(expected_html, actual_html); | ||
235 | } | ||
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 { | |||
172 | Some(range + contents_range.start()) | 172 | Some(range + contents_range.start()) |
173 | } | 173 | } |
174 | } | 174 | } |
175 | |||
176 | #[derive(Debug)] | ||
177 | pub enum FormatSpecifier { | ||
178 | Open, | ||
179 | Close, | ||
180 | Integer, | ||
181 | Identifier, | ||
182 | Colon, | ||
183 | Fill, | ||
184 | Align, | ||
185 | Sign, | ||
186 | NumberSign, | ||
187 | Zero, | ||
188 | DollarSign, | ||
189 | Dot, | ||
190 | Asterisk, | ||
191 | QuestionMark, | ||
192 | } | ||
193 | |||
194 | pub trait HasFormatSpecifier: AstToken { | ||
195 | fn lex_format_specifier<F>(&self, callback: &mut F) | ||
196 | where | ||
197 | F: FnMut(TextRange, FormatSpecifier), | ||
198 | { | ||
199 | let src = self.text().as_str(); | ||
200 | let initial_len = src.len(); | ||
201 | let mut chars = src.chars(); | ||
202 | |||
203 | while let Some(first_char) = chars.next() { | ||
204 | match first_char { | ||
205 | '{' => { | ||
206 | // Format specifier, see syntax at https://doc.rust-lang.org/std/fmt/index.html#syntax | ||
207 | if chars.clone().next() == Some('{') { | ||
208 | // Escaped format specifier, `{{` | ||
209 | chars.next(); | ||
210 | continue; | ||
211 | } | ||
212 | |||
213 | let start = initial_len - chars.as_str().len() - first_char.len_utf8(); | ||
214 | let end = initial_len - chars.as_str().len(); | ||
215 | callback( | ||
216 | TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), | ||
217 | FormatSpecifier::Open, | ||
218 | ); | ||
219 | |||
220 | let next_char = if let Some(c) = chars.clone().next() { | ||
221 | c | ||
222 | } else { | ||
223 | break; | ||
224 | }; | ||
225 | |||
226 | // check for integer/identifier | ||
227 | match next_char { | ||
228 | '0'..='9' => { | ||
229 | // integer | ||
230 | read_integer(&mut chars, initial_len, callback); | ||
231 | } | ||
232 | 'a'..='z' | 'A'..='Z' | '_' => { | ||
233 | // identifier | ||
234 | read_identifier(&mut chars, initial_len, callback); | ||
235 | } | ||
236 | _ => {} | ||
237 | } | ||
238 | |||
239 | if chars.clone().next() == Some(':') { | ||
240 | skip_char_and_emit( | ||
241 | &mut chars, | ||
242 | initial_len, | ||
243 | FormatSpecifier::Colon, | ||
244 | callback, | ||
245 | ); | ||
246 | |||
247 | // check for fill/align | ||
248 | let mut cloned = chars.clone().take(2); | ||
249 | let first = cloned.next().unwrap_or_default(); | ||
250 | let second = cloned.next().unwrap_or_default(); | ||
251 | match second { | ||
252 | '<' | '^' | '>' => { | ||
253 | // alignment specifier, first char specifies fillment | ||
254 | skip_char_and_emit( | ||
255 | &mut chars, | ||
256 | initial_len, | ||
257 | FormatSpecifier::Fill, | ||
258 | callback, | ||
259 | ); | ||
260 | skip_char_and_emit( | ||
261 | &mut chars, | ||
262 | initial_len, | ||
263 | FormatSpecifier::Align, | ||
264 | callback, | ||
265 | ); | ||
266 | } | ||
267 | _ => match first { | ||
268 | '<' | '^' | '>' => { | ||
269 | skip_char_and_emit( | ||
270 | &mut chars, | ||
271 | initial_len, | ||
272 | FormatSpecifier::Align, | ||
273 | callback, | ||
274 | ); | ||
275 | } | ||
276 | _ => {} | ||
277 | }, | ||
278 | } | ||
279 | |||
280 | // check for sign | ||
281 | match chars.clone().next().unwrap_or_default() { | ||
282 | '+' | '-' => { | ||
283 | skip_char_and_emit( | ||
284 | &mut chars, | ||
285 | initial_len, | ||
286 | FormatSpecifier::Sign, | ||
287 | callback, | ||
288 | ); | ||
289 | } | ||
290 | _ => {} | ||
291 | } | ||
292 | |||
293 | // check for `#` | ||
294 | if let Some('#') = chars.clone().next() { | ||
295 | skip_char_and_emit( | ||
296 | &mut chars, | ||
297 | initial_len, | ||
298 | FormatSpecifier::NumberSign, | ||
299 | callback, | ||
300 | ); | ||
301 | } | ||
302 | |||
303 | // check for `0` | ||
304 | let mut cloned = chars.clone().take(2); | ||
305 | let first = cloned.next(); | ||
306 | let second = cloned.next(); | ||
307 | |||
308 | if first == Some('0') && second != Some('$') { | ||
309 | skip_char_and_emit( | ||
310 | &mut chars, | ||
311 | initial_len, | ||
312 | FormatSpecifier::Zero, | ||
313 | callback, | ||
314 | ); | ||
315 | } | ||
316 | |||
317 | // width | ||
318 | match chars.clone().next().unwrap_or_default() { | ||
319 | '0'..='9' => { | ||
320 | read_integer(&mut chars, initial_len, callback); | ||
321 | if chars.clone().next() == Some('$') { | ||
322 | skip_char_and_emit( | ||
323 | &mut chars, | ||
324 | initial_len, | ||
325 | FormatSpecifier::DollarSign, | ||
326 | callback, | ||
327 | ); | ||
328 | } | ||
329 | } | ||
330 | 'a'..='z' | 'A'..='Z' | '_' => { | ||
331 | read_identifier(&mut chars, initial_len, callback); | ||
332 | if chars.clone().next() != Some('$') { | ||
333 | continue; | ||
334 | } | ||
335 | skip_char_and_emit( | ||
336 | &mut chars, | ||
337 | initial_len, | ||
338 | FormatSpecifier::DollarSign, | ||
339 | callback, | ||
340 | ); | ||
341 | } | ||
342 | _ => {} | ||
343 | } | ||
344 | |||
345 | // precision | ||
346 | if chars.clone().next() == Some('.') { | ||
347 | skip_char_and_emit( | ||
348 | &mut chars, | ||
349 | initial_len, | ||
350 | FormatSpecifier::Dot, | ||
351 | callback, | ||
352 | ); | ||
353 | |||
354 | match chars.clone().next().unwrap_or_default() { | ||
355 | '*' => { | ||
356 | skip_char_and_emit( | ||
357 | &mut chars, | ||
358 | initial_len, | ||
359 | FormatSpecifier::Asterisk, | ||
360 | callback, | ||
361 | ); | ||
362 | } | ||
363 | '0'..='9' => { | ||
364 | read_integer(&mut chars, initial_len, callback); | ||
365 | if chars.clone().next() == Some('$') { | ||
366 | skip_char_and_emit( | ||
367 | &mut chars, | ||
368 | initial_len, | ||
369 | FormatSpecifier::DollarSign, | ||
370 | callback, | ||
371 | ); | ||
372 | } | ||
373 | } | ||
374 | 'a'..='z' | 'A'..='Z' | '_' => { | ||
375 | read_identifier(&mut chars, initial_len, callback); | ||
376 | if chars.clone().next() != Some('$') { | ||
377 | continue; | ||
378 | } | ||
379 | skip_char_and_emit( | ||
380 | &mut chars, | ||
381 | initial_len, | ||
382 | FormatSpecifier::DollarSign, | ||
383 | callback, | ||
384 | ); | ||
385 | } | ||
386 | _ => { | ||
387 | continue; | ||
388 | } | ||
389 | } | ||
390 | } | ||
391 | |||
392 | // type | ||
393 | match chars.clone().next().unwrap_or_default() { | ||
394 | '?' => { | ||
395 | skip_char_and_emit( | ||
396 | &mut chars, | ||
397 | initial_len, | ||
398 | FormatSpecifier::QuestionMark, | ||
399 | callback, | ||
400 | ); | ||
401 | } | ||
402 | 'a'..='z' | 'A'..='Z' | '_' => { | ||
403 | read_identifier(&mut chars, initial_len, callback); | ||
404 | } | ||
405 | _ => {} | ||
406 | } | ||
407 | } | ||
408 | |||
409 | let mut cloned = chars.clone().take(2); | ||
410 | let first = cloned.next(); | ||
411 | let second = cloned.next(); | ||
412 | if first != Some('}') { | ||
413 | continue; | ||
414 | } | ||
415 | if second == Some('}') { | ||
416 | // Escaped format end specifier, `}}` | ||
417 | continue; | ||
418 | } | ||
419 | skip_char_and_emit(&mut chars, initial_len, FormatSpecifier::Close, callback); | ||
420 | } | ||
421 | _ => { | ||
422 | while let Some(next_char) = chars.clone().next() { | ||
423 | match next_char { | ||
424 | '{' => break, | ||
425 | _ => {} | ||
426 | } | ||
427 | chars.next(); | ||
428 | } | ||
429 | } | ||
430 | }; | ||
431 | } | ||
432 | |||
433 | fn skip_char_and_emit<F>( | ||
434 | chars: &mut std::str::Chars, | ||
435 | initial_len: usize, | ||
436 | emit: FormatSpecifier, | ||
437 | callback: &mut F, | ||
438 | ) where | ||
439 | F: FnMut(TextRange, FormatSpecifier), | ||
440 | { | ||
441 | let start = initial_len - chars.as_str().len(); | ||
442 | chars.next(); | ||
443 | let end = initial_len - chars.as_str().len(); | ||
444 | callback( | ||
445 | TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), | ||
446 | emit, | ||
447 | ); | ||
448 | } | ||
449 | |||
450 | fn read_integer<F>(chars: &mut std::str::Chars, initial_len: usize, callback: &mut F) | ||
451 | where | ||
452 | F: FnMut(TextRange, FormatSpecifier), | ||
453 | { | ||
454 | let start = initial_len - chars.as_str().len(); | ||
455 | chars.next(); | ||
456 | while let Some(next_char) = chars.clone().next() { | ||
457 | match next_char { | ||
458 | '0'..='9' => { | ||
459 | chars.next(); | ||
460 | } | ||
461 | _ => { | ||
462 | break; | ||
463 | } | ||
464 | } | ||
465 | } | ||
466 | let end = initial_len - chars.as_str().len(); | ||
467 | callback( | ||
468 | TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), | ||
469 | FormatSpecifier::Integer, | ||
470 | ); | ||
471 | } | ||
472 | fn read_identifier<F>(chars: &mut std::str::Chars, initial_len: usize, callback: &mut F) | ||
473 | where | ||
474 | F: FnMut(TextRange, FormatSpecifier), | ||
475 | { | ||
476 | let start = initial_len - chars.as_str().len(); | ||
477 | chars.next(); | ||
478 | while let Some(next_char) = chars.clone().next() { | ||
479 | match next_char { | ||
480 | 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => { | ||
481 | chars.next(); | ||
482 | } | ||
483 | _ => { | ||
484 | break; | ||
485 | } | ||
486 | } | ||
487 | } | ||
488 | let end = initial_len - chars.as_str().len(); | ||
489 | callback( | ||
490 | TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), | ||
491 | FormatSpecifier::Identifier, | ||
492 | ); | ||
493 | } | ||
494 | } | ||
495 | } | ||
496 | |||
497 | impl HasFormatSpecifier for String {} | ||
498 | impl HasFormatSpecifier for RawString {} | ||