diff options
Diffstat (limited to 'crates/ra_ide/src/syntax_highlighting.rs')
-rw-r--r-- | crates/ra_ide/src/syntax_highlighting.rs | 632 |
1 files changed, 252 insertions, 380 deletions
diff --git a/crates/ra_ide/src/syntax_highlighting.rs b/crates/ra_ide/src/syntax_highlighting.rs index 812229b4e..b94b6a022 100644 --- a/crates/ra_ide/src/syntax_highlighting.rs +++ b/crates/ra_ide/src/syntax_highlighting.rs | |||
@@ -1,134 +1,141 @@ | |||
1 | //! FIXME: write short doc here | 1 | //! Implements syntax highlighting. |
2 | 2 | ||
3 | use hir::{HirFileId, InFile, Name, SourceAnalyzer, SourceBinder}; | 3 | mod tags; |
4 | use ra_db::SourceDatabase; | 4 | mod html; |
5 | use ra_ide_db::{defs::NameDefinition, RootDatabase}; | 5 | #[cfg(test)] |
6 | mod tests; | ||
7 | |||
8 | use hir::{Name, Semantics}; | ||
9 | use ra_ide_db::{ | ||
10 | defs::{classify_name, NameClass, NameDefinition}, | ||
11 | RootDatabase, | ||
12 | }; | ||
6 | use ra_prof::profile; | 13 | use ra_prof::profile; |
7 | use ra_syntax::{ | 14 | use ra_syntax::{ |
8 | ast, AstNode, Direction, SyntaxElement, SyntaxKind, SyntaxKind::*, SyntaxToken, TextRange, | 15 | ast::{self, HasQuotes, HasStringValue}, |
9 | WalkEvent, T, | 16 | AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, |
17 | SyntaxKind::*, | ||
18 | SyntaxToken, TextRange, WalkEvent, T, | ||
10 | }; | 19 | }; |
11 | use rustc_hash::FxHashMap; | 20 | use rustc_hash::FxHashMap; |
12 | 21 | ||
13 | use crate::{ | 22 | use crate::{call_info::call_info_for_token, references::classify_name_ref, Analysis, FileId}; |
14 | expand::descend_into_macros_with_analyzer, | ||
15 | references::{classify_name, classify_name_ref}, | ||
16 | FileId, | ||
17 | }; | ||
18 | 23 | ||
19 | pub mod tags { | 24 | pub(crate) use html::highlight_as_html; |
20 | pub const FIELD: &str = "field"; | 25 | pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag}; |
21 | pub const FUNCTION: &str = "function"; | ||
22 | pub const MODULE: &str = "module"; | ||
23 | pub const CONSTANT: &str = "constant"; | ||
24 | pub const MACRO: &str = "macro"; | ||
25 | |||
26 | pub const VARIABLE: &str = "variable"; | ||
27 | pub const VARIABLE_MUT: &str = "variable.mut"; | ||
28 | |||
29 | pub const TYPE: &str = "type"; | ||
30 | pub const TYPE_BUILTIN: &str = "type.builtin"; | ||
31 | pub const TYPE_SELF: &str = "type.self"; | ||
32 | pub const TYPE_PARAM: &str = "type.param"; | ||
33 | pub const TYPE_LIFETIME: &str = "type.lifetime"; | ||
34 | |||
35 | pub const LITERAL_BYTE: &str = "literal.byte"; | ||
36 | pub const LITERAL_NUMERIC: &str = "literal.numeric"; | ||
37 | pub const LITERAL_CHAR: &str = "literal.char"; | ||
38 | |||
39 | pub const LITERAL_COMMENT: &str = "comment"; | ||
40 | pub const LITERAL_STRING: &str = "string"; | ||
41 | pub const LITERAL_ATTRIBUTE: &str = "attribute"; | ||
42 | |||
43 | pub const KEYWORD: &str = "keyword"; | ||
44 | pub const KEYWORD_UNSAFE: &str = "keyword.unsafe"; | ||
45 | pub const KEYWORD_CONTROL: &str = "keyword.control"; | ||
46 | } | ||
47 | 26 | ||
48 | #[derive(Debug)] | 27 | #[derive(Debug)] |
49 | pub struct HighlightedRange { | 28 | pub struct HighlightedRange { |
50 | pub range: TextRange, | 29 | pub range: TextRange, |
51 | pub tag: &'static str, | 30 | pub highlight: Highlight, |
52 | pub binding_hash: Option<u64>, | 31 | pub binding_hash: Option<u64>, |
53 | } | 32 | } |
54 | 33 | ||
55 | fn is_control_keyword(kind: SyntaxKind) -> bool { | 34 | pub(crate) fn highlight( |
56 | match kind { | 35 | db: &RootDatabase, |
57 | T![for] | 36 | file_id: FileId, |
58 | | T![loop] | 37 | range_to_highlight: Option<TextRange>, |
59 | | T![while] | 38 | ) -> Vec<HighlightedRange> { |
60 | | T![continue] | ||
61 | | T![break] | ||
62 | | T![if] | ||
63 | | T![else] | ||
64 | | T![match] | ||
65 | | T![return] => true, | ||
66 | _ => false, | ||
67 | } | ||
68 | } | ||
69 | |||
70 | pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Vec<HighlightedRange> { | ||
71 | let _p = profile("highlight"); | 39 | let _p = profile("highlight"); |
72 | let parse = db.parse(file_id); | 40 | let sema = Semantics::new(db); |
73 | let root = parse.tree().syntax().clone(); | 41 | |
42 | // Determine the root based on the given range. | ||
43 | let (root, range_to_highlight) = { | ||
44 | let source_file = sema.parse(file_id); | ||
45 | match range_to_highlight { | ||
46 | Some(range) => { | ||
47 | let node = match source_file.syntax().covering_element(range) { | ||
48 | NodeOrToken::Node(it) => it, | ||
49 | NodeOrToken::Token(it) => it.parent(), | ||
50 | }; | ||
51 | (node, range) | ||
52 | } | ||
53 | None => (source_file.syntax().clone(), source_file.syntax().text_range()), | ||
54 | } | ||
55 | }; | ||
74 | 56 | ||
75 | let mut sb = SourceBinder::new(db); | ||
76 | let mut bindings_shadow_count: FxHashMap<Name, u32> = FxHashMap::default(); | 57 | let mut bindings_shadow_count: FxHashMap<Name, u32> = FxHashMap::default(); |
77 | let mut res = Vec::new(); | 58 | let mut res = Vec::new(); |
78 | let analyzer = sb.analyze(InFile::new(file_id.into(), &root), None); | ||
79 | 59 | ||
80 | let mut in_macro_call = None; | 60 | let mut current_macro_call: Option<ast::MacroCall> = None; |
81 | 61 | ||
62 | // Walk all nodes, keeping track of whether we are inside a macro or not. | ||
63 | // If in macro, expand it first and highlight the expanded code. | ||
82 | for event in root.preorder_with_tokens() { | 64 | for event in root.preorder_with_tokens() { |
83 | match event { | 65 | let event_range = match &event { |
84 | WalkEvent::Enter(node) => match node.kind() { | 66 | WalkEvent::Enter(it) => it.text_range(), |
85 | MACRO_CALL => { | 67 | WalkEvent::Leave(it) => it.text_range(), |
86 | in_macro_call = Some(node.clone()); | 68 | }; |
87 | if let Some(range) = highlight_macro(InFile::new(file_id.into(), node)) { | 69 | |
88 | res.push(HighlightedRange { range, tag: tags::MACRO, binding_hash: None }); | 70 | // Element outside of the viewport, no need to highlight |
89 | } | 71 | if range_to_highlight.intersection(&event_range).is_none() { |
90 | } | 72 | continue; |
91 | _ if in_macro_call.is_some() => { | 73 | } |
92 | if let Some(token) = node.as_token() { | 74 | |
93 | if let Some((tag, binding_hash)) = highlight_token_tree( | 75 | // Track "inside macro" state |
94 | &mut sb, | 76 | match event.clone().map(|it| it.into_node().and_then(ast::MacroCall::cast)) { |
95 | &analyzer, | 77 | WalkEvent::Enter(Some(mc)) => { |
96 | &mut bindings_shadow_count, | 78 | current_macro_call = Some(mc.clone()); |
97 | InFile::new(file_id.into(), token.clone()), | 79 | if let Some(range) = macro_call_range(&mc) { |
98 | ) { | 80 | res.push(HighlightedRange { |
99 | res.push(HighlightedRange { | 81 | range, |
100 | range: node.text_range(), | 82 | highlight: HighlightTag::Macro.into(), |
101 | tag, | 83 | binding_hash: None, |
102 | binding_hash, | 84 | }); |
103 | }); | ||
104 | } | ||
105 | } | ||
106 | } | ||
107 | _ => { | ||
108 | if let Some((tag, binding_hash)) = highlight_node( | ||
109 | &mut sb, | ||
110 | &mut bindings_shadow_count, | ||
111 | InFile::new(file_id.into(), node.clone()), | ||
112 | ) { | ||
113 | res.push(HighlightedRange { range: node.text_range(), tag, binding_hash }); | ||
114 | } | ||
115 | } | ||
116 | }, | ||
117 | WalkEvent::Leave(node) => { | ||
118 | if let Some(m) = in_macro_call.as_ref() { | ||
119 | if *m == node { | ||
120 | in_macro_call = None; | ||
121 | } | ||
122 | } | 85 | } |
86 | continue; | ||
123 | } | 87 | } |
88 | WalkEvent::Leave(Some(mc)) => { | ||
89 | assert!(current_macro_call == Some(mc)); | ||
90 | current_macro_call = None; | ||
91 | continue; | ||
92 | } | ||
93 | _ => (), | ||
94 | } | ||
95 | |||
96 | let element = match event { | ||
97 | WalkEvent::Enter(it) => it, | ||
98 | WalkEvent::Leave(_) => continue, | ||
99 | }; | ||
100 | |||
101 | let range = element.text_range(); | ||
102 | |||
103 | let element_to_highlight = if current_macro_call.is_some() { | ||
104 | // Inside a macro -- expand it first | ||
105 | let token = match element.clone().into_token() { | ||
106 | Some(it) if it.parent().kind() == TOKEN_TREE => it, | ||
107 | _ => continue, | ||
108 | }; | ||
109 | let token = sema.descend_into_macros(token.clone()); | ||
110 | let parent = token.parent(); | ||
111 | // We only care Name and Name_ref | ||
112 | match (token.kind(), parent.kind()) { | ||
113 | (IDENT, NAME) | (IDENT, NAME_REF) => parent.into(), | ||
114 | _ => token.into(), | ||
115 | } | ||
116 | } else { | ||
117 | element.clone() | ||
118 | }; | ||
119 | |||
120 | if let Some(token) = element.as_token().cloned().and_then(ast::RawString::cast) { | ||
121 | let expanded = element_to_highlight.as_token().unwrap().clone(); | ||
122 | if highlight_injection(&mut res, &sema, token, expanded).is_some() { | ||
123 | eprintln!("res = {:?}", res); | ||
124 | continue; | ||
125 | } | ||
126 | } | ||
127 | |||
128 | if let Some((highlight, binding_hash)) = | ||
129 | highlight_element(&sema, &mut bindings_shadow_count, element_to_highlight) | ||
130 | { | ||
131 | res.push(HighlightedRange { range, highlight, binding_hash }); | ||
124 | } | 132 | } |
125 | } | 133 | } |
126 | 134 | ||
127 | res | 135 | res |
128 | } | 136 | } |
129 | 137 | ||
130 | fn highlight_macro(node: InFile<SyntaxElement>) -> Option<TextRange> { | 138 | fn macro_call_range(macro_call: &ast::MacroCall) -> Option<TextRange> { |
131 | let macro_call = ast::MacroCall::cast(node.value.as_node()?.clone())?; | ||
132 | let path = macro_call.path()?; | 139 | let path = macro_call.path()?; |
133 | let name_ref = path.segment()?.name_ref()?; | 140 | let name_ref = path.segment()?.name_ref()?; |
134 | 141 | ||
@@ -144,101 +151,100 @@ fn highlight_macro(node: InFile<SyntaxElement>) -> Option<TextRange> { | |||
144 | Some(TextRange::from_to(range_start, range_end)) | 151 | Some(TextRange::from_to(range_start, range_end)) |
145 | } | 152 | } |
146 | 153 | ||
147 | fn highlight_token_tree( | 154 | fn highlight_element( |
148 | sb: &mut SourceBinder<RootDatabase>, | 155 | sema: &Semantics<RootDatabase>, |
149 | analyzer: &SourceAnalyzer, | ||
150 | bindings_shadow_count: &mut FxHashMap<Name, u32>, | ||
151 | token: InFile<SyntaxToken>, | ||
152 | ) -> Option<(&'static str, Option<u64>)> { | ||
153 | if token.value.parent().kind() != TOKEN_TREE { | ||
154 | return None; | ||
155 | } | ||
156 | let token = descend_into_macros_with_analyzer(sb.db, analyzer, token); | ||
157 | let expanded = { | ||
158 | let parent = token.value.parent(); | ||
159 | // We only care Name and Name_ref | ||
160 | match (token.value.kind(), parent.kind()) { | ||
161 | (IDENT, NAME) | (IDENT, NAME_REF) => token.with_value(parent.into()), | ||
162 | _ => token.map(|it| it.into()), | ||
163 | } | ||
164 | }; | ||
165 | |||
166 | highlight_node(sb, bindings_shadow_count, expanded) | ||
167 | } | ||
168 | |||
169 | fn highlight_node( | ||
170 | sb: &mut SourceBinder<RootDatabase>, | ||
171 | bindings_shadow_count: &mut FxHashMap<Name, u32>, | 156 | bindings_shadow_count: &mut FxHashMap<Name, u32>, |
172 | node: InFile<SyntaxElement>, | 157 | element: SyntaxElement, |
173 | ) -> Option<(&'static str, Option<u64>)> { | 158 | ) -> Option<(Highlight, Option<u64>)> { |
174 | let db = sb.db; | 159 | let db = sema.db; |
175 | let mut binding_hash = None; | 160 | let mut binding_hash = None; |
176 | let tag = match node.value.kind() { | 161 | let highlight: Highlight = match element.kind() { |
177 | FN_DEF => { | 162 | FN_DEF => { |
178 | bindings_shadow_count.clear(); | 163 | bindings_shadow_count.clear(); |
179 | return None; | 164 | return None; |
180 | } | 165 | } |
181 | COMMENT => tags::LITERAL_COMMENT, | 166 | |
182 | STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => tags::LITERAL_STRING, | 167 | // Highlight definitions depending on the "type" of the definition. |
183 | ATTR => tags::LITERAL_ATTRIBUTE, | 168 | NAME => { |
184 | // Special-case field init shorthand | 169 | let name = element.into_node().and_then(ast::Name::cast).unwrap(); |
185 | NAME_REF if node.value.parent().and_then(ast::RecordField::cast).is_some() => tags::FIELD, | 170 | let name_kind = classify_name(sema, &name); |
186 | NAME_REF if node.value.ancestors().any(|it| it.kind() == ATTR) => return None, | 171 | |
187 | NAME_REF => { | 172 | if let Some(NameClass::NameDefinition(NameDefinition::Local(local))) = &name_kind { |
188 | let name_ref = node.value.as_node().cloned().and_then(ast::NameRef::cast).unwrap(); | 173 | if let Some(name) = local.name(db) { |
189 | let name_kind = classify_name_ref(sb, node.with_value(&name_ref)); | 174 | let shadow_count = bindings_shadow_count.entry(name.clone()).or_default(); |
175 | *shadow_count += 1; | ||
176 | binding_hash = Some(calc_binding_hash(&name, *shadow_count)) | ||
177 | } | ||
178 | }; | ||
179 | |||
190 | match name_kind { | 180 | match name_kind { |
191 | Some(name_kind) => { | 181 | Some(NameClass::NameDefinition(def)) => { |
192 | if let NameDefinition::Local(local) = &name_kind { | 182 | highlight_name(db, def) | HighlightModifier::Definition |
193 | if let Some(name) = local.name(db) { | ||
194 | let shadow_count = | ||
195 | bindings_shadow_count.entry(name.clone()).or_default(); | ||
196 | binding_hash = | ||
197 | Some(calc_binding_hash(node.file_id, &name, *shadow_count)) | ||
198 | } | ||
199 | }; | ||
200 | |||
201 | highlight_name(db, name_kind) | ||
202 | } | 183 | } |
203 | _ => return None, | 184 | Some(NameClass::ConstReference(def)) => highlight_name(db, def), |
185 | None => highlight_name_by_syntax(name) | HighlightModifier::Definition, | ||
204 | } | 186 | } |
205 | } | 187 | } |
206 | NAME => { | ||
207 | let name = node.value.as_node().cloned().and_then(ast::Name::cast).unwrap(); | ||
208 | let name_kind = classify_name(sb, node.with_value(&name)); | ||
209 | 188 | ||
210 | if let Some(NameDefinition::Local(local)) = &name_kind { | 189 | // Highlight references like the definitions they resolve to |
190 | |||
191 | // Special-case field init shorthand | ||
192 | NAME_REF if element.parent().and_then(ast::RecordField::cast).is_some() => { | ||
193 | HighlightTag::Field.into() | ||
194 | } | ||
195 | NAME_REF if element.ancestors().any(|it| it.kind() == ATTR) => return None, | ||
196 | NAME_REF => { | ||
197 | let name_ref = element.into_node().and_then(ast::NameRef::cast).unwrap(); | ||
198 | let name_kind = classify_name_ref(sema, &name_ref)?; | ||
199 | |||
200 | if let NameDefinition::Local(local) = &name_kind { | ||
211 | if let Some(name) = local.name(db) { | 201 | if let Some(name) = local.name(db) { |
212 | let shadow_count = bindings_shadow_count.entry(name.clone()).or_default(); | 202 | let shadow_count = bindings_shadow_count.entry(name.clone()).or_default(); |
213 | *shadow_count += 1; | 203 | binding_hash = Some(calc_binding_hash(&name, *shadow_count)) |
214 | binding_hash = Some(calc_binding_hash(node.file_id, &name, *shadow_count)) | ||
215 | } | 204 | } |
216 | }; | 205 | }; |
217 | 206 | ||
218 | match name_kind { | 207 | highlight_name(db, name_kind) |
219 | Some(name_kind) => highlight_name(db, name_kind), | 208 | } |
220 | None => name.syntax().parent().map_or(tags::FUNCTION, |x| match x.kind() { | 209 | |
221 | STRUCT_DEF | ENUM_DEF | TRAIT_DEF | TYPE_ALIAS_DEF => tags::TYPE, | 210 | // Simple token-based highlighting |
222 | TYPE_PARAM => tags::TYPE_PARAM, | 211 | COMMENT => HighlightTag::Comment.into(), |
223 | RECORD_FIELD_DEF => tags::FIELD, | 212 | STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => HighlightTag::StringLiteral.into(), |
224 | _ => tags::FUNCTION, | 213 | ATTR => HighlightTag::Attribute.into(), |
225 | }), | 214 | INT_NUMBER | FLOAT_NUMBER => HighlightTag::NumericLiteral.into(), |
215 | BYTE => HighlightTag::ByteLiteral.into(), | ||
216 | CHAR => HighlightTag::CharLiteral.into(), | ||
217 | LIFETIME => { | ||
218 | let h = Highlight::new(HighlightTag::Lifetime); | ||
219 | dbg!(match element.parent().map(|it| it.kind()) { | ||
220 | Some(LIFETIME_PARAM) | Some(LABEL) => h | HighlightModifier::Definition, | ||
221 | _ => h, | ||
222 | }) | ||
223 | } | ||
224 | |||
225 | k if k.is_keyword() => { | ||
226 | let h = Highlight::new(HighlightTag::Keyword); | ||
227 | match k { | ||
228 | T![break] | ||
229 | | T![continue] | ||
230 | | T![else] | ||
231 | | T![for] | ||
232 | | T![if] | ||
233 | | T![loop] | ||
234 | | T![match] | ||
235 | | T![return] | ||
236 | | T![while] => h | HighlightModifier::Control, | ||
237 | T![unsafe] => h | HighlightModifier::Unsafe, | ||
238 | _ => h, | ||
226 | } | 239 | } |
227 | } | 240 | } |
228 | INT_NUMBER | FLOAT_NUMBER => tags::LITERAL_NUMERIC, | ||
229 | BYTE => tags::LITERAL_BYTE, | ||
230 | CHAR => tags::LITERAL_CHAR, | ||
231 | LIFETIME => tags::TYPE_LIFETIME, | ||
232 | T![unsafe] => tags::KEYWORD_UNSAFE, | ||
233 | k if is_control_keyword(k) => tags::KEYWORD_CONTROL, | ||
234 | k if k.is_keyword() => tags::KEYWORD, | ||
235 | 241 | ||
236 | _ => return None, | 242 | _ => return None, |
237 | }; | 243 | }; |
238 | 244 | ||
239 | return Some((tag, binding_hash)); | 245 | return Some((highlight, binding_hash)); |
240 | 246 | ||
241 | fn calc_binding_hash(file_id: HirFileId, name: &Name, shadow_count: u32) -> u64 { | 247 | fn calc_binding_hash(name: &Name, shadow_count: u32) -> u64 { |
242 | fn hash<T: std::hash::Hash + std::fmt::Debug>(x: T) -> u64 { | 248 | fn hash<T: std::hash::Hash + std::fmt::Debug>(x: T) -> u64 { |
243 | use std::{collections::hash_map::DefaultHasher, hash::Hasher}; | 249 | use std::{collections::hash_map::DefaultHasher, hash::Hasher}; |
244 | 250 | ||
@@ -247,232 +253,98 @@ fn highlight_node( | |||
247 | hasher.finish() | 253 | hasher.finish() |
248 | } | 254 | } |
249 | 255 | ||
250 | hash((file_id, name, shadow_count)) | 256 | hash((name, shadow_count)) |
251 | } | ||
252 | } | ||
253 | |||
254 | pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId, rainbow: bool) -> String { | ||
255 | let parse = db.parse(file_id); | ||
256 | |||
257 | fn rainbowify(seed: u64) -> String { | ||
258 | use rand::prelude::*; | ||
259 | let mut rng = SmallRng::seed_from_u64(seed); | ||
260 | format!( | ||
261 | "hsl({h},{s}%,{l}%)", | ||
262 | h = rng.gen_range::<u16, _, _>(0, 361), | ||
263 | s = rng.gen_range::<u16, _, _>(42, 99), | ||
264 | l = rng.gen_range::<u16, _, _>(40, 91), | ||
265 | ) | ||
266 | } | ||
267 | |||
268 | let mut ranges = highlight(db, file_id); | ||
269 | ranges.sort_by_key(|it| it.range.start()); | ||
270 | // quick non-optimal heuristic to intersect token ranges and highlighted ranges | ||
271 | let mut frontier = 0; | ||
272 | let mut could_intersect: Vec<&HighlightedRange> = Vec::new(); | ||
273 | |||
274 | let mut buf = String::new(); | ||
275 | buf.push_str(&STYLE); | ||
276 | buf.push_str("<pre><code>"); | ||
277 | let tokens = parse.tree().syntax().descendants_with_tokens().filter_map(|it| it.into_token()); | ||
278 | for token in tokens { | ||
279 | could_intersect.retain(|it| token.text_range().start() <= it.range.end()); | ||
280 | while let Some(r) = ranges.get(frontier) { | ||
281 | if r.range.start() <= token.text_range().end() { | ||
282 | could_intersect.push(r); | ||
283 | frontier += 1; | ||
284 | } else { | ||
285 | break; | ||
286 | } | ||
287 | } | ||
288 | let text = html_escape(&token.text()); | ||
289 | let ranges = could_intersect | ||
290 | .iter() | ||
291 | .filter(|it| token.text_range().is_subrange(&it.range)) | ||
292 | .collect::<Vec<_>>(); | ||
293 | if ranges.is_empty() { | ||
294 | buf.push_str(&text); | ||
295 | } else { | ||
296 | let classes = ranges.iter().map(|x| x.tag).collect::<Vec<_>>().join(" "); | ||
297 | let binding_hash = ranges.first().and_then(|x| x.binding_hash); | ||
298 | let color = match (rainbow, binding_hash) { | ||
299 | (true, Some(hash)) => format!( | ||
300 | " data-binding-hash=\"{}\" style=\"color: {};\"", | ||
301 | hash, | ||
302 | rainbowify(hash) | ||
303 | ), | ||
304 | _ => "".into(), | ||
305 | }; | ||
306 | buf.push_str(&format!("<span class=\"{}\"{}>{}</span>", classes, color, text)); | ||
307 | } | ||
308 | } | 257 | } |
309 | buf.push_str("</code></pre>"); | ||
310 | buf | ||
311 | } | 258 | } |
312 | 259 | ||
313 | fn highlight_name(db: &RootDatabase, def: NameDefinition) -> &'static str { | 260 | fn highlight_name(db: &RootDatabase, def: NameDefinition) -> Highlight { |
314 | match def { | 261 | match def { |
315 | NameDefinition::Macro(_) => tags::MACRO, | 262 | NameDefinition::Macro(_) => HighlightTag::Macro, |
316 | NameDefinition::StructField(_) => tags::FIELD, | 263 | NameDefinition::StructField(_) => HighlightTag::Field, |
317 | NameDefinition::ModuleDef(hir::ModuleDef::Module(_)) => tags::MODULE, | 264 | NameDefinition::ModuleDef(def) => match def { |
318 | NameDefinition::ModuleDef(hir::ModuleDef::Function(_)) => tags::FUNCTION, | 265 | hir::ModuleDef::Module(_) => HighlightTag::Module, |
319 | NameDefinition::ModuleDef(hir::ModuleDef::Adt(_)) => tags::TYPE, | 266 | hir::ModuleDef::Function(_) => HighlightTag::Function, |
320 | NameDefinition::ModuleDef(hir::ModuleDef::EnumVariant(_)) => tags::CONSTANT, | 267 | hir::ModuleDef::Adt(hir::Adt::Struct(_)) => HighlightTag::Struct, |
321 | NameDefinition::ModuleDef(hir::ModuleDef::Const(_)) => tags::CONSTANT, | 268 | hir::ModuleDef::Adt(hir::Adt::Enum(_)) => HighlightTag::Enum, |
322 | NameDefinition::ModuleDef(hir::ModuleDef::Static(_)) => tags::CONSTANT, | 269 | hir::ModuleDef::Adt(hir::Adt::Union(_)) => HighlightTag::Union, |
323 | NameDefinition::ModuleDef(hir::ModuleDef::Trait(_)) => tags::TYPE, | 270 | hir::ModuleDef::EnumVariant(_) => HighlightTag::EnumVariant, |
324 | NameDefinition::ModuleDef(hir::ModuleDef::TypeAlias(_)) => tags::TYPE, | 271 | hir::ModuleDef::Const(_) => HighlightTag::Constant, |
325 | NameDefinition::ModuleDef(hir::ModuleDef::BuiltinType(_)) => tags::TYPE_BUILTIN, | 272 | hir::ModuleDef::Static(_) => HighlightTag::Static, |
326 | NameDefinition::SelfType(_) => tags::TYPE_SELF, | 273 | hir::ModuleDef::Trait(_) => HighlightTag::Trait, |
327 | NameDefinition::TypeParam(_) => tags::TYPE_PARAM, | 274 | hir::ModuleDef::TypeAlias(_) => HighlightTag::TypeAlias, |
275 | hir::ModuleDef::BuiltinType(_) => HighlightTag::BuiltinType, | ||
276 | }, | ||
277 | NameDefinition::SelfType(_) => HighlightTag::SelfType, | ||
278 | NameDefinition::TypeParam(_) => HighlightTag::TypeParam, | ||
279 | // FIXME: distinguish between locals and parameters | ||
328 | NameDefinition::Local(local) => { | 280 | NameDefinition::Local(local) => { |
281 | let mut h = Highlight::new(HighlightTag::Local); | ||
329 | if local.is_mut(db) || local.ty(db).is_mutable_reference() { | 282 | if local.is_mut(db) || local.ty(db).is_mutable_reference() { |
330 | tags::VARIABLE_MUT | 283 | h |= HighlightModifier::Mutable; |
331 | } else { | ||
332 | tags::VARIABLE | ||
333 | } | 284 | } |
285 | return h; | ||
334 | } | 286 | } |
335 | } | 287 | } |
288 | .into() | ||
336 | } | 289 | } |
337 | 290 | ||
338 | //FIXME: like, real html escaping | 291 | fn highlight_name_by_syntax(name: ast::Name) -> Highlight { |
339 | fn html_escape(text: &str) -> String { | 292 | let default = HighlightTag::Function.into(); |
340 | text.replace("<", "<").replace(">", ">") | ||
341 | } | ||
342 | |||
343 | const STYLE: &str = " | ||
344 | <style> | ||
345 | body { margin: 0; } | ||
346 | pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } | ||
347 | |||
348 | .comment { color: #7F9F7F; } | ||
349 | .string { color: #CC9393; } | ||
350 | .field { color: #94BFF3; } | ||
351 | .function { color: #93E0E3; } | ||
352 | .parameter { color: #94BFF3; } | ||
353 | .text { color: #DCDCCC; } | ||
354 | .type { color: #7CB8BB; } | ||
355 | .type\\.builtin { color: #8CD0D3; } | ||
356 | .type\\.param { color: #20999D; } | ||
357 | .attribute { color: #94BFF3; } | ||
358 | .literal { color: #BFEBBF; } | ||
359 | .literal\\.numeric { color: #6A8759; } | ||
360 | .macro { color: #94BFF3; } | ||
361 | .module { color: #AFD8AF; } | ||
362 | .variable { color: #DCDCCC; } | ||
363 | .variable\\.mut { color: #DCDCCC; text-decoration: underline; } | ||
364 | |||
365 | .keyword { color: #F0DFAF; } | ||
366 | .keyword\\.unsafe { color: #DFAF8F; } | ||
367 | .keyword\\.control { color: #F0DFAF; font-weight: bold; } | ||
368 | </style> | ||
369 | "; | ||
370 | |||
371 | #[cfg(test)] | ||
372 | mod tests { | ||
373 | use std::fs; | ||
374 | |||
375 | use test_utils::{assert_eq_text, project_dir, read_text}; | ||
376 | |||
377 | use crate::mock_analysis::{single_file, MockAnalysis}; | ||
378 | |||
379 | #[test] | ||
380 | fn test_highlighting() { | ||
381 | let (analysis, file_id) = single_file( | ||
382 | r#" | ||
383 | #[derive(Clone, Debug)] | ||
384 | struct Foo { | ||
385 | pub x: i32, | ||
386 | pub y: i32, | ||
387 | } | ||
388 | |||
389 | fn foo<T>() -> T { | ||
390 | unimplemented!(); | ||
391 | foo::<i32>(); | ||
392 | } | ||
393 | 293 | ||
394 | macro_rules! def_fn { | 294 | let parent = match name.syntax().parent() { |
395 | ($($tt:tt)*) => {$($tt)*} | 295 | Some(it) => it, |
396 | } | 296 | _ => return default, |
297 | }; | ||
397 | 298 | ||
398 | def_fn!{ | 299 | match parent.kind() { |
399 | fn bar() -> u32 { | 300 | STRUCT_DEF => HighlightTag::Struct.into(), |
400 | 100 | 301 | ENUM_DEF => HighlightTag::Enum.into(), |
302 | UNION_DEF => HighlightTag::Union.into(), | ||
303 | TRAIT_DEF => HighlightTag::Trait.into(), | ||
304 | TYPE_ALIAS_DEF => HighlightTag::TypeAlias.into(), | ||
305 | TYPE_PARAM => HighlightTag::TypeParam.into(), | ||
306 | RECORD_FIELD_DEF => HighlightTag::Field.into(), | ||
307 | _ => default, | ||
401 | } | 308 | } |
402 | } | 309 | } |
403 | 310 | ||
404 | // comment | 311 | fn highlight_injection( |
405 | fn main() { | 312 | acc: &mut Vec<HighlightedRange>, |
406 | println!("Hello, {}!", 92); | 313 | sema: &Semantics<RootDatabase>, |
407 | 314 | literal: ast::RawString, | |
408 | let mut vec = Vec::new(); | 315 | expanded: SyntaxToken, |
409 | if true { | 316 | ) -> Option<()> { |
410 | let x = 92; | 317 | let call_info = call_info_for_token(&sema, expanded)?; |
411 | vec.push(Foo { x, y: 1 }); | 318 | let idx = call_info.active_parameter?; |
319 | let name = call_info.signature.parameter_names.get(idx)?; | ||
320 | if name != "ra_fixture" { | ||
321 | return None; | ||
412 | } | 322 | } |
413 | unsafe { vec.set_len(0); } | 323 | let value = literal.value()?; |
414 | 324 | let (analysis, tmp_file_id) = Analysis::from_single_file(value); | |
415 | let mut x = 42; | 325 | |
416 | let y = &mut x; | 326 | if let Some(range) = literal.open_quote_text_range() { |
417 | let z = &y; | 327 | acc.push(HighlightedRange { |
418 | 328 | range, | |
419 | y; | 329 | highlight: HighlightTag::StringLiteral.into(), |
420 | } | 330 | binding_hash: None, |
421 | 331 | }) | |
422 | enum E<X> { | ||
423 | V(X) | ||
424 | } | ||
425 | |||
426 | impl<X> E<X> { | ||
427 | fn new<T>() -> E<T> {} | ||
428 | } | ||
429 | "# | ||
430 | .trim(), | ||
431 | ); | ||
432 | let dst_file = project_dir().join("crates/ra_ide/src/snapshots/highlighting.html"); | ||
433 | let actual_html = &analysis.highlight_as_html(file_id, false).unwrap(); | ||
434 | let expected_html = &read_text(&dst_file); | ||
435 | fs::write(dst_file, &actual_html).unwrap(); | ||
436 | assert_eq_text!(expected_html, actual_html); | ||
437 | } | 332 | } |
438 | 333 | ||
439 | #[test] | 334 | for mut h in analysis.highlight(tmp_file_id).unwrap() { |
440 | fn test_rainbow_highlighting() { | 335 | if let Some(r) = literal.map_range_up(h.range) { |
441 | let (analysis, file_id) = single_file( | 336 | h.range = r; |
442 | r#" | 337 | acc.push(h) |
443 | fn main() { | 338 | } |
444 | let hello = "hello"; | ||
445 | let x = hello.to_string(); | ||
446 | let y = hello.to_string(); | ||
447 | |||
448 | let x = "other color please!"; | ||
449 | let y = x.to_string(); | ||
450 | } | ||
451 | |||
452 | fn bar() { | ||
453 | let mut hello = "hello"; | ||
454 | } | ||
455 | "# | ||
456 | .trim(), | ||
457 | ); | ||
458 | let dst_file = project_dir().join("crates/ra_ide/src/snapshots/rainbow_highlighting.html"); | ||
459 | let actual_html = &analysis.highlight_as_html(file_id, true).unwrap(); | ||
460 | let expected_html = &read_text(&dst_file); | ||
461 | fs::write(dst_file, &actual_html).unwrap(); | ||
462 | assert_eq_text!(expected_html, actual_html); | ||
463 | } | 339 | } |
464 | 340 | ||
465 | #[test] | 341 | if let Some(range) = literal.close_quote_text_range() { |
466 | fn accidentally_quadratic() { | 342 | acc.push(HighlightedRange { |
467 | let file = project_dir().join("crates/ra_syntax/test_data/accidentally_quadratic"); | 343 | range, |
468 | let src = fs::read_to_string(file).unwrap(); | 344 | highlight: HighlightTag::StringLiteral.into(), |
469 | 345 | binding_hash: None, | |
470 | let mut mock = MockAnalysis::new(); | 346 | }) |
471 | let file_id = mock.add_file("/main.rs", &src); | ||
472 | let host = mock.analysis_host(); | ||
473 | |||
474 | // let t = std::time::Instant::now(); | ||
475 | let _ = host.analysis().highlight(file_id).unwrap(); | ||
476 | // eprintln!("elapsed: {:?}", t.elapsed()); | ||
477 | } | 347 | } |
348 | |||
349 | Some(()) | ||
478 | } | 350 | } |