diff options
Diffstat (limited to 'crates/ra_ide/src/syntax_highlighting.rs')
-rw-r--r-- | crates/ra_ide/src/syntax_highlighting.rs | 346 |
1 files changed, 104 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 | ||
3 | mod tags; | 3 | mod tags; |
4 | mod html; | 4 | mod html; |
5 | #[cfg(test)] | ||
6 | mod tests; | ||
5 | 7 | ||
6 | use hir::{Name, Semantics}; | 8 | use hir::{Name, Semantics}; |
7 | use ra_ide_db::{ | 9 | use ra_ide_db::{ |
@@ -10,16 +12,14 @@ use ra_ide_db::{ | |||
10 | }; | 12 | }; |
11 | use ra_prof::profile; | 13 | use ra_prof::profile; |
12 | use ra_syntax::{ | 14 | use 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 | }; |
16 | use rustc_hash::FxHashMap; | 17 | use rustc_hash::FxHashMap; |
17 | 18 | ||
18 | use crate::{references::classify_name_ref, FileId}; | 19 | use crate::{references::classify_name_ref, FileId}; |
19 | 20 | ||
20 | pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag}; | ||
21 | |||
22 | pub(crate) use html::highlight_as_html; | 21 | pub(crate) use html::highlight_as_html; |
22 | pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag}; | ||
23 | 23 | ||
24 | #[derive(Debug)] | 24 | #[derive(Debug)] |
25 | pub struct HighlightedRange { | 25 | pub 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 | ||
31 | fn 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 | |||
46 | pub(crate) fn highlight( | 31 | pub(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 | ||
134 | fn highlight_macro(macro_call: &ast::MacroCall) -> Option<TextRange> { | 126 | fn 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 | ||
150 | fn highlight_token_tree( | 142 | fn 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 | |||
171 | fn 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)] | 269 | fn highlight_name_by_syntax(name: ast::Name) -> Highlight { |
290 | mod 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, |
305 | struct Foo { | ||
306 | pub x: i32, | ||
307 | pub y: i32, | ||
308 | } | ||
309 | |||
310 | fn foo<T>() -> T { | ||
311 | unimplemented!(); | ||
312 | foo::<i32>(); | ||
313 | } | ||
314 | |||
315 | macro_rules! def_fn { | ||
316 | ($($tt:tt)*) => {$($tt)*} | ||
317 | } | ||
318 | |||
319 | def_fn!{ | ||
320 | fn bar() -> u32 { | ||
321 | 100 | ||
322 | } | ||
323 | } | ||
324 | |||
325 | // comment | ||
326 | fn 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 | |||
343 | enum E<X> { | ||
344 | V(X) | ||
345 | } | ||
346 | |||
347 | impl<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#" | ||
364 | fn 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 | |||
373 | fn 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 | } |