diff options
Diffstat (limited to 'crates/ra_ide/src/syntax_highlighting.rs')
-rw-r--r-- | crates/ra_ide/src/syntax_highlighting.rs | 342 |
1 files changed, 342 insertions, 0 deletions
diff --git a/crates/ra_ide/src/syntax_highlighting.rs b/crates/ra_ide/src/syntax_highlighting.rs new file mode 100644 index 000000000..2c568a747 --- /dev/null +++ b/crates/ra_ide/src/syntax_highlighting.rs | |||
@@ -0,0 +1,342 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use rustc_hash::{FxHashMap, FxHashSet}; | ||
4 | |||
5 | use hir::{Name, Source}; | ||
6 | use ra_db::SourceDatabase; | ||
7 | use ra_prof::profile; | ||
8 | use ra_syntax::{ast, AstNode, Direction, SyntaxElement, SyntaxKind, SyntaxKind::*, TextRange, T}; | ||
9 | |||
10 | use crate::{ | ||
11 | db::RootDatabase, | ||
12 | references::{ | ||
13 | classify_name, classify_name_ref, | ||
14 | NameKind::{self, *}, | ||
15 | }, | ||
16 | FileId, | ||
17 | }; | ||
18 | |||
19 | #[derive(Debug)] | ||
20 | pub struct HighlightedRange { | ||
21 | pub range: TextRange, | ||
22 | pub tag: &'static str, | ||
23 | pub binding_hash: Option<u64>, | ||
24 | } | ||
25 | |||
26 | fn is_control_keyword(kind: SyntaxKind) -> bool { | ||
27 | match kind { | ||
28 | T![for] | ||
29 | | T![loop] | ||
30 | | T![while] | ||
31 | | T![continue] | ||
32 | | T![break] | ||
33 | | T![if] | ||
34 | | T![else] | ||
35 | | T![match] | ||
36 | | T![return] => true, | ||
37 | _ => false, | ||
38 | } | ||
39 | } | ||
40 | |||
41 | pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRange> { | ||
42 | let _p = profile("highlight"); | ||
43 | let parse = db.parse(file_id); | ||
44 | let root = parse.tree().syntax().clone(); | ||
45 | |||
46 | fn calc_binding_hash(file_id: FileId, name: &Name, shadow_count: u32) -> u64 { | ||
47 | fn hash<T: std::hash::Hash + std::fmt::Debug>(x: T) -> u64 { | ||
48 | use std::{collections::hash_map::DefaultHasher, hash::Hasher}; | ||
49 | |||
50 | let mut hasher = DefaultHasher::new(); | ||
51 | x.hash(&mut hasher); | ||
52 | hasher.finish() | ||
53 | } | ||
54 | |||
55 | hash((file_id, name, shadow_count)) | ||
56 | } | ||
57 | |||
58 | // Visited nodes to handle highlighting priorities | ||
59 | // FIXME: retain only ranges here | ||
60 | let mut highlighted: FxHashSet<SyntaxElement> = FxHashSet::default(); | ||
61 | let mut bindings_shadow_count: FxHashMap<Name, u32> = FxHashMap::default(); | ||
62 | |||
63 | let mut res = Vec::new(); | ||
64 | for node in root.descendants_with_tokens() { | ||
65 | if highlighted.contains(&node) { | ||
66 | continue; | ||
67 | } | ||
68 | let mut binding_hash = None; | ||
69 | let tag = match node.kind() { | ||
70 | FN_DEF => { | ||
71 | bindings_shadow_count.clear(); | ||
72 | continue; | ||
73 | } | ||
74 | COMMENT => "comment", | ||
75 | STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string", | ||
76 | ATTR => "attribute", | ||
77 | NAME_REF => { | ||
78 | if node.ancestors().any(|it| it.kind() == ATTR) { | ||
79 | continue; | ||
80 | } | ||
81 | |||
82 | let name_ref = node.as_node().cloned().and_then(ast::NameRef::cast).unwrap(); | ||
83 | let name_kind = | ||
84 | classify_name_ref(db, Source::new(file_id.into(), &name_ref)).map(|d| d.kind); | ||
85 | |||
86 | if let Some(Local(local)) = &name_kind { | ||
87 | if let Some(name) = local.name(db) { | ||
88 | let shadow_count = bindings_shadow_count.entry(name.clone()).or_default(); | ||
89 | binding_hash = Some(calc_binding_hash(file_id, &name, *shadow_count)) | ||
90 | } | ||
91 | }; | ||
92 | |||
93 | name_kind.map_or("text", |it| highlight_name(db, it)) | ||
94 | } | ||
95 | NAME => { | ||
96 | let name = node.as_node().cloned().and_then(ast::Name::cast).unwrap(); | ||
97 | let name_kind = | ||
98 | classify_name(db, Source::new(file_id.into(), &name)).map(|d| d.kind); | ||
99 | |||
100 | if let Some(Local(local)) = &name_kind { | ||
101 | if let Some(name) = local.name(db) { | ||
102 | let shadow_count = bindings_shadow_count.entry(name.clone()).or_default(); | ||
103 | *shadow_count += 1; | ||
104 | binding_hash = Some(calc_binding_hash(file_id, &name, *shadow_count)) | ||
105 | } | ||
106 | }; | ||
107 | |||
108 | match name_kind { | ||
109 | Some(name_kind) => highlight_name(db, name_kind), | ||
110 | None => name.syntax().parent().map_or("function", |x| match x.kind() { | ||
111 | TYPE_PARAM | STRUCT_DEF | ENUM_DEF | TRAIT_DEF | TYPE_ALIAS_DEF => "type", | ||
112 | RECORD_FIELD_DEF => "field", | ||
113 | _ => "function", | ||
114 | }), | ||
115 | } | ||
116 | } | ||
117 | INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal", | ||
118 | LIFETIME => "parameter", | ||
119 | T![unsafe] => "keyword.unsafe", | ||
120 | k if is_control_keyword(k) => "keyword.control", | ||
121 | k if k.is_keyword() => "keyword", | ||
122 | _ => { | ||
123 | if let Some(macro_call) = node.as_node().cloned().and_then(ast::MacroCall::cast) { | ||
124 | if let Some(path) = macro_call.path() { | ||
125 | if let Some(segment) = path.segment() { | ||
126 | if let Some(name_ref) = segment.name_ref() { | ||
127 | highlighted.insert(name_ref.syntax().clone().into()); | ||
128 | let range_start = name_ref.syntax().text_range().start(); | ||
129 | let mut range_end = name_ref.syntax().text_range().end(); | ||
130 | for sibling in path.syntax().siblings_with_tokens(Direction::Next) { | ||
131 | match sibling.kind() { | ||
132 | T![!] | IDENT => range_end = sibling.text_range().end(), | ||
133 | _ => (), | ||
134 | } | ||
135 | } | ||
136 | res.push(HighlightedRange { | ||
137 | range: TextRange::from_to(range_start, range_end), | ||
138 | tag: "macro", | ||
139 | binding_hash: None, | ||
140 | }) | ||
141 | } | ||
142 | } | ||
143 | } | ||
144 | } | ||
145 | continue; | ||
146 | } | ||
147 | }; | ||
148 | res.push(HighlightedRange { range: node.text_range(), tag, binding_hash }) | ||
149 | } | ||
150 | res | ||
151 | } | ||
152 | |||
153 | pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId, rainbow: bool) -> String { | ||
154 | let parse = db.parse(file_id); | ||
155 | |||
156 | fn rainbowify(seed: u64) -> String { | ||
157 | use rand::prelude::*; | ||
158 | let mut rng = SmallRng::seed_from_u64(seed); | ||
159 | format!( | ||
160 | "hsl({h},{s}%,{l}%)", | ||
161 | h = rng.gen_range::<u16, _, _>(0, 361), | ||
162 | s = rng.gen_range::<u16, _, _>(42, 99), | ||
163 | l = rng.gen_range::<u16, _, _>(40, 91), | ||
164 | ) | ||
165 | } | ||
166 | |||
167 | let mut ranges = highlight(db, file_id); | ||
168 | ranges.sort_by_key(|it| it.range.start()); | ||
169 | // quick non-optimal heuristic to intersect token ranges and highlighted ranges | ||
170 | let mut frontier = 0; | ||
171 | let mut could_intersect: Vec<&HighlightedRange> = Vec::new(); | ||
172 | |||
173 | let mut buf = String::new(); | ||
174 | buf.push_str(&STYLE); | ||
175 | buf.push_str("<pre><code>"); | ||
176 | let tokens = parse.tree().syntax().descendants_with_tokens().filter_map(|it| it.into_token()); | ||
177 | for token in tokens { | ||
178 | could_intersect.retain(|it| token.text_range().start() <= it.range.end()); | ||
179 | while let Some(r) = ranges.get(frontier) { | ||
180 | if r.range.start() <= token.text_range().end() { | ||
181 | could_intersect.push(r); | ||
182 | frontier += 1; | ||
183 | } else { | ||
184 | break; | ||
185 | } | ||
186 | } | ||
187 | let text = html_escape(&token.text()); | ||
188 | let ranges = could_intersect | ||
189 | .iter() | ||
190 | .filter(|it| token.text_range().is_subrange(&it.range)) | ||
191 | .collect::<Vec<_>>(); | ||
192 | if ranges.is_empty() { | ||
193 | buf.push_str(&text); | ||
194 | } else { | ||
195 | let classes = ranges.iter().map(|x| x.tag).collect::<Vec<_>>().join(" "); | ||
196 | let binding_hash = ranges.first().and_then(|x| x.binding_hash); | ||
197 | let color = match (rainbow, binding_hash) { | ||
198 | (true, Some(hash)) => format!( | ||
199 | " data-binding-hash=\"{}\" style=\"color: {};\"", | ||
200 | hash, | ||
201 | rainbowify(hash) | ||
202 | ), | ||
203 | _ => "".into(), | ||
204 | }; | ||
205 | buf.push_str(&format!("<span class=\"{}\"{}>{}</span>", classes, color, text)); | ||
206 | } | ||
207 | } | ||
208 | buf.push_str("</code></pre>"); | ||
209 | buf | ||
210 | } | ||
211 | |||
212 | fn highlight_name(db: &RootDatabase, name_kind: NameKind) -> &'static str { | ||
213 | match name_kind { | ||
214 | Macro(_) => "macro", | ||
215 | Field(_) => "field", | ||
216 | AssocItem(hir::AssocItem::Function(_)) => "function", | ||
217 | AssocItem(hir::AssocItem::Const(_)) => "constant", | ||
218 | AssocItem(hir::AssocItem::TypeAlias(_)) => "type", | ||
219 | Def(hir::ModuleDef::Module(_)) => "module", | ||
220 | Def(hir::ModuleDef::Function(_)) => "function", | ||
221 | Def(hir::ModuleDef::Adt(_)) => "type", | ||
222 | Def(hir::ModuleDef::EnumVariant(_)) => "constant", | ||
223 | Def(hir::ModuleDef::Const(_)) => "constant", | ||
224 | Def(hir::ModuleDef::Static(_)) => "constant", | ||
225 | Def(hir::ModuleDef::Trait(_)) => "type", | ||
226 | Def(hir::ModuleDef::TypeAlias(_)) => "type", | ||
227 | Def(hir::ModuleDef::BuiltinType(_)) => "type", | ||
228 | SelfType(_) => "type", | ||
229 | GenericParam(_) => "type", | ||
230 | Local(local) => { | ||
231 | if local.is_mut(db) { | ||
232 | "variable.mut" | ||
233 | } else if local.ty(db).is_mutable_reference() { | ||
234 | "variable.mut" | ||
235 | } else { | ||
236 | "variable" | ||
237 | } | ||
238 | } | ||
239 | } | ||
240 | } | ||
241 | |||
242 | //FIXME: like, real html escaping | ||
243 | fn html_escape(text: &str) -> String { | ||
244 | text.replace("<", "<").replace(">", ">") | ||
245 | } | ||
246 | |||
247 | const STYLE: &str = " | ||
248 | <style> | ||
249 | body { margin: 0; } | ||
250 | pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } | ||
251 | |||
252 | .comment { color: #7F9F7F; } | ||
253 | .string { color: #CC9393; } | ||
254 | .function { color: #93E0E3; } | ||
255 | .parameter { color: #94BFF3; } | ||
256 | .builtin { color: #DD6718; } | ||
257 | .text { color: #DCDCCC; } | ||
258 | .attribute { color: #94BFF3; } | ||
259 | .literal { color: #BFEBBF; } | ||
260 | .macro { color: #94BFF3; } | ||
261 | .variable { color: #DCDCCC; } | ||
262 | .variable\\.mut { color: #DCDCCC; text-decoration: underline; } | ||
263 | |||
264 | .keyword { color: #F0DFAF; } | ||
265 | .keyword\\.unsafe { color: #DFAF8F; } | ||
266 | .keyword\\.control { color: #F0DFAF; font-weight: bold; } | ||
267 | </style> | ||
268 | "; | ||
269 | |||
270 | #[cfg(test)] | ||
271 | mod tests { | ||
272 | use crate::mock_analysis::single_file; | ||
273 | use test_utils::{assert_eq_text, project_dir, read_text}; | ||
274 | |||
275 | #[test] | ||
276 | fn test_highlighting() { | ||
277 | let (analysis, file_id) = single_file( | ||
278 | r#" | ||
279 | #[derive(Clone, Debug)] | ||
280 | struct Foo { | ||
281 | pub x: i32, | ||
282 | pub y: i32, | ||
283 | } | ||
284 | |||
285 | fn foo<T>() -> T { | ||
286 | unimplemented!(); | ||
287 | foo::<i32>(); | ||
288 | } | ||
289 | |||
290 | // comment | ||
291 | fn main() { | ||
292 | println!("Hello, {}!", 92); | ||
293 | |||
294 | let mut vec = Vec::new(); | ||
295 | if true { | ||
296 | vec.push(Foo { x: 0, y: 1 }); | ||
297 | } | ||
298 | unsafe { vec.set_len(0); } | ||
299 | |||
300 | let mut x = 42; | ||
301 | let y = &mut x; | ||
302 | let z = &y; | ||
303 | |||
304 | y; | ||
305 | } | ||
306 | "# | ||
307 | .trim(), | ||
308 | ); | ||
309 | let dst_file = project_dir().join("crates/ra_ide/src/snapshots/highlighting.html"); | ||
310 | let actual_html = &analysis.highlight_as_html(file_id, false).unwrap(); | ||
311 | let expected_html = &read_text(&dst_file); | ||
312 | std::fs::write(dst_file, &actual_html).unwrap(); | ||
313 | assert_eq_text!(expected_html, actual_html); | ||
314 | } | ||
315 | |||
316 | #[test] | ||
317 | fn test_rainbow_highlighting() { | ||
318 | let (analysis, file_id) = single_file( | ||
319 | r#" | ||
320 | fn main() { | ||
321 | let hello = "hello"; | ||
322 | let x = hello.to_string(); | ||
323 | let y = hello.to_string(); | ||
324 | |||
325 | let x = "other color please!"; | ||
326 | let y = x.to_string(); | ||
327 | } | ||
328 | |||
329 | fn bar() { | ||
330 | let mut hello = "hello"; | ||
331 | } | ||
332 | "# | ||
333 | .trim(), | ||
334 | ); | ||
335 | let dst_file = | ||
336 | project_dir().join("crates/ra_ide/src/snapshots/rainbow_highlighting.html"); | ||
337 | let actual_html = &analysis.highlight_as_html(file_id, true).unwrap(); | ||
338 | let expected_html = &read_text(&dst_file); | ||
339 | std::fs::write(dst_file, &actual_html).unwrap(); | ||
340 | assert_eq_text!(expected_html, actual_html); | ||
341 | } | ||
342 | } | ||