diff options
author | Aleksey Kladov <[email protected]> | 2020-08-13 16:42:52 +0100 |
---|---|---|
committer | Aleksey Kladov <[email protected]> | 2020-08-13 16:58:27 +0100 |
commit | 1b0c7701cc97cd7bef8bb9729011d4cf291a60c5 (patch) | |
tree | b69f0c9947d9cec522ce835d7213b21075fe6dcf /crates/ide/src/syntax_highlighting | |
parent | fc34403018079ea053f26d0a31b7517053c7dd8c (diff) |
Rename ra_ide -> ide
Diffstat (limited to 'crates/ide/src/syntax_highlighting')
-rw-r--r-- | crates/ide/src/syntax_highlighting/html.rs | 97 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/injection.rs | 187 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/tags.rs | 203 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/tests.rs | 445 |
4 files changed, 932 insertions, 0 deletions
diff --git a/crates/ide/src/syntax_highlighting/html.rs b/crates/ide/src/syntax_highlighting/html.rs new file mode 100644 index 000000000..249368ff8 --- /dev/null +++ b/crates/ide/src/syntax_highlighting/html.rs | |||
@@ -0,0 +1,97 @@ | |||
1 | //! Renders a bit of code as HTML. | ||
2 | |||
3 | use base_db::SourceDatabase; | ||
4 | use oorandom::Rand32; | ||
5 | use syntax::{AstNode, TextRange, TextSize}; | ||
6 | |||
7 | use crate::{syntax_highlighting::highlight, FileId, RootDatabase}; | ||
8 | |||
9 | pub(crate) fn highlight_as_html(db: &RootDatabase, file_id: FileId, rainbow: bool) -> String { | ||
10 | let parse = db.parse(file_id); | ||
11 | |||
12 | fn rainbowify(seed: u64) -> String { | ||
13 | let mut rng = Rand32::new(seed); | ||
14 | format!( | ||
15 | "hsl({h},{s}%,{l}%)", | ||
16 | h = rng.rand_range(0..361), | ||
17 | s = rng.rand_range(42..99), | ||
18 | l = rng.rand_range(40..91), | ||
19 | ) | ||
20 | } | ||
21 | |||
22 | let ranges = highlight(db, file_id, None, false); | ||
23 | let text = parse.tree().syntax().to_string(); | ||
24 | let mut prev_pos = TextSize::from(0); | ||
25 | let mut buf = String::new(); | ||
26 | buf.push_str(&STYLE); | ||
27 | buf.push_str("<pre><code>"); | ||
28 | for range in &ranges { | ||
29 | if range.range.start() > prev_pos { | ||
30 | let curr = &text[TextRange::new(prev_pos, range.range.start())]; | ||
31 | let text = html_escape(curr); | ||
32 | buf.push_str(&text); | ||
33 | } | ||
34 | let curr = &text[TextRange::new(range.range.start(), range.range.end())]; | ||
35 | |||
36 | let class = range.highlight.to_string().replace('.', " "); | ||
37 | let color = match (rainbow, range.binding_hash) { | ||
38 | (true, Some(hash)) => { | ||
39 | format!(" data-binding-hash=\"{}\" style=\"color: {};\"", hash, rainbowify(hash)) | ||
40 | } | ||
41 | _ => "".into(), | ||
42 | }; | ||
43 | buf.push_str(&format!("<span class=\"{}\"{}>{}</span>", class, color, html_escape(curr))); | ||
44 | |||
45 | prev_pos = range.range.end(); | ||
46 | } | ||
47 | // Add the remaining (non-highlighted) text | ||
48 | let curr = &text[TextRange::new(prev_pos, TextSize::of(&text))]; | ||
49 | let text = html_escape(curr); | ||
50 | buf.push_str(&text); | ||
51 | buf.push_str("</code></pre>"); | ||
52 | buf | ||
53 | } | ||
54 | |||
55 | //FIXME: like, real html escaping | ||
56 | fn html_escape(text: &str) -> String { | ||
57 | text.replace("<", "<").replace(">", ">") | ||
58 | } | ||
59 | |||
60 | const STYLE: &str = " | ||
61 | <style> | ||
62 | body { margin: 0; } | ||
63 | pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; } | ||
64 | |||
65 | .lifetime { color: #DFAF8F; font-style: italic; } | ||
66 | .comment { color: #7F9F7F; } | ||
67 | .documentation { color: #629755; } | ||
68 | .injected { opacity: 0.65 ; } | ||
69 | .struct, .enum { color: #7CB8BB; } | ||
70 | .enum_variant { color: #BDE0F3; } | ||
71 | .string_literal { color: #CC9393; } | ||
72 | .field { color: #94BFF3; } | ||
73 | .function { color: #93E0E3; } | ||
74 | .function.unsafe { color: #BC8383; } | ||
75 | .operator.unsafe { color: #BC8383; } | ||
76 | .parameter { color: #94BFF3; } | ||
77 | .text { color: #DCDCCC; } | ||
78 | .type { color: #7CB8BB; } | ||
79 | .builtin_type { color: #8CD0D3; } | ||
80 | .type_param { color: #DFAF8F; } | ||
81 | .attribute { color: #94BFF3; } | ||
82 | .numeric_literal { color: #BFEBBF; } | ||
83 | .bool_literal { color: #BFE6EB; } | ||
84 | .macro { color: #94BFF3; } | ||
85 | .module { color: #AFD8AF; } | ||
86 | .value_param { color: #DCDCCC; } | ||
87 | .variable { color: #DCDCCC; } | ||
88 | .format_specifier { color: #CC696B; } | ||
89 | .mutable { text-decoration: underline; } | ||
90 | .escape_sequence { color: #94BFF3; } | ||
91 | .keyword { color: #F0DFAF; font-weight: bold; } | ||
92 | .keyword.unsafe { color: #BC8383; font-weight: bold; } | ||
93 | .control { font-style: italic; } | ||
94 | |||
95 | .unresolved_reference { color: #FC5555; text-decoration: wavy underline; } | ||
96 | </style> | ||
97 | "; | ||
diff --git a/crates/ide/src/syntax_highlighting/injection.rs b/crates/ide/src/syntax_highlighting/injection.rs new file mode 100644 index 000000000..43f4e6fea --- /dev/null +++ b/crates/ide/src/syntax_highlighting/injection.rs | |||
@@ -0,0 +1,187 @@ | |||
1 | //! Syntax highlighting injections such as highlighting of documentation tests. | ||
2 | |||
3 | use std::{collections::BTreeMap, convert::TryFrom}; | ||
4 | |||
5 | use ast::{HasQuotes, HasStringValue}; | ||
6 | use hir::Semantics; | ||
7 | use itertools::Itertools; | ||
8 | use syntax::{ast, AstToken, SyntaxNode, SyntaxToken, TextRange, TextSize}; | ||
9 | |||
10 | use crate::{ | ||
11 | call_info::ActiveParameter, Analysis, Highlight, HighlightModifier, HighlightTag, | ||
12 | HighlightedRange, RootDatabase, | ||
13 | }; | ||
14 | |||
15 | use super::HighlightedRangeStack; | ||
16 | |||
17 | pub(super) fn highlight_injection( | ||
18 | acc: &mut HighlightedRangeStack, | ||
19 | sema: &Semantics<RootDatabase>, | ||
20 | literal: ast::RawString, | ||
21 | expanded: SyntaxToken, | ||
22 | ) -> Option<()> { | ||
23 | let active_parameter = ActiveParameter::at_token(&sema, expanded)?; | ||
24 | if !active_parameter.name.starts_with("ra_fixture") { | ||
25 | return None; | ||
26 | } | ||
27 | let value = literal.value()?; | ||
28 | let (analysis, tmp_file_id) = Analysis::from_single_file(value.into_owned()); | ||
29 | |||
30 | if let Some(range) = literal.open_quote_text_range() { | ||
31 | acc.add(HighlightedRange { | ||
32 | range, | ||
33 | highlight: HighlightTag::StringLiteral.into(), | ||
34 | binding_hash: None, | ||
35 | }) | ||
36 | } | ||
37 | |||
38 | for mut h in analysis.highlight(tmp_file_id).unwrap() { | ||
39 | if let Some(r) = literal.map_range_up(h.range) { | ||
40 | h.range = r; | ||
41 | acc.add(h) | ||
42 | } | ||
43 | } | ||
44 | |||
45 | if let Some(range) = literal.close_quote_text_range() { | ||
46 | acc.add(HighlightedRange { | ||
47 | range, | ||
48 | highlight: HighlightTag::StringLiteral.into(), | ||
49 | binding_hash: None, | ||
50 | }) | ||
51 | } | ||
52 | |||
53 | Some(()) | ||
54 | } | ||
55 | |||
56 | /// Mapping from extracted documentation code to original code | ||
57 | type RangesMap = BTreeMap<TextSize, TextSize>; | ||
58 | |||
59 | const RUSTDOC_FENCE: &'static str = "```"; | ||
60 | const RUSTDOC_FENCE_TOKENS: &[&'static str] = | ||
61 | &["", "rust", "should_panic", "ignore", "no_run", "compile_fail", "edition2015", "edition2018"]; | ||
62 | |||
63 | /// Extracts Rust code from documentation comments as well as a mapping from | ||
64 | /// the extracted source code back to the original source ranges. | ||
65 | /// Lastly, a vector of new comment highlight ranges (spanning only the | ||
66 | /// comment prefix) is returned which is used in the syntax highlighting | ||
67 | /// injection to replace the previous (line-spanning) comment ranges. | ||
68 | pub(super) fn extract_doc_comments( | ||
69 | node: &SyntaxNode, | ||
70 | ) -> Option<(String, RangesMap, Vec<HighlightedRange>)> { | ||
71 | // wrap the doctest into function body to get correct syntax highlighting | ||
72 | let prefix = "fn doctest() {\n"; | ||
73 | let suffix = "}\n"; | ||
74 | // Mapping from extracted documentation code to original code | ||
75 | let mut range_mapping: RangesMap = BTreeMap::new(); | ||
76 | let mut line_start = TextSize::try_from(prefix.len()).unwrap(); | ||
77 | let mut is_codeblock = false; | ||
78 | let mut is_doctest = false; | ||
79 | // Replace the original, line-spanning comment ranges by new, only comment-prefix | ||
80 | // spanning comment ranges. | ||
81 | let mut new_comments = Vec::new(); | ||
82 | let doctest = node | ||
83 | .children_with_tokens() | ||
84 | .filter_map(|el| el.into_token().and_then(ast::Comment::cast)) | ||
85 | .filter(|comment| comment.kind().doc.is_some()) | ||
86 | .filter(|comment| { | ||
87 | if let Some(idx) = comment.text().find(RUSTDOC_FENCE) { | ||
88 | is_codeblock = !is_codeblock; | ||
89 | // Check whether code is rust by inspecting fence guards | ||
90 | let guards = &comment.text()[idx + RUSTDOC_FENCE.len()..]; | ||
91 | let is_rust = | ||
92 | guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim())); | ||
93 | is_doctest = is_codeblock && is_rust; | ||
94 | false | ||
95 | } else { | ||
96 | is_doctest | ||
97 | } | ||
98 | }) | ||
99 | .map(|comment| { | ||
100 | let prefix_len = comment.prefix().len(); | ||
101 | let line: &str = comment.text().as_str(); | ||
102 | let range = comment.syntax().text_range(); | ||
103 | |||
104 | // whitespace after comment is ignored | ||
105 | let pos = if let Some(ws) = line.chars().nth(prefix_len).filter(|c| c.is_whitespace()) { | ||
106 | prefix_len + ws.len_utf8() | ||
107 | } else { | ||
108 | prefix_len | ||
109 | }; | ||
110 | |||
111 | // lines marked with `#` should be ignored in output, we skip the `#` char | ||
112 | let pos = if let Some(ws) = line.chars().nth(pos).filter(|&c| c == '#') { | ||
113 | pos + ws.len_utf8() | ||
114 | } else { | ||
115 | pos | ||
116 | }; | ||
117 | |||
118 | range_mapping.insert(line_start, range.start() + TextSize::try_from(pos).unwrap()); | ||
119 | new_comments.push(HighlightedRange { | ||
120 | range: TextRange::new( | ||
121 | range.start(), | ||
122 | range.start() + TextSize::try_from(pos).unwrap(), | ||
123 | ), | ||
124 | highlight: HighlightTag::Comment | HighlightModifier::Documentation, | ||
125 | binding_hash: None, | ||
126 | }); | ||
127 | line_start += range.len() - TextSize::try_from(pos).unwrap(); | ||
128 | line_start += TextSize::try_from('\n'.len_utf8()).unwrap(); | ||
129 | |||
130 | line[pos..].to_owned() | ||
131 | }) | ||
132 | .join("\n"); | ||
133 | |||
134 | if doctest.is_empty() { | ||
135 | return None; | ||
136 | } | ||
137 | |||
138 | let doctest = format!("{}{}{}", prefix, doctest, suffix); | ||
139 | Some((doctest, range_mapping, new_comments)) | ||
140 | } | ||
141 | |||
142 | /// Injection of syntax highlighting of doctests. | ||
143 | pub(super) fn highlight_doc_comment( | ||
144 | text: String, | ||
145 | range_mapping: RangesMap, | ||
146 | new_comments: Vec<HighlightedRange>, | ||
147 | stack: &mut HighlightedRangeStack, | ||
148 | ) { | ||
149 | let (analysis, tmp_file_id) = Analysis::from_single_file(text); | ||
150 | |||
151 | stack.push(); | ||
152 | for mut h in analysis.with_db(|db| super::highlight(db, tmp_file_id, None, true)).unwrap() { | ||
153 | // Determine start offset and end offset in case of multi-line ranges | ||
154 | let mut start_offset = None; | ||
155 | let mut end_offset = None; | ||
156 | for (line_start, orig_line_start) in range_mapping.range(..h.range.end()).rev() { | ||
157 | // It's possible for orig_line_start - line_start to be negative. Add h.range.start() | ||
158 | // here and remove it from the end range after the loop below so that the values are | ||
159 | // always non-negative. | ||
160 | let offset = h.range.start() + orig_line_start - line_start; | ||
161 | if line_start <= &h.range.start() { | ||
162 | start_offset.get_or_insert(offset); | ||
163 | break; | ||
164 | } else { | ||
165 | end_offset.get_or_insert(offset); | ||
166 | } | ||
167 | } | ||
168 | if let Some(start_offset) = start_offset { | ||
169 | h.range = TextRange::new( | ||
170 | start_offset, | ||
171 | h.range.end() + end_offset.unwrap_or(start_offset) - h.range.start(), | ||
172 | ); | ||
173 | |||
174 | h.highlight |= HighlightModifier::Injected; | ||
175 | stack.add(h); | ||
176 | } | ||
177 | } | ||
178 | |||
179 | // Inject the comment prefix highlight ranges | ||
180 | stack.push(); | ||
181 | for comment in new_comments { | ||
182 | stack.add(comment); | ||
183 | } | ||
184 | stack.pop_and_inject(None); | ||
185 | stack | ||
186 | .pop_and_inject(Some(Highlight::from(HighlightTag::Generic) | HighlightModifier::Injected)); | ||
187 | } | ||
diff --git a/crates/ide/src/syntax_highlighting/tags.rs b/crates/ide/src/syntax_highlighting/tags.rs new file mode 100644 index 000000000..49ec94bdc --- /dev/null +++ b/crates/ide/src/syntax_highlighting/tags.rs | |||
@@ -0,0 +1,203 @@ | |||
1 | //! Defines token tags we use for syntax highlighting. | ||
2 | //! A tag is not unlike a CSS class. | ||
3 | |||
4 | use std::{fmt, ops}; | ||
5 | |||
6 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
7 | pub struct Highlight { | ||
8 | pub tag: HighlightTag, | ||
9 | pub modifiers: HighlightModifiers, | ||
10 | } | ||
11 | |||
12 | #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
13 | pub struct HighlightModifiers(u32); | ||
14 | |||
15 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
16 | pub enum HighlightTag { | ||
17 | Attribute, | ||
18 | BoolLiteral, | ||
19 | BuiltinType, | ||
20 | ByteLiteral, | ||
21 | CharLiteral, | ||
22 | Comment, | ||
23 | Constant, | ||
24 | Enum, | ||
25 | EnumVariant, | ||
26 | EscapeSequence, | ||
27 | Field, | ||
28 | Function, | ||
29 | Generic, | ||
30 | Keyword, | ||
31 | Lifetime, | ||
32 | Macro, | ||
33 | Module, | ||
34 | NumericLiteral, | ||
35 | Punctuation, | ||
36 | SelfKeyword, | ||
37 | SelfType, | ||
38 | Static, | ||
39 | StringLiteral, | ||
40 | Struct, | ||
41 | Trait, | ||
42 | TypeAlias, | ||
43 | TypeParam, | ||
44 | Union, | ||
45 | ValueParam, | ||
46 | Local, | ||
47 | UnresolvedReference, | ||
48 | FormatSpecifier, | ||
49 | Operator, | ||
50 | } | ||
51 | |||
52 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] | ||
53 | #[repr(u8)] | ||
54 | pub enum HighlightModifier { | ||
55 | /// Used to differentiate individual elements within attributes. | ||
56 | Attribute = 0, | ||
57 | /// Used with keywords like `if` and `break`. | ||
58 | ControlFlow, | ||
59 | /// `foo` in `fn foo(x: i32)` is a definition, `foo` in `foo(90 + 2)` is | ||
60 | /// not. | ||
61 | Definition, | ||
62 | Documentation, | ||
63 | Injected, | ||
64 | Mutable, | ||
65 | Unsafe, | ||
66 | } | ||
67 | |||
68 | impl HighlightTag { | ||
69 | fn as_str(self) -> &'static str { | ||
70 | match self { | ||
71 | HighlightTag::Attribute => "attribute", | ||
72 | HighlightTag::BoolLiteral => "bool_literal", | ||
73 | HighlightTag::BuiltinType => "builtin_type", | ||
74 | HighlightTag::ByteLiteral => "byte_literal", | ||
75 | HighlightTag::CharLiteral => "char_literal", | ||
76 | HighlightTag::Comment => "comment", | ||
77 | HighlightTag::Constant => "constant", | ||
78 | HighlightTag::Enum => "enum", | ||
79 | HighlightTag::EnumVariant => "enum_variant", | ||
80 | HighlightTag::EscapeSequence => "escape_sequence", | ||
81 | HighlightTag::Field => "field", | ||
82 | HighlightTag::FormatSpecifier => "format_specifier", | ||
83 | HighlightTag::Function => "function", | ||
84 | HighlightTag::Generic => "generic", | ||
85 | HighlightTag::Keyword => "keyword", | ||
86 | HighlightTag::Lifetime => "lifetime", | ||
87 | HighlightTag::Punctuation => "punctuation", | ||
88 | HighlightTag::Macro => "macro", | ||
89 | HighlightTag::Module => "module", | ||
90 | HighlightTag::NumericLiteral => "numeric_literal", | ||
91 | HighlightTag::Operator => "operator", | ||
92 | HighlightTag::SelfKeyword => "self_keyword", | ||
93 | HighlightTag::SelfType => "self_type", | ||
94 | HighlightTag::Static => "static", | ||
95 | HighlightTag::StringLiteral => "string_literal", | ||
96 | HighlightTag::Struct => "struct", | ||
97 | HighlightTag::Trait => "trait", | ||
98 | HighlightTag::TypeAlias => "type_alias", | ||
99 | HighlightTag::TypeParam => "type_param", | ||
100 | HighlightTag::Union => "union", | ||
101 | HighlightTag::ValueParam => "value_param", | ||
102 | HighlightTag::Local => "variable", | ||
103 | HighlightTag::UnresolvedReference => "unresolved_reference", | ||
104 | } | ||
105 | } | ||
106 | } | ||
107 | |||
108 | impl fmt::Display for HighlightTag { | ||
109 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
110 | fmt::Display::fmt(self.as_str(), f) | ||
111 | } | ||
112 | } | ||
113 | |||
114 | impl HighlightModifier { | ||
115 | const ALL: &'static [HighlightModifier] = &[ | ||
116 | HighlightModifier::Attribute, | ||
117 | HighlightModifier::ControlFlow, | ||
118 | HighlightModifier::Definition, | ||
119 | HighlightModifier::Documentation, | ||
120 | HighlightModifier::Injected, | ||
121 | HighlightModifier::Mutable, | ||
122 | HighlightModifier::Unsafe, | ||
123 | ]; | ||
124 | |||
125 | fn as_str(self) -> &'static str { | ||
126 | match self { | ||
127 | HighlightModifier::Attribute => "attribute", | ||
128 | HighlightModifier::ControlFlow => "control", | ||
129 | HighlightModifier::Definition => "declaration", | ||
130 | HighlightModifier::Documentation => "documentation", | ||
131 | HighlightModifier::Injected => "injected", | ||
132 | HighlightModifier::Mutable => "mutable", | ||
133 | HighlightModifier::Unsafe => "unsafe", | ||
134 | } | ||
135 | } | ||
136 | |||
137 | fn mask(self) -> u32 { | ||
138 | 1 << (self as u32) | ||
139 | } | ||
140 | } | ||
141 | |||
142 | impl fmt::Display for HighlightModifier { | ||
143 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
144 | fmt::Display::fmt(self.as_str(), f) | ||
145 | } | ||
146 | } | ||
147 | |||
148 | impl fmt::Display for Highlight { | ||
149 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
150 | write!(f, "{}", self.tag)?; | ||
151 | for modifier in self.modifiers.iter() { | ||
152 | write!(f, ".{}", modifier)? | ||
153 | } | ||
154 | Ok(()) | ||
155 | } | ||
156 | } | ||
157 | |||
158 | impl From<HighlightTag> for Highlight { | ||
159 | fn from(tag: HighlightTag) -> Highlight { | ||
160 | Highlight::new(tag) | ||
161 | } | ||
162 | } | ||
163 | |||
164 | impl Highlight { | ||
165 | pub(crate) fn new(tag: HighlightTag) -> Highlight { | ||
166 | Highlight { tag, modifiers: HighlightModifiers::default() } | ||
167 | } | ||
168 | } | ||
169 | |||
170 | impl ops::BitOr<HighlightModifier> for HighlightTag { | ||
171 | type Output = Highlight; | ||
172 | |||
173 | fn bitor(self, rhs: HighlightModifier) -> Highlight { | ||
174 | Highlight::new(self) | rhs | ||
175 | } | ||
176 | } | ||
177 | |||
178 | impl ops::BitOrAssign<HighlightModifier> for HighlightModifiers { | ||
179 | fn bitor_assign(&mut self, rhs: HighlightModifier) { | ||
180 | self.0 |= rhs.mask(); | ||
181 | } | ||
182 | } | ||
183 | |||
184 | impl ops::BitOrAssign<HighlightModifier> for Highlight { | ||
185 | fn bitor_assign(&mut self, rhs: HighlightModifier) { | ||
186 | self.modifiers |= rhs; | ||
187 | } | ||
188 | } | ||
189 | |||
190 | impl ops::BitOr<HighlightModifier> for Highlight { | ||
191 | type Output = Highlight; | ||
192 | |||
193 | fn bitor(mut self, rhs: HighlightModifier) -> Highlight { | ||
194 | self |= rhs; | ||
195 | self | ||
196 | } | ||
197 | } | ||
198 | |||
199 | impl HighlightModifiers { | ||
200 | pub fn iter(self) -> impl Iterator<Item = HighlightModifier> { | ||
201 | HighlightModifier::ALL.iter().copied().filter(move |it| self.0 & it.mask() == it.mask()) | ||
202 | } | ||
203 | } | ||
diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs new file mode 100644 index 000000000..94f37d773 --- /dev/null +++ b/crates/ide/src/syntax_highlighting/tests.rs | |||
@@ -0,0 +1,445 @@ | |||
1 | use std::fs; | ||
2 | |||
3 | use expect::{expect_file, ExpectFile}; | ||
4 | use test_utils::project_dir; | ||
5 | |||
6 | use crate::{mock_analysis::single_file, FileRange, TextRange}; | ||
7 | |||
8 | #[test] | ||
9 | fn test_highlighting() { | ||
10 | check_highlighting( | ||
11 | r#" | ||
12 | use inner::{self as inner_mod}; | ||
13 | mod inner {} | ||
14 | |||
15 | #[derive(Clone, Debug)] | ||
16 | struct Foo { | ||
17 | pub x: i32, | ||
18 | pub y: i32, | ||
19 | } | ||
20 | |||
21 | trait Bar { | ||
22 | fn bar(&self) -> i32; | ||
23 | } | ||
24 | |||
25 | impl Bar for Foo { | ||
26 | fn bar(&self) -> i32 { | ||
27 | self.x | ||
28 | } | ||
29 | } | ||
30 | |||
31 | impl Foo { | ||
32 | fn baz(mut self) -> i32 { | ||
33 | self.x | ||
34 | } | ||
35 | |||
36 | fn qux(&mut self) { | ||
37 | self.x = 0; | ||
38 | } | ||
39 | } | ||
40 | |||
41 | static mut STATIC_MUT: i32 = 0; | ||
42 | |||
43 | fn foo<'a, T>() -> T { | ||
44 | foo::<'a, i32>() | ||
45 | } | ||
46 | |||
47 | macro_rules! def_fn { | ||
48 | ($($tt:tt)*) => {$($tt)*} | ||
49 | } | ||
50 | |||
51 | def_fn! { | ||
52 | fn bar() -> u32 { | ||
53 | 100 | ||
54 | } | ||
55 | } | ||
56 | |||
57 | macro_rules! noop { | ||
58 | ($expr:expr) => { | ||
59 | $expr | ||
60 | } | ||
61 | } | ||
62 | |||
63 | // comment | ||
64 | fn main() { | ||
65 | println!("Hello, {}!", 92); | ||
66 | |||
67 | let mut vec = Vec::new(); | ||
68 | if true { | ||
69 | let x = 92; | ||
70 | vec.push(Foo { x, y: 1 }); | ||
71 | } | ||
72 | unsafe { | ||
73 | vec.set_len(0); | ||
74 | STATIC_MUT = 1; | ||
75 | } | ||
76 | |||
77 | for e in vec { | ||
78 | // Do nothing | ||
79 | } | ||
80 | |||
81 | noop!(noop!(1)); | ||
82 | |||
83 | let mut x = 42; | ||
84 | let y = &mut x; | ||
85 | let z = &y; | ||
86 | |||
87 | let Foo { x: z, y } = Foo { x: z, y }; | ||
88 | |||
89 | y; | ||
90 | } | ||
91 | |||
92 | enum Option<T> { | ||
93 | Some(T), | ||
94 | None, | ||
95 | } | ||
96 | use Option::*; | ||
97 | |||
98 | impl<T> Option<T> { | ||
99 | fn and<U>(self, other: Option<U>) -> Option<(T, U)> { | ||
100 | match other { | ||
101 | None => unimplemented!(), | ||
102 | Nope => Nope, | ||
103 | } | ||
104 | } | ||
105 | } | ||
106 | "# | ||
107 | .trim(), | ||
108 | expect_file!["crates/ide/test_data/highlighting.html"], | ||
109 | false, | ||
110 | ); | ||
111 | } | ||
112 | |||
113 | #[test] | ||
114 | fn test_rainbow_highlighting() { | ||
115 | check_highlighting( | ||
116 | r#" | ||
117 | fn main() { | ||
118 | let hello = "hello"; | ||
119 | let x = hello.to_string(); | ||
120 | let y = hello.to_string(); | ||
121 | |||
122 | let x = "other color please!"; | ||
123 | let y = x.to_string(); | ||
124 | } | ||
125 | |||
126 | fn bar() { | ||
127 | let mut hello = "hello"; | ||
128 | } | ||
129 | "# | ||
130 | .trim(), | ||
131 | expect_file!["crates/ide/test_data/rainbow_highlighting.html"], | ||
132 | true, | ||
133 | ); | ||
134 | } | ||
135 | |||
136 | #[test] | ||
137 | fn accidentally_quadratic() { | ||
138 | let file = project_dir().join("crates/syntax/test_data/accidentally_quadratic"); | ||
139 | let src = fs::read_to_string(file).unwrap(); | ||
140 | |||
141 | let (analysis, file_id) = single_file(&src); | ||
142 | |||
143 | // let t = std::time::Instant::now(); | ||
144 | let _ = analysis.highlight(file_id).unwrap(); | ||
145 | // eprintln!("elapsed: {:?}", t.elapsed()); | ||
146 | } | ||
147 | |||
148 | #[test] | ||
149 | fn test_ranges() { | ||
150 | let (analysis, file_id) = single_file( | ||
151 | r#" | ||
152 | #[derive(Clone, Debug)] | ||
153 | struct Foo { | ||
154 | pub x: i32, | ||
155 | pub y: i32, | ||
156 | } | ||
157 | "#, | ||
158 | ); | ||
159 | |||
160 | // The "x" | ||
161 | let highlights = &analysis | ||
162 | .highlight_range(FileRange { file_id, range: TextRange::at(45.into(), 1.into()) }) | ||
163 | .unwrap(); | ||
164 | |||
165 | assert_eq!(&highlights[0].highlight.to_string(), "field.declaration"); | ||
166 | } | ||
167 | |||
168 | #[test] | ||
169 | fn test_flattening() { | ||
170 | check_highlighting( | ||
171 | r##" | ||
172 | fn fixture(ra_fixture: &str) {} | ||
173 | |||
174 | fn main() { | ||
175 | fixture(r#" | ||
176 | trait Foo { | ||
177 | fn foo() { | ||
178 | println!("2 + 2 = {}", 4); | ||
179 | } | ||
180 | }"# | ||
181 | ); | ||
182 | }"## | ||
183 | .trim(), | ||
184 | expect_file!["crates/ide/test_data/highlight_injection.html"], | ||
185 | false, | ||
186 | ); | ||
187 | } | ||
188 | |||
189 | #[test] | ||
190 | fn ranges_sorted() { | ||
191 | let (analysis, file_id) = single_file( | ||
192 | r#" | ||
193 | #[foo(bar = "bar")] | ||
194 | macro_rules! test {} | ||
195 | }"# | ||
196 | .trim(), | ||
197 | ); | ||
198 | let _ = analysis.highlight(file_id).unwrap(); | ||
199 | } | ||
200 | |||
201 | #[test] | ||
202 | fn test_string_highlighting() { | ||
203 | // The format string detection is based on macro-expansion, | ||
204 | // thus, we have to copy the macro definition from `std` | ||
205 | check_highlighting( | ||
206 | r#" | ||
207 | macro_rules! println { | ||
208 | ($($arg:tt)*) => ({ | ||
209 | $crate::io::_print($crate::format_args_nl!($($arg)*)); | ||
210 | }) | ||
211 | } | ||
212 | #[rustc_builtin_macro] | ||
213 | macro_rules! format_args_nl { | ||
214 | ($fmt:expr) => {{ /* compiler built-in */ }}; | ||
215 | ($fmt:expr, $($args:tt)*) => {{ /* compiler built-in */ }}; | ||
216 | } | ||
217 | |||
218 | fn main() { | ||
219 | // from https://doc.rust-lang.org/std/fmt/index.html | ||
220 | println!("Hello"); // => "Hello" | ||
221 | println!("Hello, {}!", "world"); // => "Hello, world!" | ||
222 | println!("The number is {}", 1); // => "The number is 1" | ||
223 | println!("{:?}", (3, 4)); // => "(3, 4)" | ||
224 | println!("{value}", value=4); // => "4" | ||
225 | println!("{} {}", 1, 2); // => "1 2" | ||
226 | println!("{:04}", 42); // => "0042" with leading zerosV | ||
227 | println!("{1} {} {0} {}", 1, 2); // => "2 1 1 2" | ||
228 | println!("{argument}", argument = "test"); // => "test" | ||
229 | println!("{name} {}", 1, name = 2); // => "2 1" | ||
230 | println!("{a} {c} {b}", a="a", b='b', c=3); // => "a 3 b" | ||
231 | println!("{{{}}}", 2); // => "{2}" | ||
232 | println!("Hello {:5}!", "x"); | ||
233 | println!("Hello {:1$}!", "x", 5); | ||
234 | println!("Hello {1:0$}!", 5, "x"); | ||
235 | println!("Hello {:width$}!", "x", width = 5); | ||
236 | println!("Hello {:<5}!", "x"); | ||
237 | println!("Hello {:-<5}!", "x"); | ||
238 | println!("Hello {:^5}!", "x"); | ||
239 | println!("Hello {:>5}!", "x"); | ||
240 | println!("Hello {:+}!", 5); | ||
241 | println!("{:#x}!", 27); | ||
242 | println!("Hello {:05}!", 5); | ||
243 | println!("Hello {:05}!", -5); | ||
244 | println!("{:#010x}!", 27); | ||
245 | println!("Hello {0} is {1:.5}", "x", 0.01); | ||
246 | println!("Hello {1} is {2:.0$}", 5, "x", 0.01); | ||
247 | println!("Hello {0} is {2:.1$}", "x", 5, 0.01); | ||
248 | println!("Hello {} is {:.*}", "x", 5, 0.01); | ||
249 | println!("Hello {} is {2:.*}", "x", 5, 0.01); | ||
250 | println!("Hello {} is {number:.prec$}", "x", prec = 5, number = 0.01); | ||
251 | println!("{}, `{name:.*}` has 3 fractional digits", "Hello", 3, name=1234.56); | ||
252 | println!("{}, `{name:.*}` has 3 characters", "Hello", 3, name="1234.56"); | ||
253 | println!("{}, `{name:>8.*}` has 3 right-aligned characters", "Hello", 3, name="1234.56"); | ||
254 | println!("Hello {{}}"); | ||
255 | println!("{{ Hello"); | ||
256 | |||
257 | println!(r"Hello, {}!", "world"); | ||
258 | |||
259 | // escape sequences | ||
260 | println!("Hello\nWorld"); | ||
261 | println!("\u{48}\x65\x6C\x6C\x6F World"); | ||
262 | |||
263 | println!("{\x41}", A = 92); | ||
264 | println!("{ничоси}", ничоси = 92); | ||
265 | }"# | ||
266 | .trim(), | ||
267 | expect_file!["crates/ide/test_data/highlight_strings.html"], | ||
268 | false, | ||
269 | ); | ||
270 | } | ||
271 | |||
272 | #[test] | ||
273 | fn test_unsafe_highlighting() { | ||
274 | check_highlighting( | ||
275 | r#" | ||
276 | unsafe fn unsafe_fn() {} | ||
277 | |||
278 | union Union { | ||
279 | a: u32, | ||
280 | b: f32, | ||
281 | } | ||
282 | |||
283 | struct HasUnsafeFn; | ||
284 | |||
285 | impl HasUnsafeFn { | ||
286 | unsafe fn unsafe_method(&self) {} | ||
287 | } | ||
288 | |||
289 | struct TypeForStaticMut { | ||
290 | a: u8 | ||
291 | } | ||
292 | |||
293 | static mut global_mut: TypeForStaticMut = TypeForStaticMut { a: 0 }; | ||
294 | |||
295 | #[repr(packed)] | ||
296 | struct Packed { | ||
297 | a: u16, | ||
298 | } | ||
299 | |||
300 | trait DoTheAutoref { | ||
301 | fn calls_autoref(&self); | ||
302 | } | ||
303 | |||
304 | impl DoTheAutoref for u16 { | ||
305 | fn calls_autoref(&self) {} | ||
306 | } | ||
307 | |||
308 | fn main() { | ||
309 | let x = &5 as *const _ as *const usize; | ||
310 | let u = Union { b: 0 }; | ||
311 | unsafe { | ||
312 | // unsafe fn and method calls | ||
313 | unsafe_fn(); | ||
314 | let b = u.b; | ||
315 | match u { | ||
316 | Union { b: 0 } => (), | ||
317 | Union { a } => (), | ||
318 | } | ||
319 | HasUnsafeFn.unsafe_method(); | ||
320 | |||
321 | // unsafe deref | ||
322 | let y = *x; | ||
323 | |||
324 | // unsafe access to a static mut | ||
325 | let a = global_mut.a; | ||
326 | |||
327 | // unsafe ref of packed fields | ||
328 | let packed = Packed { a: 0 }; | ||
329 | let a = &packed.a; | ||
330 | let ref a = packed.a; | ||
331 | let Packed { ref a } = packed; | ||
332 | let Packed { a: ref _a } = packed; | ||
333 | |||
334 | // unsafe auto ref of packed field | ||
335 | packed.a.calls_autoref(); | ||
336 | } | ||
337 | } | ||
338 | "# | ||
339 | .trim(), | ||
340 | expect_file!["crates/ide/test_data/highlight_unsafe.html"], | ||
341 | false, | ||
342 | ); | ||
343 | } | ||
344 | |||
345 | #[test] | ||
346 | fn test_highlight_doctest() { | ||
347 | check_highlighting( | ||
348 | r#" | ||
349 | /// ``` | ||
350 | /// let _ = "early doctests should not go boom"; | ||
351 | /// ``` | ||
352 | struct Foo { | ||
353 | bar: bool, | ||
354 | } | ||
355 | |||
356 | impl Foo { | ||
357 | pub const bar: bool = true; | ||
358 | |||
359 | /// Constructs a new `Foo`. | ||
360 | /// | ||
361 | /// # Examples | ||
362 | /// | ||
363 | /// ``` | ||
364 | /// # #![allow(unused_mut)] | ||
365 | /// let mut foo: Foo = Foo::new(); | ||
366 | /// ``` | ||
367 | pub const fn new() -> Foo { | ||
368 | Foo { bar: true } | ||
369 | } | ||
370 | |||
371 | /// `bar` method on `Foo`. | ||
372 | /// | ||
373 | /// # Examples | ||
374 | /// | ||
375 | /// ``` | ||
376 | /// use x::y; | ||
377 | /// | ||
378 | /// let foo = Foo::new(); | ||
379 | /// | ||
380 | /// // calls bar on foo | ||
381 | /// assert!(foo.bar()); | ||
382 | /// | ||
383 | /// let bar = foo.bar || Foo::bar; | ||
384 | /// | ||
385 | /// /* multi-line | ||
386 | /// comment */ | ||
387 | /// | ||
388 | /// let multi_line_string = "Foo | ||
389 | /// bar | ||
390 | /// "; | ||
391 | /// | ||
392 | /// ``` | ||
393 | /// | ||
394 | /// ```rust,no_run | ||
395 | /// let foobar = Foo::new().bar(); | ||
396 | /// ``` | ||
397 | /// | ||
398 | /// ```sh | ||
399 | /// echo 1 | ||
400 | /// ``` | ||
401 | pub fn foo(&self) -> bool { | ||
402 | true | ||
403 | } | ||
404 | } | ||
405 | |||
406 | /// ``` | ||
407 | /// noop!(1); | ||
408 | /// ``` | ||
409 | macro_rules! noop { | ||
410 | ($expr:expr) => { | ||
411 | $expr | ||
412 | } | ||
413 | } | ||
414 | "# | ||
415 | .trim(), | ||
416 | expect_file!["crates/ide/test_data/highlight_doctest.html"], | ||
417 | false, | ||
418 | ); | ||
419 | } | ||
420 | |||
421 | #[test] | ||
422 | fn test_extern_crate() { | ||
423 | check_highlighting( | ||
424 | r#" | ||
425 | //- /main.rs | ||
426 | extern crate std; | ||
427 | extern crate alloc as abc; | ||
428 | //- /std/lib.rs | ||
429 | pub struct S; | ||
430 | //- /alloc/lib.rs | ||
431 | pub struct A | ||
432 | "#, | ||
433 | expect_file!["crates/ide/test_data/highlight_extern_crate.html"], | ||
434 | false, | ||
435 | ); | ||
436 | } | ||
437 | |||
438 | /// Highlights the code given by the `ra_fixture` argument, renders the | ||
439 | /// result as HTML, and compares it with the HTML file given as `snapshot`. | ||
440 | /// Note that the `snapshot` file is overwritten by the rendered HTML. | ||
441 | fn check_highlighting(ra_fixture: &str, expect: ExpectFile, rainbow: bool) { | ||
442 | let (analysis, file_id) = single_file(ra_fixture); | ||
443 | let actual_html = &analysis.highlight_as_html(file_id, rainbow).unwrap(); | ||
444 | expect.assert_eq(actual_html) | ||
445 | } | ||