aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide/src/syntax_highlighting.rs
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2019-11-27 18:32:33 +0000
committerAleksey Kladov <[email protected]>2019-11-27 18:35:06 +0000
commit757e593b253b4df7e6fc8bf15a4d4f34c9d484c5 (patch)
treed972d3a7e6457efdb5e0c558a8350db1818d07ae /crates/ra_ide/src/syntax_highlighting.rs
parentd9a36a736bfb91578a36505e7237212959bb55fe (diff)
rename ra_ide_api -> ra_ide
Diffstat (limited to 'crates/ra_ide/src/syntax_highlighting.rs')
-rw-r--r--crates/ra_ide/src/syntax_highlighting.rs342
1 files changed, 342 insertions, 0 deletions
diff --git a/crates/ra_ide/src/syntax_highlighting.rs b/crates/ra_ide/src/syntax_highlighting.rs
new file mode 100644
index 000000000..2c568a747
--- /dev/null
+++ b/crates/ra_ide/src/syntax_highlighting.rs
@@ -0,0 +1,342 @@
1//! FIXME: write short doc here
2
3use rustc_hash::{FxHashMap, FxHashSet};
4
5use hir::{Name, Source};
6use ra_db::SourceDatabase;
7use ra_prof::profile;
8use ra_syntax::{ast, AstNode, Direction, SyntaxElement, SyntaxKind, SyntaxKind::*, TextRange, T};
9
10use crate::{
11 db::RootDatabase,
12 references::{
13 classify_name, classify_name_ref,
14 NameKind::{self, *},
15 },
16 FileId,
17};
18
19#[derive(Debug)]
20pub struct HighlightedRange {
21 pub range: TextRange,
22 pub tag: &'static str,
23 pub binding_hash: Option<u64>,
24}
25
26fn is_control_keyword(kind: SyntaxKind) -> bool {
27 match kind {
28 T![for]
29 | T![loop]
30 | T![while]
31 | T![continue]
32 | T![break]
33 | T![if]
34 | T![else]
35 | T![match]
36 | T![return] => true,
37 _ => false,
38 }
39}
40
41pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRange> {
42 let _p = profile("highlight");
43 let parse = db.parse(file_id);
44 let root = parse.tree().syntax().clone();
45
46 fn calc_binding_hash(file_id: FileId, name: &Name, shadow_count: u32) -> u64 {
47 fn hash<T: std::hash::Hash + std::fmt::Debug>(x: T) -> u64 {
48 use std::{collections::hash_map::DefaultHasher, hash::Hasher};
49
50 let mut hasher = DefaultHasher::new();
51 x.hash(&mut hasher);
52 hasher.finish()
53 }
54
55 hash((file_id, name, shadow_count))
56 }
57
58 // Visited nodes to handle highlighting priorities
59 // FIXME: retain only ranges here
60 let mut highlighted: FxHashSet<SyntaxElement> = FxHashSet::default();
61 let mut bindings_shadow_count: FxHashMap<Name, u32> = FxHashMap::default();
62
63 let mut res = Vec::new();
64 for node in root.descendants_with_tokens() {
65 if highlighted.contains(&node) {
66 continue;
67 }
68 let mut binding_hash = None;
69 let tag = match node.kind() {
70 FN_DEF => {
71 bindings_shadow_count.clear();
72 continue;
73 }
74 COMMENT => "comment",
75 STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string",
76 ATTR => "attribute",
77 NAME_REF => {
78 if node.ancestors().any(|it| it.kind() == ATTR) {
79 continue;
80 }
81
82 let name_ref = node.as_node().cloned().and_then(ast::NameRef::cast).unwrap();
83 let name_kind =
84 classify_name_ref(db, Source::new(file_id.into(), &name_ref)).map(|d| d.kind);
85
86 if let Some(Local(local)) = &name_kind {
87 if let Some(name) = local.name(db) {
88 let shadow_count = bindings_shadow_count.entry(name.clone()).or_default();
89 binding_hash = Some(calc_binding_hash(file_id, &name, *shadow_count))
90 }
91 };
92
93 name_kind.map_or("text", |it| highlight_name(db, it))
94 }
95 NAME => {
96 let name = node.as_node().cloned().and_then(ast::Name::cast).unwrap();
97 let name_kind =
98 classify_name(db, Source::new(file_id.into(), &name)).map(|d| d.kind);
99
100 if let Some(Local(local)) = &name_kind {
101 if let Some(name) = local.name(db) {
102 let shadow_count = bindings_shadow_count.entry(name.clone()).or_default();
103 *shadow_count += 1;
104 binding_hash = Some(calc_binding_hash(file_id, &name, *shadow_count))
105 }
106 };
107
108 match name_kind {
109 Some(name_kind) => highlight_name(db, name_kind),
110 None => name.syntax().parent().map_or("function", |x| match x.kind() {
111 TYPE_PARAM | STRUCT_DEF | ENUM_DEF | TRAIT_DEF | TYPE_ALIAS_DEF => "type",
112 RECORD_FIELD_DEF => "field",
113 _ => "function",
114 }),
115 }
116 }
117 INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal",
118 LIFETIME => "parameter",
119 T![unsafe] => "keyword.unsafe",
120 k if is_control_keyword(k) => "keyword.control",
121 k if k.is_keyword() => "keyword",
122 _ => {
123 if let Some(macro_call) = node.as_node().cloned().and_then(ast::MacroCall::cast) {
124 if let Some(path) = macro_call.path() {
125 if let Some(segment) = path.segment() {
126 if let Some(name_ref) = segment.name_ref() {
127 highlighted.insert(name_ref.syntax().clone().into());
128 let range_start = name_ref.syntax().text_range().start();
129 let mut range_end = name_ref.syntax().text_range().end();
130 for sibling in path.syntax().siblings_with_tokens(Direction::Next) {
131 match sibling.kind() {
132 T![!] | IDENT => range_end = sibling.text_range().end(),
133 _ => (),
134 }
135 }
136 res.push(HighlightedRange {
137 range: TextRange::from_to(range_start, range_end),
138 tag: "macro",
139 binding_hash: None,
140 })
141 }
142 }
143 }
144 }
145 continue;
146 }
147 };
148 res.push(HighlightedRange { range: node.text_range(), tag, binding_hash })
149 }
150 res
151}
152
153pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId, rainbow: bool) -> String {
154 let parse = db.parse(file_id);
155
156 fn rainbowify(seed: u64) -> String {
157 use rand::prelude::*;
158 let mut rng = SmallRng::seed_from_u64(seed);
159 format!(
160 "hsl({h},{s}%,{l}%)",
161 h = rng.gen_range::<u16, _, _>(0, 361),
162 s = rng.gen_range::<u16, _, _>(42, 99),
163 l = rng.gen_range::<u16, _, _>(40, 91),
164 )
165 }
166
167 let mut ranges = highlight(db, file_id);
168 ranges.sort_by_key(|it| it.range.start());
169 // quick non-optimal heuristic to intersect token ranges and highlighted ranges
170 let mut frontier = 0;
171 let mut could_intersect: Vec<&HighlightedRange> = Vec::new();
172
173 let mut buf = String::new();
174 buf.push_str(&STYLE);
175 buf.push_str("<pre><code>");
176 let tokens = parse.tree().syntax().descendants_with_tokens().filter_map(|it| it.into_token());
177 for token in tokens {
178 could_intersect.retain(|it| token.text_range().start() <= it.range.end());
179 while let Some(r) = ranges.get(frontier) {
180 if r.range.start() <= token.text_range().end() {
181 could_intersect.push(r);
182 frontier += 1;
183 } else {
184 break;
185 }
186 }
187 let text = html_escape(&token.text());
188 let ranges = could_intersect
189 .iter()
190 .filter(|it| token.text_range().is_subrange(&it.range))
191 .collect::<Vec<_>>();
192 if ranges.is_empty() {
193 buf.push_str(&text);
194 } else {
195 let classes = ranges.iter().map(|x| x.tag).collect::<Vec<_>>().join(" ");
196 let binding_hash = ranges.first().and_then(|x| x.binding_hash);
197 let color = match (rainbow, binding_hash) {
198 (true, Some(hash)) => format!(
199 " data-binding-hash=\"{}\" style=\"color: {};\"",
200 hash,
201 rainbowify(hash)
202 ),
203 _ => "".into(),
204 };
205 buf.push_str(&format!("<span class=\"{}\"{}>{}</span>", classes, color, text));
206 }
207 }
208 buf.push_str("</code></pre>");
209 buf
210}
211
212fn highlight_name(db: &RootDatabase, name_kind: NameKind) -> &'static str {
213 match name_kind {
214 Macro(_) => "macro",
215 Field(_) => "field",
216 AssocItem(hir::AssocItem::Function(_)) => "function",
217 AssocItem(hir::AssocItem::Const(_)) => "constant",
218 AssocItem(hir::AssocItem::TypeAlias(_)) => "type",
219 Def(hir::ModuleDef::Module(_)) => "module",
220 Def(hir::ModuleDef::Function(_)) => "function",
221 Def(hir::ModuleDef::Adt(_)) => "type",
222 Def(hir::ModuleDef::EnumVariant(_)) => "constant",
223 Def(hir::ModuleDef::Const(_)) => "constant",
224 Def(hir::ModuleDef::Static(_)) => "constant",
225 Def(hir::ModuleDef::Trait(_)) => "type",
226 Def(hir::ModuleDef::TypeAlias(_)) => "type",
227 Def(hir::ModuleDef::BuiltinType(_)) => "type",
228 SelfType(_) => "type",
229 GenericParam(_) => "type",
230 Local(local) => {
231 if local.is_mut(db) {
232 "variable.mut"
233 } else if local.ty(db).is_mutable_reference() {
234 "variable.mut"
235 } else {
236 "variable"
237 }
238 }
239 }
240}
241
242//FIXME: like, real html escaping
243fn html_escape(text: &str) -> String {
244 text.replace("<", "&lt;").replace(">", "&gt;")
245}
246
247const STYLE: &str = "
248<style>
249body { margin: 0; }
250pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; }
251
252.comment { color: #7F9F7F; }
253.string { color: #CC9393; }
254.function { color: #93E0E3; }
255.parameter { color: #94BFF3; }
256.builtin { color: #DD6718; }
257.text { color: #DCDCCC; }
258.attribute { color: #94BFF3; }
259.literal { color: #BFEBBF; }
260.macro { color: #94BFF3; }
261.variable { color: #DCDCCC; }
262.variable\\.mut { color: #DCDCCC; text-decoration: underline; }
263
264.keyword { color: #F0DFAF; }
265.keyword\\.unsafe { color: #DFAF8F; }
266.keyword\\.control { color: #F0DFAF; font-weight: bold; }
267</style>
268";
269
270#[cfg(test)]
271mod tests {
272 use crate::mock_analysis::single_file;
273 use test_utils::{assert_eq_text, project_dir, read_text};
274
275 #[test]
276 fn test_highlighting() {
277 let (analysis, file_id) = single_file(
278 r#"
279#[derive(Clone, Debug)]
280struct Foo {
281 pub x: i32,
282 pub y: i32,
283}
284
285fn foo<T>() -> T {
286 unimplemented!();
287 foo::<i32>();
288}
289
290// comment
291fn main() {
292 println!("Hello, {}!", 92);
293
294 let mut vec = Vec::new();
295 if true {
296 vec.push(Foo { x: 0, y: 1 });
297 }
298 unsafe { vec.set_len(0); }
299
300 let mut x = 42;
301 let y = &mut x;
302 let z = &y;
303
304 y;
305}
306"#
307 .trim(),
308 );
309 let dst_file = project_dir().join("crates/ra_ide/src/snapshots/highlighting.html");
310 let actual_html = &analysis.highlight_as_html(file_id, false).unwrap();
311 let expected_html = &read_text(&dst_file);
312 std::fs::write(dst_file, &actual_html).unwrap();
313 assert_eq_text!(expected_html, actual_html);
314 }
315
316 #[test]
317 fn test_rainbow_highlighting() {
318 let (analysis, file_id) = single_file(
319 r#"
320fn main() {
321 let hello = "hello";
322 let x = hello.to_string();
323 let y = hello.to_string();
324
325 let x = "other color please!";
326 let y = x.to_string();
327}
328
329fn bar() {
330 let mut hello = "hello";
331}
332"#
333 .trim(),
334 );
335 let dst_file =
336 project_dir().join("crates/ra_ide/src/snapshots/rainbow_highlighting.html");
337 let actual_html = &analysis.highlight_as_html(file_id, true).unwrap();
338 let expected_html = &read_text(&dst_file);
339 std::fs::write(dst_file, &actual_html).unwrap();
340 assert_eq_text!(expected_html, actual_html);
341 }
342}