aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api/src/syntax_highlighting.rs
diff options
context:
space:
mode:
authorPascal Hertleif <[email protected]>2019-05-25 15:23:58 +0100
committerPascal Hertleif <[email protected]>2019-05-27 10:26:35 +0100
commit43d5a4965308ec4b594725c0bd02cb046bdb730c (patch)
tree3a6e2965b065e61310deaa4186a8cec6535fd244 /crates/ra_ide_api/src/syntax_highlighting.rs
parented89b0638b1dbf8f9a33d9a95e829e602142bb05 (diff)
More clever highlighting, incl draft for structs
Diffstat (limited to 'crates/ra_ide_api/src/syntax_highlighting.rs')
-rw-r--r--crates/ra_ide_api/src/syntax_highlighting.rs185
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};
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 id: Option<u64>, 13 pub binding_hash: Option<u64>,
14} 14}
15 15
16fn is_control_keyword(kind: SyntaxKind) -> bool { 16fn is_control_keyword(kind: SyntaxKind) -> bool {
@@ -30,15 +30,18 @@ fn is_control_keyword(kind: SyntaxKind) -> bool {
30 30
31pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRange> { 31pub(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
145pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId) -> String { 174pub(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
190const STYLE: &str = " 233const STYLE: &str = "
191<style> 234<style>
192pre { 235body { margin: 0; }
193 color: #DCDCCC; 236pre { 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}