aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api/src/syntax_highlighting.rs
diff options
context:
space:
mode:
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}