aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/ra_ide/src/syntax_highlighting.rs346
-rw-r--r--crates/ra_ide/src/syntax_highlighting/tests.rs127
2 files changed, 231 insertions, 242 deletions
diff --git a/crates/ra_ide/src/syntax_highlighting.rs b/crates/ra_ide/src/syntax_highlighting.rs
index e8ca7d652..796f0e545 100644
--- a/crates/ra_ide/src/syntax_highlighting.rs
+++ b/crates/ra_ide/src/syntax_highlighting.rs
@@ -1,7 +1,9 @@
1//! FIXME: write short doc here 1//! Implements syntax highlighting.
2 2
3mod tags; 3mod tags;
4mod html; 4mod html;
5#[cfg(test)]
6mod tests;
5 7
6use hir::{Name, Semantics}; 8use hir::{Name, Semantics};
7use ra_ide_db::{ 9use ra_ide_db::{
@@ -10,16 +12,14 @@ use ra_ide_db::{
10}; 12};
11use ra_prof::profile; 13use ra_prof::profile;
12use ra_syntax::{ 14use ra_syntax::{
13 ast, AstNode, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxKind::*, SyntaxToken, 15 ast, AstNode, Direction, NodeOrToken, SyntaxElement, SyntaxKind::*, TextRange, WalkEvent, T,
14 TextRange, WalkEvent, T,
15}; 16};
16use rustc_hash::FxHashMap; 17use rustc_hash::FxHashMap;
17 18
18use crate::{references::classify_name_ref, FileId}; 19use crate::{references::classify_name_ref, FileId};
19 20
20pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag};
21
22pub(crate) use html::highlight_as_html; 21pub(crate) use html::highlight_as_html;
22pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag};
23 23
24#[derive(Debug)] 24#[derive(Debug)]
25pub struct HighlightedRange { 25pub struct HighlightedRange {
@@ -28,21 +28,6 @@ pub struct HighlightedRange {
28 pub binding_hash: Option<u64>, 28 pub binding_hash: Option<u64>,
29} 29}
30 30
31fn is_control_keyword(kind: SyntaxKind) -> bool {
32 match kind {
33 T![for]
34 | T![loop]
35 | T![while]
36 | T![continue]
37 | T![break]
38 | T![if]
39 | T![else]
40 | T![match]
41 | T![return] => true,
42 _ => false,
43 }
44}
45
46pub(crate) fn highlight( 31pub(crate) fn highlight(
47 db: &RootDatabase, 32 db: &RootDatabase,
48 file_id: FileId, 33 file_id: FileId,
@@ -71,20 +56,24 @@ pub(crate) fn highlight(
71 56
72 let mut current_macro_call: Option<ast::MacroCall> = None; 57 let mut current_macro_call: Option<ast::MacroCall> = None;
73 58
59 // Walk all nodes, keeping track of whether we are inside a macro or not.
60 // If in macro, expand it first and highlight the expanded code.
74 for event in root.preorder_with_tokens() { 61 for event in root.preorder_with_tokens() {
75 let event_range = match &event { 62 let event_range = match &event {
76 WalkEvent::Enter(it) => it.text_range(), 63 WalkEvent::Enter(it) => it.text_range(),
77 WalkEvent::Leave(it) => it.text_range(), 64 WalkEvent::Leave(it) => it.text_range(),
78 }; 65 };
79 66
80 if event_range.intersection(&range_to_highlight).is_none() { 67 // Element outside of the viewport, no need to highlight
68 if range_to_highlight.intersection(&event_range).is_none() {
81 continue; 69 continue;
82 } 70 }
83 71
72 // Track "inside macro" state
84 match event.clone().map(|it| it.into_node().and_then(ast::MacroCall::cast)) { 73 match event.clone().map(|it| it.into_node().and_then(ast::MacroCall::cast)) {
85 WalkEvent::Enter(Some(mc)) => { 74 WalkEvent::Enter(Some(mc)) => {
86 current_macro_call = Some(mc.clone()); 75 current_macro_call = Some(mc.clone());
87 if let Some(range) = highlight_macro(&mc) { 76 if let Some(range) = macro_call_range(&mc) {
88 res.push(HighlightedRange { 77 res.push(HighlightedRange {
89 range, 78 range,
90 highlight: HighlightTag::Macro.into(), 79 highlight: HighlightTag::Macro.into(),
@@ -101,37 +90,40 @@ pub(crate) fn highlight(
101 _ => (), 90 _ => (),
102 } 91 }
103 92
104 let node = match event { 93 let element = match event {
105 WalkEvent::Enter(it) => it, 94 WalkEvent::Enter(it) => it,
106 WalkEvent::Leave(_) => continue, 95 WalkEvent::Leave(_) => continue,
107 }; 96 };
97 let range = element.text_range();
108 98
109 if current_macro_call.is_some() { 99 let element_to_highlight = if current_macro_call.is_some() {
110 if let Some(token) = node.into_token() { 100 // Inside a macro -- expand it first
111 if let Some((highlight, binding_hash)) = 101 let token = match element.into_token() {
112 highlight_token_tree(&sema, &mut bindings_shadow_count, token.clone()) 102 Some(it) if it.parent().kind() == TOKEN_TREE => it,
113 { 103 _ => continue,
114 res.push(HighlightedRange { 104 };
115 range: token.text_range(), 105 let token = sema.descend_into_macros(token.clone());
116 highlight, 106 let parent = token.parent();
117 binding_hash, 107 // We only care Name and Name_ref
118 }); 108 match (token.kind(), parent.kind()) {
119 } 109 (IDENT, NAME) | (IDENT, NAME_REF) => parent.into(),
110 _ => token.into(),
120 } 111 }
121 continue; 112 } else {
122 } 113 element
114 };
123 115
124 if let Some((highlight, binding_hash)) = 116 if let Some((highlight, binding_hash)) =
125 highlight_node(&sema, &mut bindings_shadow_count, node.clone()) 117 highlight_element(&sema, &mut bindings_shadow_count, element_to_highlight)
126 { 118 {
127 res.push(HighlightedRange { range: node.text_range(), highlight, binding_hash }); 119 res.push(HighlightedRange { range, highlight, binding_hash });
128 } 120 }
129 } 121 }
130 122
131 res 123 res
132} 124}
133 125
134fn highlight_macro(macro_call: &ast::MacroCall) -> Option<TextRange> { 126fn macro_call_range(macro_call: &ast::MacroCall) -> Option<TextRange> {
135 let path = macro_call.path()?; 127 let path = macro_call.path()?;
136 let name_ref = path.segment()?.name_ref()?; 128 let name_ref = path.segment()?.name_ref()?;
137 129
@@ -147,67 +139,22 @@ fn highlight_macro(macro_call: &ast::MacroCall) -> Option<TextRange> {
147 Some(TextRange::from_to(range_start, range_end)) 139 Some(TextRange::from_to(range_start, range_end))
148} 140}
149 141
150fn highlight_token_tree( 142fn highlight_element(
151 sema: &Semantics<RootDatabase>,
152 bindings_shadow_count: &mut FxHashMap<Name, u32>,
153 token: SyntaxToken,
154) -> Option<(Highlight, Option<u64>)> {
155 if token.parent().kind() != TOKEN_TREE {
156 return None;
157 }
158 let token = sema.descend_into_macros(token.clone());
159 let expanded = {
160 let parent = token.parent();
161 // We only care Name and Name_ref
162 match (token.kind(), parent.kind()) {
163 (IDENT, NAME) | (IDENT, NAME_REF) => parent.into(),
164 _ => token.into(),
165 }
166 };
167
168 highlight_node(sema, bindings_shadow_count, expanded)
169}
170
171fn highlight_node(
172 sema: &Semantics<RootDatabase>, 143 sema: &Semantics<RootDatabase>,
173 bindings_shadow_count: &mut FxHashMap<Name, u32>, 144 bindings_shadow_count: &mut FxHashMap<Name, u32>,
174 node: SyntaxElement, 145 element: SyntaxElement,
175) -> Option<(Highlight, Option<u64>)> { 146) -> Option<(Highlight, Option<u64>)> {
176 let db = sema.db; 147 let db = sema.db;
177 let mut binding_hash = None; 148 let mut binding_hash = None;
178 let highlight: Highlight = match node.kind() { 149 let highlight: Highlight = match element.kind() {
179 FN_DEF => { 150 FN_DEF => {
180 bindings_shadow_count.clear(); 151 bindings_shadow_count.clear();
181 return None; 152 return None;
182 } 153 }
183 COMMENT => HighlightTag::Comment.into(), 154
184 STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => HighlightTag::LiteralString.into(), 155 // Highlight definitions depending on the "type" of the definition.
185 ATTR => HighlightTag::Attribute.into(),
186 // Special-case field init shorthand
187 NAME_REF if node.parent().and_then(ast::RecordField::cast).is_some() => {
188 HighlightTag::Field.into()
189 }
190 NAME_REF if node.ancestors().any(|it| it.kind() == ATTR) => return None,
191 NAME_REF => {
192 let name_ref = node.as_node().cloned().and_then(ast::NameRef::cast).unwrap();
193 let name_kind = classify_name_ref(sema, &name_ref);
194 match name_kind {
195 Some(name_kind) => {
196 if let NameDefinition::Local(local) = &name_kind {
197 if let Some(name) = local.name(db) {
198 let shadow_count =
199 bindings_shadow_count.entry(name.clone()).or_default();
200 binding_hash = Some(calc_binding_hash(&name, *shadow_count))
201 }
202 };
203
204 highlight_name(db, name_kind)
205 }
206 _ => return None,
207 }
208 }
209 NAME => { 156 NAME => {
210 let name = node.as_node().cloned().and_then(ast::Name::cast).unwrap(); 157 let name = element.into_node().and_then(ast::Name::cast).unwrap();
211 let name_kind = classify_name(sema, &name); 158 let name_kind = classify_name(sema, &name);
212 159
213 if let Some(NameDefinition::Local(local)) = &name_kind { 160 if let Some(NameDefinition::Local(local)) = &name_kind {
@@ -220,25 +167,56 @@ fn highlight_node(
220 167
221 match name_kind { 168 match name_kind {
222 Some(name_kind) => highlight_name(db, name_kind), 169 Some(name_kind) => highlight_name(db, name_kind),
223 None => name.syntax().parent().map_or(HighlightTag::Function.into(), |x| { 170 None => highlight_name_by_syntax(name),
224 match x.kind() {
225 STRUCT_DEF | ENUM_DEF | TRAIT_DEF | TYPE_ALIAS_DEF => {
226 HighlightTag::Type.into()
227 }
228 TYPE_PARAM => HighlightTag::TypeParam.into(),
229 RECORD_FIELD_DEF => HighlightTag::Field.into(),
230 _ => HighlightTag::Function.into(),
231 }
232 }),
233 } 171 }
234 } 172 }
173
174 // Highlight references like the definitions they resolve to
175
176 // Special-case field init shorthand
177 NAME_REF if element.parent().and_then(ast::RecordField::cast).is_some() => {
178 HighlightTag::Field.into()
179 }
180 NAME_REF if element.ancestors().any(|it| it.kind() == ATTR) => return None,
181 NAME_REF => {
182 let name_ref = element.into_node().and_then(ast::NameRef::cast).unwrap();
183 let name_kind = classify_name_ref(sema, &name_ref)?;
184
185 if let NameDefinition::Local(local) = &name_kind {
186 if let Some(name) = local.name(db) {
187 let shadow_count = bindings_shadow_count.entry(name.clone()).or_default();
188 binding_hash = Some(calc_binding_hash(&name, *shadow_count))
189 }
190 };
191
192 highlight_name(db, name_kind)
193 }
194
195 // Simple token-based highlighting
196 COMMENT => HighlightTag::Comment.into(),
197 STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => HighlightTag::LiteralString.into(),
198 ATTR => HighlightTag::Attribute.into(),
235 INT_NUMBER | FLOAT_NUMBER => HighlightTag::LiteralNumeric.into(), 199 INT_NUMBER | FLOAT_NUMBER => HighlightTag::LiteralNumeric.into(),
236 BYTE => HighlightTag::LiteralByte.into(), 200 BYTE => HighlightTag::LiteralByte.into(),
237 CHAR => HighlightTag::LiteralChar.into(), 201 CHAR => HighlightTag::LiteralChar.into(),
238 LIFETIME => HighlightTag::TypeLifetime.into(), 202 LIFETIME => HighlightTag::TypeLifetime.into(),
239 T![unsafe] => HighlightTag::Keyword | HighlightModifier::Unsafe, 203
240 k if is_control_keyword(k) => HighlightTag::Keyword | HighlightModifier::Control, 204 k if k.is_keyword() => {
241 k if k.is_keyword() => HighlightTag::Keyword.into(), 205 let h = Highlight::new(HighlightTag::Keyword);
206 match k {
207 T![break]
208 | T![continue]
209 | T![else]
210 | T![for]
211 | T![if]
212 | T![loop]
213 | T![match]
214 | T![return]
215 | T![while] => h | HighlightModifier::Control,
216 T![unsafe] => h | HighlightModifier::Unsafe,
217 _ => h,
218 }
219 }
242 220
243 _ => return None, 221 _ => return None,
244 }; 222 };
@@ -262,17 +240,19 @@ fn highlight_name(db: &RootDatabase, def: NameDefinition) -> Highlight {
262 match def { 240 match def {
263 NameDefinition::Macro(_) => HighlightTag::Macro, 241 NameDefinition::Macro(_) => HighlightTag::Macro,
264 NameDefinition::StructField(_) => HighlightTag::Field, 242 NameDefinition::StructField(_) => HighlightTag::Field,
265 NameDefinition::ModuleDef(hir::ModuleDef::Module(_)) => HighlightTag::Module, 243 NameDefinition::ModuleDef(def) => match def {
266 NameDefinition::ModuleDef(hir::ModuleDef::Function(_)) => HighlightTag::Function, 244 hir::ModuleDef::Module(_) => HighlightTag::Module,
267 NameDefinition::ModuleDef(hir::ModuleDef::Adt(_)) => HighlightTag::Type, 245 hir::ModuleDef::Function(_) => HighlightTag::Function,
268 NameDefinition::ModuleDef(hir::ModuleDef::EnumVariant(_)) => HighlightTag::Constant, 246 hir::ModuleDef::Adt(_) => HighlightTag::Type,
269 NameDefinition::ModuleDef(hir::ModuleDef::Const(_)) => HighlightTag::Constant, 247 hir::ModuleDef::EnumVariant(_) => HighlightTag::Constant,
270 NameDefinition::ModuleDef(hir::ModuleDef::Static(_)) => HighlightTag::Constant, 248 hir::ModuleDef::Const(_) => HighlightTag::Constant,
271 NameDefinition::ModuleDef(hir::ModuleDef::Trait(_)) => HighlightTag::Type, 249 hir::ModuleDef::Static(_) => HighlightTag::Constant,
272 NameDefinition::ModuleDef(hir::ModuleDef::TypeAlias(_)) => HighlightTag::Type, 250 hir::ModuleDef::Trait(_) => HighlightTag::Type,
273 NameDefinition::ModuleDef(hir::ModuleDef::BuiltinType(_)) => { 251 hir::ModuleDef::TypeAlias(_) => HighlightTag::Type,
274 return HighlightTag::Type | HighlightModifier::Builtin 252 hir::ModuleDef::BuiltinType(_) => {
275 } 253 return HighlightTag::Type | HighlightModifier::Builtin
254 }
255 },
276 NameDefinition::SelfType(_) => HighlightTag::TypeSelf, 256 NameDefinition::SelfType(_) => HighlightTag::TypeSelf,
277 NameDefinition::TypeParam(_) => HighlightTag::TypeParam, 257 NameDefinition::TypeParam(_) => HighlightTag::TypeParam,
278 NameDefinition::Local(local) => { 258 NameDefinition::Local(local) => {
@@ -286,136 +266,18 @@ fn highlight_name(db: &RootDatabase, def: NameDefinition) -> Highlight {
286 .into() 266 .into()
287} 267}
288 268
289#[cfg(test)] 269fn highlight_name_by_syntax(name: ast::Name) -> Highlight {
290mod tests { 270 let default = HighlightTag::Function.into();
291 use std::fs;
292
293 use test_utils::{assert_eq_text, project_dir, read_text};
294 271
295 use crate::{ 272 let parent = match name.syntax().parent() {
296 mock_analysis::{single_file, MockAnalysis}, 273 Some(it) => it,
297 FileRange, TextRange, 274 _ => return default,
298 }; 275 };
299 276
300 #[test] 277 match parent.kind() {
301 fn test_highlighting() { 278 STRUCT_DEF | ENUM_DEF | TRAIT_DEF | TYPE_ALIAS_DEF => HighlightTag::Type.into(),
302 let (analysis, file_id) = single_file( 279 TYPE_PARAM => HighlightTag::TypeParam.into(),
303 r#" 280 RECORD_FIELD_DEF => HighlightTag::Field.into(),
304#[derive(Clone, Debug)] 281 _ => default,
305struct Foo {
306 pub x: i32,
307 pub y: i32,
308}
309
310fn foo<T>() -> T {
311 unimplemented!();
312 foo::<i32>();
313}
314
315macro_rules! def_fn {
316 ($($tt:tt)*) => {$($tt)*}
317}
318
319def_fn!{
320 fn bar() -> u32 {
321 100
322 }
323}
324
325// comment
326fn main() {
327 println!("Hello, {}!", 92);
328
329 let mut vec = Vec::new();
330 if true {
331 let x = 92;
332 vec.push(Foo { x, y: 1 });
333 }
334 unsafe { vec.set_len(0); }
335
336 let mut x = 42;
337 let y = &mut x;
338 let z = &y;
339
340 y;
341}
342
343enum E<X> {
344 V(X)
345}
346
347impl<X> E<X> {
348 fn new<T>() -> E<T> {}
349}
350"#
351 .trim(),
352 );
353 let dst_file = project_dir().join("crates/ra_ide/src/snapshots/highlighting.html");
354 let actual_html = &analysis.highlight_as_html(file_id, false).unwrap();
355 let expected_html = &read_text(&dst_file);
356 fs::write(dst_file, &actual_html).unwrap();
357 assert_eq_text!(expected_html, actual_html);
358 }
359
360 #[test]
361 fn test_rainbow_highlighting() {
362 let (analysis, file_id) = single_file(
363 r#"
364fn main() {
365 let hello = "hello";
366 let x = hello.to_string();
367 let y = hello.to_string();
368
369 let x = "other color please!";
370 let y = x.to_string();
371}
372
373fn bar() {
374 let mut hello = "hello";
375}
376"#
377 .trim(),
378 );
379 let dst_file = project_dir().join("crates/ra_ide/src/snapshots/rainbow_highlighting.html");
380 let actual_html = &analysis.highlight_as_html(file_id, true).unwrap();
381 let expected_html = &read_text(&dst_file);
382 fs::write(dst_file, &actual_html).unwrap();
383 assert_eq_text!(expected_html, actual_html);
384 }
385
386 #[test]
387 fn accidentally_quadratic() {
388 let file = project_dir().join("crates/ra_syntax/test_data/accidentally_quadratic");
389 let src = fs::read_to_string(file).unwrap();
390
391 let mut mock = MockAnalysis::new();
392 let file_id = mock.add_file("/main.rs", &src);
393 let host = mock.analysis_host();
394
395 // let t = std::time::Instant::now();
396 let _ = host.analysis().highlight(file_id).unwrap();
397 // eprintln!("elapsed: {:?}", t.elapsed());
398 }
399
400 #[test]
401 fn test_ranges() {
402 let (analysis, file_id) = single_file(
403 r#"
404 #[derive(Clone, Debug)]
405 struct Foo {
406 pub x: i32,
407 pub y: i32,
408 }"#,
409 );
410
411 // The "x"
412 let highlights = &analysis
413 .highlight_range(FileRange {
414 file_id,
415 range: TextRange::offset_len(82.into(), 1.into()),
416 })
417 .unwrap();
418
419 assert_eq!(&highlights[0].highlight.to_string(), "field");
420 } 282 }
421} 283}
diff --git a/crates/ra_ide/src/syntax_highlighting/tests.rs b/crates/ra_ide/src/syntax_highlighting/tests.rs
new file mode 100644
index 000000000..ff23d4ac5
--- /dev/null
+++ b/crates/ra_ide/src/syntax_highlighting/tests.rs
@@ -0,0 +1,127 @@
1use std::fs;
2
3use test_utils::{assert_eq_text, project_dir, read_text};
4
5use crate::{
6 mock_analysis::{single_file, MockAnalysis},
7 FileRange, TextRange,
8};
9
10#[test]
11fn test_highlighting() {
12 let (analysis, file_id) = single_file(
13 r#"
14#[derive(Clone, Debug)]
15struct Foo {
16 pub x: i32,
17 pub y: i32,
18}
19
20fn foo<T>() -> T {
21 unimplemented!();
22 foo::<i32>();
23}
24
25macro_rules! def_fn {
26 ($($tt:tt)*) => {$($tt)*}
27}
28
29def_fn!{
30 fn bar() -> u32 {
31 100
32 }
33}
34
35// comment
36fn main() {
37 println!("Hello, {}!", 92);
38
39 let mut vec = Vec::new();
40 if true {
41 let x = 92;
42 vec.push(Foo { x, y: 1 });
43 }
44 unsafe { vec.set_len(0); }
45
46 let mut x = 42;
47 let y = &mut x;
48 let z = &y;
49
50 y;
51}
52
53enum E<X> {
54 V(X)
55}
56
57impl<X> E<X> {
58 fn new<T>() -> E<T> {}
59}
60"#
61 .trim(),
62 );
63 let dst_file = project_dir().join("crates/ra_ide/src/snapshots/highlighting.html");
64 let actual_html = &analysis.highlight_as_html(file_id, false).unwrap();
65 let expected_html = &read_text(&dst_file);
66 fs::write(dst_file, &actual_html).unwrap();
67 assert_eq_text!(expected_html, actual_html);
68}
69
70#[test]
71fn test_rainbow_highlighting() {
72 let (analysis, file_id) = single_file(
73 r#"
74fn main() {
75 let hello = "hello";
76 let x = hello.to_string();
77 let y = hello.to_string();
78
79 let x = "other color please!";
80 let y = x.to_string();
81}
82
83fn bar() {
84 let mut hello = "hello";
85}
86"#
87 .trim(),
88 );
89 let dst_file = project_dir().join("crates/ra_ide/src/snapshots/rainbow_highlighting.html");
90 let actual_html = &analysis.highlight_as_html(file_id, true).unwrap();
91 let expected_html = &read_text(&dst_file);
92 fs::write(dst_file, &actual_html).unwrap();
93 assert_eq_text!(expected_html, actual_html);
94}
95
96#[test]
97fn accidentally_quadratic() {
98 let file = project_dir().join("crates/ra_syntax/test_data/accidentally_quadratic");
99 let src = fs::read_to_string(file).unwrap();
100
101 let mut mock = MockAnalysis::new();
102 let file_id = mock.add_file("/main.rs", &src);
103 let host = mock.analysis_host();
104
105 // let t = std::time::Instant::now();
106 let _ = host.analysis().highlight(file_id).unwrap();
107 // eprintln!("elapsed: {:?}", t.elapsed());
108}
109
110#[test]
111fn test_ranges() {
112 let (analysis, file_id) = single_file(
113 r#"
114 #[derive(Clone, Debug)]
115 struct Foo {
116 pub x: i32,
117 pub y: i32,
118 }"#,
119 );
120
121 // The "x"
122 let highlights = &analysis
123 .highlight_range(FileRange { file_id, range: TextRange::offset_len(82.into(), 1.into()) })
124 .unwrap();
125
126 assert_eq!(&highlights[0].highlight.to_string(), "field");
127}