diff options
Diffstat (limited to 'crates/ra_ide_api/src/syntax_highlighting.rs')
-rw-r--r-- | crates/ra_ide_api/src/syntax_highlighting.rs | 185 |
1 files changed, 114 insertions, 71 deletions
diff --git a/crates/ra_ide_api/src/syntax_highlighting.rs b/crates/ra_ide_api/src/syntax_highlighting.rs index 407fcda4a..8981c85e6 100644 --- a/crates/ra_ide_api/src/syntax_highlighting.rs +++ b/crates/ra_ide_api/src/syntax_highlighting.rs | |||
@@ -10,7 +10,7 @@ use crate::{FileId, db::RootDatabase}; | |||
10 | pub struct HighlightedRange { | 10 | pub struct HighlightedRange { |
11 | pub range: TextRange, | 11 | pub range: TextRange, |
12 | pub tag: &'static str, | 12 | pub tag: &'static str, |
13 | pub id: Option<u64>, | 13 | pub binding_hash: Option<u64>, |
14 | } | 14 | } |
15 | 15 | ||
16 | fn is_control_keyword(kind: SyntaxKind) -> bool { | 16 | fn is_control_keyword(kind: SyntaxKind) -> bool { |
@@ -30,15 +30,18 @@ fn is_control_keyword(kind: SyntaxKind) -> bool { | |||
30 | 30 | ||
31 | pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRange> { | 31 | pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRange> { |
32 | let _p = profile("highlight"); | 32 | let _p = profile("highlight"); |
33 | |||
34 | let source_file = db.parse(file_id); | 33 | let source_file = db.parse(file_id); |
35 | 34 | ||
36 | fn hash<T: std::hash::Hash + std::fmt::Debug>(x: T) -> u64 { | 35 | fn calc_binding_hash(file_id: FileId, text: &SmolStr, shadow_count: u32) -> u64 { |
37 | use std::{collections::hash_map::DefaultHasher, hash::Hasher}; | 36 | fn hash<T: std::hash::Hash + std::fmt::Debug>(x: T) -> u64 { |
37 | use std::{collections::hash_map::DefaultHasher, hash::Hasher}; | ||
38 | |||
39 | let mut hasher = DefaultHasher::new(); | ||
40 | x.hash(&mut hasher); | ||
41 | hasher.finish() | ||
42 | } | ||
38 | 43 | ||
39 | let mut hasher = DefaultHasher::new(); | 44 | hash((file_id, text, shadow_count)) |
40 | x.hash(&mut hasher); | ||
41 | hasher.finish() | ||
42 | } | 45 | } |
43 | 46 | ||
44 | // Visited nodes to handle highlighting priorities | 47 | // Visited nodes to handle highlighting priorities |
@@ -50,66 +53,92 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa | |||
50 | if highlighted.contains(&node) { | 53 | if highlighted.contains(&node) { |
51 | continue; | 54 | continue; |
52 | } | 55 | } |
53 | let (tag, id) = match node.kind() { | 56 | let mut binding_hash = None; |
54 | COMMENT => ("comment", None), | 57 | let tag = match node.kind() { |
55 | STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => ("string", None), | 58 | COMMENT => "comment", |
56 | ATTR => ("attribute", None), | 59 | STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string", |
60 | ATTR => "attribute", | ||
57 | NAME_REF => { | 61 | NAME_REF => { |
58 | if let Some(name_ref) = node.as_ast_node::<ast::NameRef>() { | 62 | if let Some(name_ref) = node.as_node().and_then(ast::NameRef::cast) { |
59 | use crate::name_ref_kind::{classify_name_ref, NameRefKind::*}; | 63 | use crate::name_ref_kind::{classify_name_ref, NameRefKind::*}; |
60 | use hir::{ModuleDef, ImplItem}; | 64 | use hir::{ModuleDef, ImplItem}; |
61 | 65 | ||
62 | // FIXME: try to reuse the SourceAnalyzers | 66 | // FIXME: try to reuse the SourceAnalyzers |
63 | let analyzer = hir::SourceAnalyzer::new(db, file_id, name_ref.syntax(), None); | 67 | let analyzer = hir::SourceAnalyzer::new(db, file_id, name_ref.syntax(), None); |
64 | match classify_name_ref(db, &analyzer, name_ref) { | 68 | match classify_name_ref(db, &analyzer, name_ref) { |
65 | Some(Method(_)) => ("function", None), | 69 | Some(Method(_)) => "function", |
66 | Some(Macro(_)) => ("macro", None), | 70 | Some(Macro(_)) => "macro", |
67 | Some(FieldAccess(_)) => ("field", None), | 71 | Some(FieldAccess(field)) => { |
68 | Some(AssocItem(ImplItem::Method(_))) => ("function", None), | 72 | let (hir_file_id, src) = field.source(db); |
69 | Some(AssocItem(ImplItem::Const(_))) => ("constant", None), | 73 | if let hir::FieldSource::Named(name) = src { |
70 | Some(AssocItem(ImplItem::TypeAlias(_))) => ("type", None), | 74 | let text = name.syntax().text().to_smol_string(); |
71 | Some(Def(ModuleDef::Module(_))) => ("module", None), | 75 | let shadow_count = 0; // potentially even from different file |
72 | Some(Def(ModuleDef::Function(_))) => ("function", None), | 76 | binding_hash = Some(calc_binding_hash(hir_file_id.original_file(db), &text, shadow_count)); |
73 | Some(Def(ModuleDef::Struct(_))) => ("type", None), | 77 | } |
74 | Some(Def(ModuleDef::Union(_))) => ("type", None), | 78 | |
75 | Some(Def(ModuleDef::Enum(_))) => ("type", None), | 79 | "field" |
76 | Some(Def(ModuleDef::EnumVariant(_))) => ("constant", None), | 80 | }, |
77 | Some(Def(ModuleDef::Const(_))) => ("constant", None), | 81 | Some(AssocItem(ImplItem::Method(_))) => "function", |
78 | Some(Def(ModuleDef::Static(_))) => ("constant", None), | 82 | Some(AssocItem(ImplItem::Const(_))) => "constant", |
79 | Some(Def(ModuleDef::Trait(_))) => ("type", None), | 83 | Some(AssocItem(ImplItem::TypeAlias(_))) => "type", |
80 | Some(Def(ModuleDef::TypeAlias(_))) => ("type", None), | 84 | Some(Def(ModuleDef::Module(_))) => "module", |
81 | Some(SelfType(_)) => ("type", None), | 85 | Some(Def(ModuleDef::Function(_))) => "function", |
82 | Some(Pat(ptr)) => ("variable", Some(hash({ | 86 | Some(Def(ModuleDef::Struct(_))) => "type", |
83 | let text = ptr.syntax_node_ptr().to_node(&source_file.syntax()).text().to_smol_string(); | 87 | Some(Def(ModuleDef::Union(_))) => "type", |
84 | let shadow_count = bindings_shadow_count.entry(text.clone()).or_default(); | 88 | Some(Def(ModuleDef::Enum(_))) => "type", |
85 | (text, shadow_count) | 89 | Some(Def(ModuleDef::EnumVariant(_))) => "constant", |
86 | }))), | 90 | Some(Def(ModuleDef::Const(_))) => "constant", |
87 | Some(SelfParam(_)) => ("type", None), | 91 | Some(Def(ModuleDef::Static(_))) => "constant", |
88 | Some(GenericParam(_)) => ("type", None), | 92 | Some(Def(ModuleDef::Trait(_))) => "type", |
89 | None => ("text", None), | 93 | Some(Def(ModuleDef::TypeAlias(_))) => "type", |
94 | Some(SelfType(_)) => "type", | ||
95 | Some(Pat(ptr)) => { | ||
96 | binding_hash = Some({ | ||
97 | let text = ptr.syntax_node_ptr().to_node(&source_file.syntax()).text().to_smol_string(); | ||
98 | let shadow_count = bindings_shadow_count.entry(text.clone()).or_default(); | ||
99 | calc_binding_hash(file_id, &text, *shadow_count) | ||
100 | }); | ||
101 | |||
102 | "variable" | ||
103 | }, | ||
104 | Some(SelfParam(_)) => "type", | ||
105 | Some(GenericParam(_)) => "type", | ||
106 | None => "text", | ||
90 | } | 107 | } |
91 | } else { | 108 | } else { |
92 | ("text", None) | 109 | "text" |
93 | } | 110 | } |
94 | } | 111 | } |
95 | NAME => { | 112 | NAME => { |
96 | if let Some(name) = node.as_ast_node::<ast::Name>() { | 113 | if let Some(name) = node.as_node().and_then(ast::Name::cast) { |
97 | ("variable", Some(hash({ | 114 | if name.syntax().ancestors().any(|x| ast::BindPat::cast(x).is_some()) { |
98 | let text = name.syntax().text().to_smol_string(); | 115 | binding_hash = Some({ |
99 | let shadow_count = bindings_shadow_count.entry(text.clone()).or_insert(1); | 116 | let text = name.syntax().text().to_smol_string(); |
100 | *shadow_count += 1; | 117 | let shadow_count = bindings_shadow_count.entry(text.clone()).or_insert(0); |
101 | (text, shadow_count) | 118 | *shadow_count += 1; |
102 | }))) | 119 | calc_binding_hash(file_id, &text, *shadow_count) |
120 | }); | ||
121 | "variable" | ||
122 | } else if name.syntax().ancestors().any(|x| ast::NamedFieldDef::cast(x).is_some()) { | ||
123 | binding_hash = Some({ | ||
124 | let text = name.syntax().text().to_smol_string(); | ||
125 | let shadow_count = 0; | ||
126 | calc_binding_hash(file_id, &text, shadow_count) | ||
127 | }); | ||
128 | "variable" | ||
129 | } else { | ||
130 | "function" | ||
131 | } | ||
103 | } else { | 132 | } else { |
104 | ("text", None) | 133 | "text" |
105 | } | 134 | } |
106 | } | 135 | } |
107 | TYPE_ALIAS_DEF | TYPE_ARG | TYPE_PARAM => ("type", None), | 136 | TYPE_ALIAS_DEF | TYPE_ARG | TYPE_PARAM => "type", |
108 | INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => ("literal", None), | 137 | INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal", |
109 | LIFETIME => ("parameter", None), | 138 | LIFETIME => "parameter", |
110 | T![unsafe] => ("keyword.unsafe", None), | 139 | T![unsafe] => "keyword.unsafe", |
111 | k if is_control_keyword(k) => ("keyword.control", None), | 140 | k if is_control_keyword(k) => "keyword.control", |
112 | k if k.is_keyword() => ("keyword", None), | 141 | k if k.is_keyword() => "keyword", |
113 | _ => { | 142 | _ => { |
114 | // let analyzer = hir::SourceAnalyzer::new(db, file_id, name_ref.syntax(), None); | 143 | // let analyzer = hir::SourceAnalyzer::new(db, file_id, name_ref.syntax(), None); |
115 | if let Some(macro_call) = node.as_node().and_then(ast::MacroCall::cast) { | 144 | if let Some(macro_call) = node.as_node().and_then(ast::MacroCall::cast) { |
@@ -128,7 +157,7 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa | |||
128 | res.push(HighlightedRange { | 157 | res.push(HighlightedRange { |
129 | range: TextRange::from_to(range_start, range_end), | 158 | range: TextRange::from_to(range_start, range_end), |
130 | tag: "macro", | 159 | tag: "macro", |
131 | id: None, | 160 | binding_hash: None, |
132 | }) | 161 | }) |
133 | } | 162 | } |
134 | } | 163 | } |
@@ -137,14 +166,24 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa | |||
137 | continue; | 166 | continue; |
138 | } | 167 | } |
139 | }; | 168 | }; |
140 | res.push(HighlightedRange { range: node.range(), tag, id }) | 169 | res.push(HighlightedRange { range: node.range(), tag, binding_hash }) |
141 | } | 170 | } |
142 | res | 171 | res |
143 | } | 172 | } |
144 | 173 | ||
145 | pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId) -> String { | 174 | pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId, rainbow: bool) -> String { |
146 | let source_file = db.parse(file_id); | 175 | let source_file = db.parse(file_id); |
147 | 176 | ||
177 | fn rainbowify(seed: u64) -> String { | ||
178 | use rand::prelude::*; | ||
179 | let mut rng = SmallRng::seed_from_u64(seed); | ||
180 | format!("hsl({h},{s}%,{l}%)", | ||
181 | h = rng.gen_range::<u16, _, _>(0, 361), | ||
182 | s = rng.gen_range::<u16, _, _>(42, 99), | ||
183 | l = rng.gen_range::<u16, _, _>(40, 91), | ||
184 | ) | ||
185 | } | ||
186 | |||
148 | let mut ranges = highlight(db, file_id); | 187 | let mut ranges = highlight(db, file_id); |
149 | ranges.sort_by_key(|it| it.range.start()); | 188 | ranges.sort_by_key(|it| it.range.start()); |
150 | // quick non-optimal heuristic to intersect token ranges and highlighted ranges | 189 | // quick non-optimal heuristic to intersect token ranges and highlighted ranges |
@@ -166,16 +205,20 @@ pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId) -> String { | |||
166 | } | 205 | } |
167 | } | 206 | } |
168 | let text = html_escape(&token.text()); | 207 | let text = html_escape(&token.text()); |
169 | let classes = could_intersect | 208 | let ranges = could_intersect |
170 | .iter() | 209 | .iter() |
171 | .filter(|it| token.range().is_subrange(&it.range)) | 210 | .filter(|it| token.range().is_subrange(&it.range)) |
172 | .map(|it| it.tag) | ||
173 | .collect::<Vec<_>>(); | 211 | .collect::<Vec<_>>(); |
174 | if classes.is_empty() { | 212 | if ranges.is_empty() { |
175 | buf.push_str(&text); | 213 | buf.push_str(&text); |
176 | } else { | 214 | } else { |
177 | let classes = classes.join(" "); | 215 | let classes = ranges.iter().map(|x| x.tag).collect::<Vec<_>>().join(" "); |
178 | buf.push_str(&format!("<span class=\"{}\">{}</span>", classes, text)); | 216 | let binding_hash = ranges.first().and_then(|x| x.binding_hash); |
217 | let color = match (rainbow, binding_hash) { | ||
218 | (true, Some(hash)) => format!(" data-binding-hash=\"{}\" style=\"color: {};\"", hash, rainbowify(hash)), | ||
219 | _ => "".into() | ||
220 | }; | ||
221 | buf.push_str(&format!("<span class=\"{}\"{}>{}</span>", classes, color, text)); | ||
179 | } | 222 | } |
180 | } | 223 | } |
181 | buf.push_str("</code></pre>"); | 224 | buf.push_str("</code></pre>"); |
@@ -189,11 +232,8 @@ fn html_escape(text: &str) -> String { | |||
189 | 232 | ||
190 | const STYLE: &str = " | 233 | const STYLE: &str = " |
191 | <style> | 234 | <style> |
192 | pre { | 235 | body { margin: 0; } |
193 | color: #DCDCCC; | 236 | pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } |
194 | background-color: #3F3F3F; | ||
195 | font-size: 22px; | ||
196 | } | ||
197 | 237 | ||
198 | .comment { color: #7F9F7F; } | 238 | .comment { color: #7F9F7F; } |
199 | .string { color: #CC9393; } | 239 | .string { color: #CC9393; } |
@@ -208,7 +248,6 @@ pre { | |||
208 | .keyword { color: #F0DFAF; } | 248 | .keyword { color: #F0DFAF; } |
209 | .keyword\\.unsafe { color: #F0DFAF; font-weight: bold; } | 249 | .keyword\\.unsafe { color: #F0DFAF; font-weight: bold; } |
210 | .keyword\\.control { color: #DC8CC3; } | 250 | .keyword\\.control { color: #DC8CC3; } |
211 | |||
212 | </style> | 251 | </style> |
213 | "; | 252 | "; |
214 | 253 | ||
@@ -241,12 +280,12 @@ fn main() { | |||
241 | } | 280 | } |
242 | unsafe { vec.set_len(0); } | 281 | unsafe { vec.set_len(0); } |
243 | } | 282 | } |
244 | "#, | 283 | "#.trim(), |
245 | ); | 284 | ); |
246 | let dst_file = project_dir().join("crates/ra_ide_api/src/snapshots/highlighting.html"); | 285 | let dst_file = project_dir().join("crates/ra_ide_api/src/snapshots/highlighting.html"); |
247 | let actual_html = &analysis.highlight_as_html(file_id).unwrap(); | 286 | let actual_html = &analysis.highlight_as_html(file_id).unwrap(); |
248 | let expected_html = &read_text(&dst_file); | 287 | let expected_html = &read_text(&dst_file); |
249 | // std::fs::write(dst_file, &actual_html).unwrap(); | 288 | std::fs::write(dst_file, &actual_html).unwrap(); |
250 | assert_eq_text!(expected_html, actual_html); | 289 | assert_eq_text!(expected_html, actual_html); |
251 | } | 290 | } |
252 | 291 | ||
@@ -261,9 +300,13 @@ fn main() { | |||
261 | 300 | ||
262 | let x = "other color please!"; | 301 | let x = "other color please!"; |
263 | let y = x.to_string(); | 302 | let y = x.to_string(); |
264 | }"#, | 303 | } |
304 | "#.trim(), | ||
265 | ); | 305 | ); |
266 | let result = analysis.highlight(file_id); | 306 | let dst_file = project_dir().join("crates/ra_ide_api/src/snapshots/rainbow_highlighting.html"); |
267 | assert_debug_snapshot_matches!("rainbow_highlighting", result); | 307 | let actual_html = &analysis.highlight_as_html(file_id).unwrap(); |
308 | let expected_html = &read_text(&dst_file); | ||
309 | std::fs::write(dst_file, &actual_html).unwrap(); | ||
310 | assert_eq_text!(expected_html, actual_html); | ||
268 | } | 311 | } |
269 | } | 312 | } |