diff options
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 | 206 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/tests.rs | 484 |
4 files changed, 974 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..c1b817f06 --- /dev/null +++ b/crates/ide/src/syntax_highlighting/tags.rs | |||
@@ -0,0 +1,206 @@ | |||
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 | Consuming, | ||
66 | Unsafe, | ||
67 | } | ||
68 | |||
69 | impl HighlightTag { | ||
70 | fn as_str(self) -> &'static str { | ||
71 | match self { | ||
72 | HighlightTag::Attribute => "attribute", | ||
73 | HighlightTag::BoolLiteral => "bool_literal", | ||
74 | HighlightTag::BuiltinType => "builtin_type", | ||
75 | HighlightTag::ByteLiteral => "byte_literal", | ||
76 | HighlightTag::CharLiteral => "char_literal", | ||
77 | HighlightTag::Comment => "comment", | ||
78 | HighlightTag::Constant => "constant", | ||
79 | HighlightTag::Enum => "enum", | ||
80 | HighlightTag::EnumVariant => "enum_variant", | ||
81 | HighlightTag::EscapeSequence => "escape_sequence", | ||
82 | HighlightTag::Field => "field", | ||
83 | HighlightTag::FormatSpecifier => "format_specifier", | ||
84 | HighlightTag::Function => "function", | ||
85 | HighlightTag::Generic => "generic", | ||
86 | HighlightTag::Keyword => "keyword", | ||
87 | HighlightTag::Lifetime => "lifetime", | ||
88 | HighlightTag::Punctuation => "punctuation", | ||
89 | HighlightTag::Macro => "macro", | ||
90 | HighlightTag::Module => "module", | ||
91 | HighlightTag::NumericLiteral => "numeric_literal", | ||
92 | HighlightTag::Operator => "operator", | ||
93 | HighlightTag::SelfKeyword => "self_keyword", | ||
94 | HighlightTag::SelfType => "self_type", | ||
95 | HighlightTag::Static => "static", | ||
96 | HighlightTag::StringLiteral => "string_literal", | ||
97 | HighlightTag::Struct => "struct", | ||
98 | HighlightTag::Trait => "trait", | ||
99 | HighlightTag::TypeAlias => "type_alias", | ||
100 | HighlightTag::TypeParam => "type_param", | ||
101 | HighlightTag::Union => "union", | ||
102 | HighlightTag::ValueParam => "value_param", | ||
103 | HighlightTag::Local => "variable", | ||
104 | HighlightTag::UnresolvedReference => "unresolved_reference", | ||
105 | } | ||
106 | } | ||
107 | } | ||
108 | |||
109 | impl fmt::Display for HighlightTag { | ||
110 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
111 | fmt::Display::fmt(self.as_str(), f) | ||
112 | } | ||
113 | } | ||
114 | |||
115 | impl HighlightModifier { | ||
116 | const ALL: &'static [HighlightModifier] = &[ | ||
117 | HighlightModifier::Attribute, | ||
118 | HighlightModifier::ControlFlow, | ||
119 | HighlightModifier::Definition, | ||
120 | HighlightModifier::Documentation, | ||
121 | HighlightModifier::Injected, | ||
122 | HighlightModifier::Mutable, | ||
123 | HighlightModifier::Consuming, | ||
124 | HighlightModifier::Unsafe, | ||
125 | ]; | ||
126 | |||
127 | fn as_str(self) -> &'static str { | ||
128 | match self { | ||
129 | HighlightModifier::Attribute => "attribute", | ||
130 | HighlightModifier::ControlFlow => "control", | ||
131 | HighlightModifier::Definition => "declaration", | ||
132 | HighlightModifier::Documentation => "documentation", | ||
133 | HighlightModifier::Injected => "injected", | ||
134 | HighlightModifier::Mutable => "mutable", | ||
135 | HighlightModifier::Consuming => "consuming", | ||
136 | HighlightModifier::Unsafe => "unsafe", | ||
137 | } | ||
138 | } | ||
139 | |||
140 | fn mask(self) -> u32 { | ||
141 | 1 << (self as u32) | ||
142 | } | ||
143 | } | ||
144 | |||
145 | impl fmt::Display for HighlightModifier { | ||
146 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
147 | fmt::Display::fmt(self.as_str(), f) | ||
148 | } | ||
149 | } | ||
150 | |||
151 | impl fmt::Display for Highlight { | ||
152 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
153 | write!(f, "{}", self.tag)?; | ||
154 | for modifier in self.modifiers.iter() { | ||
155 | write!(f, ".{}", modifier)? | ||
156 | } | ||
157 | Ok(()) | ||
158 | } | ||
159 | } | ||
160 | |||
161 | impl From<HighlightTag> for Highlight { | ||
162 | fn from(tag: HighlightTag) -> Highlight { | ||
163 | Highlight::new(tag) | ||
164 | } | ||
165 | } | ||
166 | |||
167 | impl Highlight { | ||
168 | pub(crate) fn new(tag: HighlightTag) -> Highlight { | ||
169 | Highlight { tag, modifiers: HighlightModifiers::default() } | ||
170 | } | ||
171 | } | ||
172 | |||
173 | impl ops::BitOr<HighlightModifier> for HighlightTag { | ||
174 | type Output = Highlight; | ||
175 | |||
176 | fn bitor(self, rhs: HighlightModifier) -> Highlight { | ||
177 | Highlight::new(self) | rhs | ||
178 | } | ||
179 | } | ||
180 | |||
181 | impl ops::BitOrAssign<HighlightModifier> for HighlightModifiers { | ||
182 | fn bitor_assign(&mut self, rhs: HighlightModifier) { | ||
183 | self.0 |= rhs.mask(); | ||
184 | } | ||
185 | } | ||
186 | |||
187 | impl ops::BitOrAssign<HighlightModifier> for Highlight { | ||
188 | fn bitor_assign(&mut self, rhs: HighlightModifier) { | ||
189 | self.modifiers |= rhs; | ||
190 | } | ||
191 | } | ||
192 | |||
193 | impl ops::BitOr<HighlightModifier> for Highlight { | ||
194 | type Output = Highlight; | ||
195 | |||
196 | fn bitor(mut self, rhs: HighlightModifier) -> Highlight { | ||
197 | self |= rhs; | ||
198 | self | ||
199 | } | ||
200 | } | ||
201 | |||
202 | impl HighlightModifiers { | ||
203 | pub fn iter(self) -> impl Iterator<Item = HighlightModifier> { | ||
204 | HighlightModifier::ALL.iter().copied().filter(move |it| self.0 & it.mask() == it.mask()) | ||
205 | } | ||
206 | } | ||
diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs new file mode 100644 index 000000000..1c3fea058 --- /dev/null +++ b/crates/ide/src/syntax_highlighting/tests.rs | |||
@@ -0,0 +1,484 @@ | |||
1 | use std::fs; | ||
2 | |||
3 | use expect_test::{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 | // Needed for function consuming vs normal | ||
16 | pub mod marker { | ||
17 | #[lang = "copy"] | ||
18 | pub trait Copy {} | ||
19 | } | ||
20 | |||
21 | #[derive(Clone, Debug)] | ||
22 | struct Foo { | ||
23 | pub x: i32, | ||
24 | pub y: i32, | ||
25 | } | ||
26 | |||
27 | trait Bar { | ||
28 | fn bar(&self) -> i32; | ||
29 | } | ||
30 | |||
31 | impl Bar for Foo { | ||
32 | fn bar(&self) -> i32 { | ||
33 | self.x | ||
34 | } | ||
35 | } | ||
36 | |||
37 | impl Foo { | ||
38 | fn baz(mut self) -> i32 { | ||
39 | self.x | ||
40 | } | ||
41 | |||
42 | fn qux(&mut self) { | ||
43 | self.x = 0; | ||
44 | } | ||
45 | |||
46 | fn quop(&self) -> i32 { | ||
47 | self.x | ||
48 | } | ||
49 | } | ||
50 | |||
51 | #[derive(Copy, Clone)] | ||
52 | struct FooCopy { | ||
53 | x: u32, | ||
54 | } | ||
55 | |||
56 | impl FooCopy { | ||
57 | fn baz(self) -> u32 { | ||
58 | self.x | ||
59 | } | ||
60 | |||
61 | fn qux(&mut self) { | ||
62 | self.x = 0; | ||
63 | } | ||
64 | |||
65 | fn quop(&self) -> u32 { | ||
66 | self.x | ||
67 | } | ||
68 | } | ||
69 | |||
70 | static mut STATIC_MUT: i32 = 0; | ||
71 | |||
72 | fn foo<'a, T>() -> T { | ||
73 | foo::<'a, i32>() | ||
74 | } | ||
75 | |||
76 | macro_rules! def_fn { | ||
77 | ($($tt:tt)*) => {$($tt)*} | ||
78 | } | ||
79 | |||
80 | def_fn! { | ||
81 | fn bar() -> u32 { | ||
82 | 100 | ||
83 | } | ||
84 | } | ||
85 | |||
86 | macro_rules! noop { | ||
87 | ($expr:expr) => { | ||
88 | $expr | ||
89 | } | ||
90 | } | ||
91 | |||
92 | // comment | ||
93 | fn main() { | ||
94 | println!("Hello, {}!", 92); | ||
95 | |||
96 | let mut vec = Vec::new(); | ||
97 | if true { | ||
98 | let x = 92; | ||
99 | vec.push(Foo { x, y: 1 }); | ||
100 | } | ||
101 | unsafe { | ||
102 | vec.set_len(0); | ||
103 | STATIC_MUT = 1; | ||
104 | } | ||
105 | |||
106 | for e in vec { | ||
107 | // Do nothing | ||
108 | } | ||
109 | |||
110 | noop!(noop!(1)); | ||
111 | |||
112 | let mut x = 42; | ||
113 | let y = &mut x; | ||
114 | let z = &y; | ||
115 | |||
116 | let Foo { x: z, y } = Foo { x: z, y }; | ||
117 | |||
118 | y; | ||
119 | |||
120 | let mut foo = Foo { x, y: x }; | ||
121 | foo.quop(); | ||
122 | foo.qux(); | ||
123 | foo.baz(); | ||
124 | |||
125 | let mut copy = FooCopy { x }; | ||
126 | copy.quop(); | ||
127 | copy.qux(); | ||
128 | copy.baz(); | ||
129 | } | ||
130 | |||
131 | enum Option<T> { | ||
132 | Some(T), | ||
133 | None, | ||
134 | } | ||
135 | use Option::*; | ||
136 | |||
137 | impl<T> Option<T> { | ||
138 | fn and<U>(self, other: Option<U>) -> Option<(T, U)> { | ||
139 | match other { | ||
140 | None => unimplemented!(), | ||
141 | Nope => Nope, | ||
142 | } | ||
143 | } | ||
144 | } | ||
145 | "# | ||
146 | .trim(), | ||
147 | expect_file!["crates/ide/test_data/highlighting.html"], | ||
148 | false, | ||
149 | ); | ||
150 | } | ||
151 | |||
152 | #[test] | ||
153 | fn test_rainbow_highlighting() { | ||
154 | check_highlighting( | ||
155 | r#" | ||
156 | fn main() { | ||
157 | let hello = "hello"; | ||
158 | let x = hello.to_string(); | ||
159 | let y = hello.to_string(); | ||
160 | |||
161 | let x = "other color please!"; | ||
162 | let y = x.to_string(); | ||
163 | } | ||
164 | |||
165 | fn bar() { | ||
166 | let mut hello = "hello"; | ||
167 | } | ||
168 | "# | ||
169 | .trim(), | ||
170 | expect_file!["crates/ide/test_data/rainbow_highlighting.html"], | ||
171 | true, | ||
172 | ); | ||
173 | } | ||
174 | |||
175 | #[test] | ||
176 | fn accidentally_quadratic() { | ||
177 | let file = project_dir().join("crates/syntax/test_data/accidentally_quadratic"); | ||
178 | let src = fs::read_to_string(file).unwrap(); | ||
179 | |||
180 | let (analysis, file_id) = single_file(&src); | ||
181 | |||
182 | // let t = std::time::Instant::now(); | ||
183 | let _ = analysis.highlight(file_id).unwrap(); | ||
184 | // eprintln!("elapsed: {:?}", t.elapsed()); | ||
185 | } | ||
186 | |||
187 | #[test] | ||
188 | fn test_ranges() { | ||
189 | let (analysis, file_id) = single_file( | ||
190 | r#" | ||
191 | #[derive(Clone, Debug)] | ||
192 | struct Foo { | ||
193 | pub x: i32, | ||
194 | pub y: i32, | ||
195 | } | ||
196 | "#, | ||
197 | ); | ||
198 | |||
199 | // The "x" | ||
200 | let highlights = &analysis | ||
201 | .highlight_range(FileRange { file_id, range: TextRange::at(45.into(), 1.into()) }) | ||
202 | .unwrap(); | ||
203 | |||
204 | assert_eq!(&highlights[0].highlight.to_string(), "field.declaration"); | ||
205 | } | ||
206 | |||
207 | #[test] | ||
208 | fn test_flattening() { | ||
209 | check_highlighting( | ||
210 | r##" | ||
211 | fn fixture(ra_fixture: &str) {} | ||
212 | |||
213 | fn main() { | ||
214 | fixture(r#" | ||
215 | trait Foo { | ||
216 | fn foo() { | ||
217 | println!("2 + 2 = {}", 4); | ||
218 | } | ||
219 | }"# | ||
220 | ); | ||
221 | }"## | ||
222 | .trim(), | ||
223 | expect_file!["crates/ide/test_data/highlight_injection.html"], | ||
224 | false, | ||
225 | ); | ||
226 | } | ||
227 | |||
228 | #[test] | ||
229 | fn ranges_sorted() { | ||
230 | let (analysis, file_id) = single_file( | ||
231 | r#" | ||
232 | #[foo(bar = "bar")] | ||
233 | macro_rules! test {} | ||
234 | }"# | ||
235 | .trim(), | ||
236 | ); | ||
237 | let _ = analysis.highlight(file_id).unwrap(); | ||
238 | } | ||
239 | |||
240 | #[test] | ||
241 | fn test_string_highlighting() { | ||
242 | // The format string detection is based on macro-expansion, | ||
243 | // thus, we have to copy the macro definition from `std` | ||
244 | check_highlighting( | ||
245 | r#" | ||
246 | macro_rules! println { | ||
247 | ($($arg:tt)*) => ({ | ||
248 | $crate::io::_print($crate::format_args_nl!($($arg)*)); | ||
249 | }) | ||
250 | } | ||
251 | #[rustc_builtin_macro] | ||
252 | macro_rules! format_args_nl { | ||
253 | ($fmt:expr) => {{ /* compiler built-in */ }}; | ||
254 | ($fmt:expr, $($args:tt)*) => {{ /* compiler built-in */ }}; | ||
255 | } | ||
256 | |||
257 | fn main() { | ||
258 | // from https://doc.rust-lang.org/std/fmt/index.html | ||
259 | println!("Hello"); // => "Hello" | ||
260 | println!("Hello, {}!", "world"); // => "Hello, world!" | ||
261 | println!("The number is {}", 1); // => "The number is 1" | ||
262 | println!("{:?}", (3, 4)); // => "(3, 4)" | ||
263 | println!("{value}", value=4); // => "4" | ||
264 | println!("{} {}", 1, 2); // => "1 2" | ||
265 | println!("{:04}", 42); // => "0042" with leading zerosV | ||
266 | println!("{1} {} {0} {}", 1, 2); // => "2 1 1 2" | ||
267 | println!("{argument}", argument = "test"); // => "test" | ||
268 | println!("{name} {}", 1, name = 2); // => "2 1" | ||
269 | println!("{a} {c} {b}", a="a", b='b', c=3); // => "a 3 b" | ||
270 | println!("{{{}}}", 2); // => "{2}" | ||
271 | println!("Hello {:5}!", "x"); | ||
272 | println!("Hello {:1$}!", "x", 5); | ||
273 | println!("Hello {1:0$}!", 5, "x"); | ||
274 | println!("Hello {:width$}!", "x", width = 5); | ||
275 | println!("Hello {:<5}!", "x"); | ||
276 | println!("Hello {:-<5}!", "x"); | ||
277 | println!("Hello {:^5}!", "x"); | ||
278 | println!("Hello {:>5}!", "x"); | ||
279 | println!("Hello {:+}!", 5); | ||
280 | println!("{:#x}!", 27); | ||
281 | println!("Hello {:05}!", 5); | ||
282 | println!("Hello {:05}!", -5); | ||
283 | println!("{:#010x}!", 27); | ||
284 | println!("Hello {0} is {1:.5}", "x", 0.01); | ||
285 | println!("Hello {1} is {2:.0$}", 5, "x", 0.01); | ||
286 | println!("Hello {0} is {2:.1$}", "x", 5, 0.01); | ||
287 | println!("Hello {} is {:.*}", "x", 5, 0.01); | ||
288 | println!("Hello {} is {2:.*}", "x", 5, 0.01); | ||
289 | println!("Hello {} is {number:.prec$}", "x", prec = 5, number = 0.01); | ||
290 | println!("{}, `{name:.*}` has 3 fractional digits", "Hello", 3, name=1234.56); | ||
291 | println!("{}, `{name:.*}` has 3 characters", "Hello", 3, name="1234.56"); | ||
292 | println!("{}, `{name:>8.*}` has 3 right-aligned characters", "Hello", 3, name="1234.56"); | ||
293 | println!("Hello {{}}"); | ||
294 | println!("{{ Hello"); | ||
295 | |||
296 | println!(r"Hello, {}!", "world"); | ||
297 | |||
298 | // escape sequences | ||
299 | println!("Hello\nWorld"); | ||
300 | println!("\u{48}\x65\x6C\x6C\x6F World"); | ||
301 | |||
302 | println!("{\x41}", A = 92); | ||
303 | println!("{ничоси}", ничоси = 92); | ||
304 | }"# | ||
305 | .trim(), | ||
306 | expect_file!["crates/ide/test_data/highlight_strings.html"], | ||
307 | false, | ||
308 | ); | ||
309 | } | ||
310 | |||
311 | #[test] | ||
312 | fn test_unsafe_highlighting() { | ||
313 | check_highlighting( | ||
314 | r#" | ||
315 | unsafe fn unsafe_fn() {} | ||
316 | |||
317 | union Union { | ||
318 | a: u32, | ||
319 | b: f32, | ||
320 | } | ||
321 | |||
322 | struct HasUnsafeFn; | ||
323 | |||
324 | impl HasUnsafeFn { | ||
325 | unsafe fn unsafe_method(&self) {} | ||
326 | } | ||
327 | |||
328 | struct TypeForStaticMut { | ||
329 | a: u8 | ||
330 | } | ||
331 | |||
332 | static mut global_mut: TypeForStaticMut = TypeForStaticMut { a: 0 }; | ||
333 | |||
334 | #[repr(packed)] | ||
335 | struct Packed { | ||
336 | a: u16, | ||
337 | } | ||
338 | |||
339 | trait DoTheAutoref { | ||
340 | fn calls_autoref(&self); | ||
341 | } | ||
342 | |||
343 | impl DoTheAutoref for u16 { | ||
344 | fn calls_autoref(&self) {} | ||
345 | } | ||
346 | |||
347 | fn main() { | ||
348 | let x = &5 as *const _ as *const usize; | ||
349 | let u = Union { b: 0 }; | ||
350 | unsafe { | ||
351 | // unsafe fn and method calls | ||
352 | unsafe_fn(); | ||
353 | let b = u.b; | ||
354 | match u { | ||
355 | Union { b: 0 } => (), | ||
356 | Union { a } => (), | ||
357 | } | ||
358 | HasUnsafeFn.unsafe_method(); | ||
359 | |||
360 | // unsafe deref | ||
361 | let y = *x; | ||
362 | |||
363 | // unsafe access to a static mut | ||
364 | let a = global_mut.a; | ||
365 | |||
366 | // unsafe ref of packed fields | ||
367 | let packed = Packed { a: 0 }; | ||
368 | let a = &packed.a; | ||
369 | let ref a = packed.a; | ||
370 | let Packed { ref a } = packed; | ||
371 | let Packed { a: ref _a } = packed; | ||
372 | |||
373 | // unsafe auto ref of packed field | ||
374 | packed.a.calls_autoref(); | ||
375 | } | ||
376 | } | ||
377 | "# | ||
378 | .trim(), | ||
379 | expect_file!["crates/ide/test_data/highlight_unsafe.html"], | ||
380 | false, | ||
381 | ); | ||
382 | } | ||
383 | |||
384 | #[test] | ||
385 | fn test_highlight_doctest() { | ||
386 | check_highlighting( | ||
387 | r#" | ||
388 | /// ``` | ||
389 | /// let _ = "early doctests should not go boom"; | ||
390 | /// ``` | ||
391 | struct Foo { | ||
392 | bar: bool, | ||
393 | } | ||
394 | |||
395 | impl Foo { | ||
396 | pub const bar: bool = true; | ||
397 | |||
398 | /// Constructs a new `Foo`. | ||
399 | /// | ||
400 | /// # Examples | ||
401 | /// | ||
402 | /// ``` | ||
403 | /// # #![allow(unused_mut)] | ||
404 | /// let mut foo: Foo = Foo::new(); | ||
405 | /// ``` | ||
406 | pub const fn new() -> Foo { | ||
407 | Foo { bar: true } | ||
408 | } | ||
409 | |||
410 | /// `bar` method on `Foo`. | ||
411 | /// | ||
412 | /// # Examples | ||
413 | /// | ||
414 | /// ``` | ||
415 | /// use x::y; | ||
416 | /// | ||
417 | /// let foo = Foo::new(); | ||
418 | /// | ||
419 | /// // calls bar on foo | ||
420 | /// assert!(foo.bar()); | ||
421 | /// | ||
422 | /// let bar = foo.bar || Foo::bar; | ||
423 | /// | ||
424 | /// /* multi-line | ||
425 | /// comment */ | ||
426 | /// | ||
427 | /// let multi_line_string = "Foo | ||
428 | /// bar | ||
429 | /// "; | ||
430 | /// | ||
431 | /// ``` | ||
432 | /// | ||
433 | /// ```rust,no_run | ||
434 | /// let foobar = Foo::new().bar(); | ||
435 | /// ``` | ||
436 | /// | ||
437 | /// ```sh | ||
438 | /// echo 1 | ||
439 | /// ``` | ||
440 | pub fn foo(&self) -> bool { | ||
441 | true | ||
442 | } | ||
443 | } | ||
444 | |||
445 | /// ``` | ||
446 | /// noop!(1); | ||
447 | /// ``` | ||
448 | macro_rules! noop { | ||
449 | ($expr:expr) => { | ||
450 | $expr | ||
451 | } | ||
452 | } | ||
453 | "# | ||
454 | .trim(), | ||
455 | expect_file!["crates/ide/test_data/highlight_doctest.html"], | ||
456 | false, | ||
457 | ); | ||
458 | } | ||
459 | |||
460 | #[test] | ||
461 | fn test_extern_crate() { | ||
462 | check_highlighting( | ||
463 | r#" | ||
464 | //- /main.rs | ||
465 | extern crate std; | ||
466 | extern crate alloc as abc; | ||
467 | //- /std/lib.rs | ||
468 | pub struct S; | ||
469 | //- /alloc/lib.rs | ||
470 | pub struct A | ||
471 | "#, | ||
472 | expect_file!["crates/ide/test_data/highlight_extern_crate.html"], | ||
473 | false, | ||
474 | ); | ||
475 | } | ||
476 | |||
477 | /// Highlights the code given by the `ra_fixture` argument, renders the | ||
478 | /// result as HTML, and compares it with the HTML file given as `snapshot`. | ||
479 | /// Note that the `snapshot` file is overwritten by the rendered HTML. | ||
480 | fn check_highlighting(ra_fixture: &str, expect: ExpectFile, rainbow: bool) { | ||
481 | let (analysis, file_id) = single_file(ra_fixture); | ||
482 | let actual_html = &analysis.highlight_as_html(file_id, rainbow).unwrap(); | ||
483 | expect.assert_eq(actual_html) | ||
484 | } | ||