aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api/src/syntax_highlighting.rs
diff options
context:
space:
mode:
authorbors[bot] <bors[bot]@users.noreply.github.com>2019-05-27 10:56:06 +0100
committerbors[bot] <bors[bot]@users.noreply.github.com>2019-05-27 10:56:06 +0100
commit0d1c6076073c73f57340e256dc25da9d37311ef0 (patch)
tree60ff1f4a42f8ef297c07d5716af67e3057c8e1bd /crates/ra_ide_api/src/syntax_highlighting.rs
parent4b48cff022a1606bde596f01fbf44361640b10d8 (diff)
parent1e6ba1901550fb1610a1a464c48ec358cd3c339c (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.rs130
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 @@
1use rustc_hash::FxHashSet; 1use rustc_hash::{FxHashSet, FxHashMap};
2 2
3use ra_syntax::{ast, AstNode, TextRange, Direction, SyntaxKind, SyntaxKind::*, SyntaxElement, T}; 3use ra_syntax::{ast, AstNode, TextRange, Direction, SmolStr, SyntaxKind, SyntaxKind::*, SyntaxElement, T};
4use ra_db::SourceDatabase; 4use ra_db::SourceDatabase;
5use ra_prof::profile; 5use ra_prof::profile;
6 6
@@ -10,6 +10,7 @@ use crate::{FileId, db::RootDatabase};
10pub struct HighlightedRange { 10pub 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
15fn is_control_keyword(kind: SyntaxKind) -> bool { 16fn is_control_keyword(kind: SyntaxKind) -> bool {
@@ -29,22 +30,36 @@ fn is_control_keyword(kind: SyntaxKind) -> bool {
29 30
30pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRange> { 31pub(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
117pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId) -> String { 164pub(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
162const STYLE: &str = " 228const STYLE: &str = "
163<style> 229<style>
164pre { 230body { margin: 0; }
165 color: #DCDCCC; 231pre { 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#"
292fn 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}