diff options
author | bors[bot] <bors[bot]@users.noreply.github.com> | 2019-05-27 10:56:06 +0100 |
---|---|---|
committer | bors[bot] <bors[bot]@users.noreply.github.com> | 2019-05-27 10:56:06 +0100 |
commit | 0d1c6076073c73f57340e256dc25da9d37311ef0 (patch) | |
tree | 60ff1f4a42f8ef297c07d5716af67e3057c8e1bd /crates/ra_ide_api/src/syntax_highlighting.rs | |
parent | 4b48cff022a1606bde596f01fbf44361640b10d8 (diff) | |
parent | 1e6ba1901550fb1610a1a464c48ec358cd3c339c (diff) |
Merge #1319
1319: Rainbow highlighting spike 🌈 r=killercup a=killercup
Very simple approach: For each identifier, set the hash of the range
where it's defined as its 'id' and use it in the VSCode extension to
generate unique colors.
Thus, the generated colors are per-file. They are also quite fragile,
and I'm not entirely sure why. Looks like we need to make sure the
same ranges aren't overwritten by a later request?
Co-authored-by: Pascal Hertleif <[email protected]>
Diffstat (limited to 'crates/ra_ide_api/src/syntax_highlighting.rs')
-rw-r--r-- | crates/ra_ide_api/src/syntax_highlighting.rs | 130 |
1 files changed, 108 insertions, 22 deletions
diff --git a/crates/ra_ide_api/src/syntax_highlighting.rs b/crates/ra_ide_api/src/syntax_highlighting.rs index 87e053364..dcefb0513 100644 --- a/crates/ra_ide_api/src/syntax_highlighting.rs +++ b/crates/ra_ide_api/src/syntax_highlighting.rs | |||
@@ -1,6 +1,6 @@ | |||
1 | use rustc_hash::FxHashSet; | 1 | use rustc_hash::{FxHashSet, FxHashMap}; |
2 | 2 | ||
3 | use ra_syntax::{ast, AstNode, TextRange, Direction, SyntaxKind, SyntaxKind::*, SyntaxElement, T}; | 3 | use ra_syntax::{ast, AstNode, TextRange, Direction, SmolStr, SyntaxKind, SyntaxKind::*, SyntaxElement, T}; |
4 | use ra_db::SourceDatabase; | 4 | use ra_db::SourceDatabase; |
5 | use ra_prof::profile; | 5 | use ra_prof::profile; |
6 | 6 | ||
@@ -10,6 +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 binding_hash: Option<u64>, | ||
13 | } | 14 | } |
14 | 15 | ||
15 | fn is_control_keyword(kind: SyntaxKind) -> bool { | 16 | fn is_control_keyword(kind: SyntaxKind) -> bool { |
@@ -29,22 +30,36 @@ fn is_control_keyword(kind: SyntaxKind) -> bool { | |||
29 | 30 | ||
30 | pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRange> { | 31 | pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRange> { |
31 | let _p = profile("highlight"); | 32 | let _p = profile("highlight"); |
32 | |||
33 | let source_file = db.parse(file_id); | 33 | let source_file = db.parse(file_id); |
34 | 34 | ||
35 | fn calc_binding_hash(file_id: FileId, text: &SmolStr, shadow_count: u32) -> u64 { | ||
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 | } | ||
43 | |||
44 | hash((file_id, text, shadow_count)) | ||
45 | } | ||
46 | |||
35 | // Visited nodes to handle highlighting priorities | 47 | // Visited nodes to handle highlighting priorities |
36 | let mut highlighted: FxHashSet<SyntaxElement> = FxHashSet::default(); | 48 | let mut highlighted: FxHashSet<SyntaxElement> = FxHashSet::default(); |
49 | let mut bindings_shadow_count: FxHashMap<SmolStr, u32> = FxHashMap::default(); | ||
50 | |||
37 | let mut res = Vec::new(); | 51 | let mut res = Vec::new(); |
38 | for node in source_file.syntax().descendants_with_tokens() { | 52 | for node in source_file.syntax().descendants_with_tokens() { |
39 | if highlighted.contains(&node) { | 53 | if highlighted.contains(&node) { |
40 | continue; | 54 | continue; |
41 | } | 55 | } |
56 | let mut binding_hash = None; | ||
42 | let tag = match node.kind() { | 57 | let tag = match node.kind() { |
43 | COMMENT => "comment", | 58 | COMMENT => "comment", |
44 | STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string", | 59 | STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string", |
45 | ATTR => "attribute", | 60 | ATTR => "attribute", |
46 | NAME_REF => { | 61 | NAME_REF => { |
47 | if let Some(name_ref) = node.as_node().and_then(|n| ast::NameRef::cast(n)) { | 62 | if let Some(name_ref) = node.as_node().and_then(ast::NameRef::cast) { |
48 | use crate::name_ref_kind::{classify_name_ref, NameRefKind::*}; | 63 | use crate::name_ref_kind::{classify_name_ref, NameRefKind::*}; |
49 | use hir::{ModuleDef, ImplItem}; | 64 | use hir::{ModuleDef, ImplItem}; |
50 | 65 | ||
@@ -68,7 +83,20 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa | |||
68 | Some(Def(ModuleDef::Trait(_))) => "type", | 83 | Some(Def(ModuleDef::Trait(_))) => "type", |
69 | Some(Def(ModuleDef::TypeAlias(_))) => "type", | 84 | Some(Def(ModuleDef::TypeAlias(_))) => "type", |
70 | Some(SelfType(_)) => "type", | 85 | Some(SelfType(_)) => "type", |
71 | Some(Pat(_)) => "text", | 86 | Some(Pat(ptr)) => { |
87 | binding_hash = Some({ | ||
88 | let text = ptr | ||
89 | .syntax_node_ptr() | ||
90 | .to_node(&source_file.syntax()) | ||
91 | .text() | ||
92 | .to_smol_string(); | ||
93 | let shadow_count = | ||
94 | bindings_shadow_count.entry(text.clone()).or_default(); | ||
95 | calc_binding_hash(file_id, &text, *shadow_count) | ||
96 | }); | ||
97 | |||
98 | "variable" | ||
99 | } | ||
72 | Some(SelfParam(_)) => "type", | 100 | Some(SelfParam(_)) => "type", |
73 | Some(GenericParam(_)) => "type", | 101 | Some(GenericParam(_)) => "type", |
74 | None => "text", | 102 | None => "text", |
@@ -77,7 +105,24 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa | |||
77 | "text" | 105 | "text" |
78 | } | 106 | } |
79 | } | 107 | } |
80 | NAME => "function", | 108 | NAME => { |
109 | if let Some(name) = node.as_node().and_then(ast::Name::cast) { | ||
110 | if name.syntax().ancestors().any(|x| ast::BindPat::cast(x).is_some()) { | ||
111 | binding_hash = Some({ | ||
112 | let text = name.syntax().text().to_smol_string(); | ||
113 | let shadow_count = | ||
114 | bindings_shadow_count.entry(text.clone()).or_insert(0); | ||
115 | *shadow_count += 1; | ||
116 | calc_binding_hash(file_id, &text, *shadow_count) | ||
117 | }); | ||
118 | "variable" | ||
119 | } else { | ||
120 | "function" | ||
121 | } | ||
122 | } else { | ||
123 | "text" | ||
124 | } | ||
125 | } | ||
81 | TYPE_ALIAS_DEF | TYPE_ARG | TYPE_PARAM => "type", | 126 | TYPE_ALIAS_DEF | TYPE_ARG | TYPE_PARAM => "type", |
82 | INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal", | 127 | INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal", |
83 | LIFETIME => "parameter", | 128 | LIFETIME => "parameter", |
@@ -85,6 +130,7 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa | |||
85 | k if is_control_keyword(k) => "keyword.control", | 130 | k if is_control_keyword(k) => "keyword.control", |
86 | k if k.is_keyword() => "keyword", | 131 | k if k.is_keyword() => "keyword", |
87 | _ => { | 132 | _ => { |
133 | // let analyzer = hir::SourceAnalyzer::new(db, file_id, name_ref.syntax(), None); | ||
88 | if let Some(macro_call) = node.as_node().and_then(ast::MacroCall::cast) { | 134 | if let Some(macro_call) = node.as_node().and_then(ast::MacroCall::cast) { |
89 | if let Some(path) = macro_call.path() { | 135 | if let Some(path) = macro_call.path() { |
90 | if let Some(segment) = path.segment() { | 136 | if let Some(segment) = path.segment() { |
@@ -101,6 +147,7 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa | |||
101 | res.push(HighlightedRange { | 147 | res.push(HighlightedRange { |
102 | range: TextRange::from_to(range_start, range_end), | 148 | range: TextRange::from_to(range_start, range_end), |
103 | tag: "macro", | 149 | tag: "macro", |
150 | binding_hash: None, | ||
104 | }) | 151 | }) |
105 | } | 152 | } |
106 | } | 153 | } |
@@ -109,14 +156,25 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRa | |||
109 | continue; | 156 | continue; |
110 | } | 157 | } |
111 | }; | 158 | }; |
112 | res.push(HighlightedRange { range: node.range(), tag }) | 159 | res.push(HighlightedRange { range: node.range(), tag, binding_hash }) |
113 | } | 160 | } |
114 | res | 161 | res |
115 | } | 162 | } |
116 | 163 | ||
117 | pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId) -> String { | 164 | pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId, rainbow: bool) -> String { |
118 | let source_file = db.parse(file_id); | 165 | let source_file = db.parse(file_id); |
119 | 166 | ||
167 | fn rainbowify(seed: u64) -> String { | ||
168 | use rand::prelude::*; | ||
169 | let mut rng = SmallRng::seed_from_u64(seed); | ||
170 | format!( | ||
171 | "hsl({h},{s}%,{l}%)", | ||
172 | h = rng.gen_range::<u16, _, _>(0, 361), | ||
173 | s = rng.gen_range::<u16, _, _>(42, 99), | ||
174 | l = rng.gen_range::<u16, _, _>(40, 91), | ||
175 | ) | ||
176 | } | ||
177 | |||
120 | let mut ranges = highlight(db, file_id); | 178 | let mut ranges = highlight(db, file_id); |
121 | ranges.sort_by_key(|it| it.range.start()); | 179 | ranges.sort_by_key(|it| it.range.start()); |
122 | // quick non-optimal heuristic to intersect token ranges and highlighted ranges | 180 | // quick non-optimal heuristic to intersect token ranges and highlighted ranges |
@@ -138,16 +196,24 @@ pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId) -> String { | |||
138 | } | 196 | } |
139 | } | 197 | } |
140 | let text = html_escape(&token.text()); | 198 | let text = html_escape(&token.text()); |
141 | let classes = could_intersect | 199 | let ranges = could_intersect |
142 | .iter() | 200 | .iter() |
143 | .filter(|it| token.range().is_subrange(&it.range)) | 201 | .filter(|it| token.range().is_subrange(&it.range)) |
144 | .map(|it| it.tag) | ||
145 | .collect::<Vec<_>>(); | 202 | .collect::<Vec<_>>(); |
146 | if classes.is_empty() { | 203 | if ranges.is_empty() { |
147 | buf.push_str(&text); | 204 | buf.push_str(&text); |
148 | } else { | 205 | } else { |
149 | let classes = classes.join(" "); | 206 | let classes = ranges.iter().map(|x| x.tag).collect::<Vec<_>>().join(" "); |
150 | buf.push_str(&format!("<span class=\"{}\">{}</span>", classes, text)); | 207 | let binding_hash = ranges.first().and_then(|x| x.binding_hash); |
208 | let color = match (rainbow, binding_hash) { | ||
209 | (true, Some(hash)) => format!( | ||
210 | " data-binding-hash=\"{}\" style=\"color: {};\"", | ||
211 | hash, | ||
212 | rainbowify(hash) | ||
213 | ), | ||
214 | _ => "".into(), | ||
215 | }; | ||
216 | buf.push_str(&format!("<span class=\"{}\"{}>{}</span>", classes, color, text)); | ||
151 | } | 217 | } |
152 | } | 218 | } |
153 | buf.push_str("</code></pre>"); | 219 | buf.push_str("</code></pre>"); |
@@ -161,11 +227,8 @@ fn html_escape(text: &str) -> String { | |||
161 | 227 | ||
162 | const STYLE: &str = " | 228 | const STYLE: &str = " |
163 | <style> | 229 | <style> |
164 | pre { | 230 | body { margin: 0; } |
165 | color: #DCDCCC; | 231 | pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } |
166 | background-color: #3F3F3F; | ||
167 | font-size: 22px; | ||
168 | } | ||
169 | 232 | ||
170 | .comment { color: #7F9F7F; } | 233 | .comment { color: #7F9F7F; } |
171 | .string { color: #CC9393; } | 234 | .string { color: #CC9393; } |
@@ -180,7 +243,6 @@ pre { | |||
180 | .keyword { color: #F0DFAF; } | 243 | .keyword { color: #F0DFAF; } |
181 | .keyword\\.unsafe { color: #F0DFAF; font-weight: bold; } | 244 | .keyword\\.unsafe { color: #F0DFAF; font-weight: bold; } |
182 | .keyword\\.control { color: #DC8CC3; } | 245 | .keyword\\.control { color: #DC8CC3; } |
183 | |||
184 | </style> | 246 | </style> |
185 | "; | 247 | "; |
186 | 248 | ||
@@ -213,12 +275,36 @@ fn main() { | |||
213 | } | 275 | } |
214 | unsafe { vec.set_len(0); } | 276 | unsafe { vec.set_len(0); } |
215 | } | 277 | } |
216 | "#, | 278 | "# |
279 | .trim(), | ||
217 | ); | 280 | ); |
218 | let dst_file = project_dir().join("crates/ra_ide_api/src/snapshots/highlighting.html"); | 281 | let dst_file = project_dir().join("crates/ra_ide_api/src/snapshots/highlighting.html"); |
219 | let actual_html = &analysis.highlight_as_html(file_id).unwrap(); | 282 | let actual_html = &analysis.highlight_as_html(file_id, true).unwrap(); |
283 | let expected_html = &read_text(&dst_file); | ||
284 | std::fs::write(dst_file, &actual_html).unwrap(); | ||
285 | assert_eq_text!(expected_html, actual_html); | ||
286 | } | ||
287 | |||
288 | #[test] | ||
289 | fn test_rainbow_highlighting() { | ||
290 | let (analysis, file_id) = single_file( | ||
291 | r#" | ||
292 | fn main() { | ||
293 | let hello = "hello"; | ||
294 | let x = hello.to_string(); | ||
295 | let y = hello.to_string(); | ||
296 | |||
297 | let x = "other color please!"; | ||
298 | let y = x.to_string(); | ||
299 | } | ||
300 | "# | ||
301 | .trim(), | ||
302 | ); | ||
303 | let dst_file = | ||
304 | project_dir().join("crates/ra_ide_api/src/snapshots/rainbow_highlighting.html"); | ||
305 | let actual_html = &analysis.highlight_as_html(file_id, true).unwrap(); | ||
220 | let expected_html = &read_text(&dst_file); | 306 | let expected_html = &read_text(&dst_file); |
221 | // std::fs::write(dst_file, &actual_html).unwrap(); | 307 | std::fs::write(dst_file, &actual_html).unwrap(); |
222 | assert_eq_text!(expected_html, actual_html); | 308 | assert_eq_text!(expected_html, actual_html); |
223 | } | 309 | } |
224 | } | 310 | } |