diff options
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 | } |