Skip to content

Commit db36a25

Browse files
bors[bot]ltentrup
andauthored
Merge #4683
4683: Implement syntax highlighting for doctests r=ltentrup a=ltentrup The implementation is more complicated than the previous injection logic as the doctest comments consist of multiple ranges. The implementation extracts the doctests together with an offset-mapping, applies the syntax highlighting, and updates the text ranges. <img width="478" alt="Bildschirmfoto 2020-06-01 um 15 45 25" src="https://user-images.githubusercontent.com/201808/83415249-1f0b5800-a41f-11ea-8fa6-c282434d6ff7.png"> Part of #4170. Co-authored-by: Leander Tentrup <leander.tentrup@gmail.com>
2 parents ab86f15 + 4a2efb2 commit db36a25

File tree

4 files changed

+368
-46
lines changed

4 files changed

+368
-46
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
2+
<style>
3+
body { margin: 0; }
4+
pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padding: 0.4em; }
5+
6+
.lifetime { color: #DFAF8F; font-style: italic; }
7+
.comment { color: #7F9F7F; }
8+
.struct, .enum { color: #7CB8BB; }
9+
.enum_variant { color: #BDE0F3; }
10+
.string_literal { color: #CC9393; }
11+
.field { color: #94BFF3; }
12+
.function { color: #93E0E3; }
13+
.operator.unsafe { color: #E28C14; }
14+
.parameter { color: #94BFF3; }
15+
.text { color: #DCDCCC; }
16+
.type { color: #7CB8BB; }
17+
.builtin_type { color: #8CD0D3; }
18+
.type_param { color: #DFAF8F; }
19+
.attribute { color: #94BFF3; }
20+
.numeric_literal { color: #BFEBBF; }
21+
.bool_literal { color: #BFE6EB; }
22+
.macro { color: #94BFF3; }
23+
.module { color: #AFD8AF; }
24+
.variable { color: #DCDCCC; }
25+
.format_specifier { color: #CC696B; }
26+
.mutable { text-decoration: underline; }
27+
28+
.keyword { color: #F0DFAF; font-weight: bold; }
29+
.keyword.unsafe { color: #BC8383; font-weight: bold; }
30+
.control { font-style: italic; }
31+
</style>
32+
<pre><code><span class="keyword">impl</span> <span class="unresolved_reference">Foo</span> {
33+
<span class="comment">/// Constructs a new `Foo`.</span>
34+
<span class="comment">///</span>
35+
<span class="comment">/// # Examples</span>
36+
<span class="comment">///</span>
37+
<span class="comment">/// ```</span>
38+
<span class="comment">/// #</span> <span class="attribute">#![</span><span class="function attribute">allow</span><span class="attribute">(unused_mut)]</span>
39+
<span class="comment">/// </span><span class="keyword">let</span> <span class="keyword">mut</span> <span class="variable declaration mutable">foo</span>: <span class="unresolved_reference">Foo</span> = <span class="unresolved_reference">Foo</span>::<span class="unresolved_reference">new</span>();
40+
<span class="comment">/// ```</span>
41+
<span class="keyword">pub</span> <span class="keyword">const</span> <span class="keyword">fn</span> <span class="function declaration">new</span>() -&gt; <span class="unresolved_reference">Foo</span> {
42+
<span class="unresolved_reference">Foo</span> { }
43+
}
44+
45+
<span class="comment">/// `bar` method on `Foo`.</span>
46+
<span class="comment">///</span>
47+
<span class="comment">/// # Examples</span>
48+
<span class="comment">///</span>
49+
<span class="comment">/// ```</span>
50+
<span class="comment">/// </span><span class="keyword">let</span> <span class="variable declaration">foo</span> = <span class="unresolved_reference">Foo</span>::<span class="unresolved_reference">new</span>();
51+
<span class="comment">///</span>
52+
<span class="comment">/// </span><span class="comment">// calls bar on foo</span>
53+
<span class="comment">/// </span><span class="macro">assert!</span>(foo.bar());
54+
<span class="comment">///</span>
55+
<span class="comment">/// </span><span class="comment">/* multi-line
56+
</span><span class="comment">/// </span><span class="comment"> comment */</span>
57+
<span class="comment">///</span>
58+
<span class="comment">/// </span><span class="keyword">let</span> <span class="variable declaration">multi_line_string</span> = <span class="string_literal">"Foo
59+
</span><span class="comment">/// </span><span class="string_literal"> bar
60+
</span><span class="comment">/// </span><span class="string_literal"> "</span>;
61+
<span class="comment">///</span>
62+
<span class="comment">/// ```</span>
63+
<span class="comment">///</span>
64+
<span class="comment">/// ```</span>
65+
<span class="comment">/// </span><span class="keyword">let</span> <span class="variable declaration">foobar</span> = <span class="unresolved_reference">Foo</span>::<span class="unresolved_reference">new</span>().<span class="unresolved_reference">bar</span>();
66+
<span class="comment">/// ```</span>
67+
<span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function declaration">foo</span>(&<span class="self_keyword">self</span>) -&gt; <span class="builtin_type">bool</span> {
68+
<span class="bool_literal">true</span>
69+
}
70+
}</code></pre>

crates/ra_ide/src/syntax_highlighting.rs

Lines changed: 80 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod tags;
22
mod html;
3+
mod injection;
34
#[cfg(test)]
45
mod tests;
56

@@ -10,14 +11,14 @@ use ra_ide_db::{
1011
};
1112
use ra_prof::profile;
1213
use ra_syntax::{
13-
ast::{self, HasFormatSpecifier, HasQuotes, HasStringValue},
14+
ast::{self, HasFormatSpecifier},
1415
AstNode, AstToken, Direction, NodeOrToken, SyntaxElement,
1516
SyntaxKind::*,
16-
SyntaxToken, TextRange, WalkEvent, T,
17+
TextRange, WalkEvent, T,
1718
};
1819
use rustc_hash::FxHashMap;
1920

20-
use crate::{call_info::ActiveParameter, Analysis, FileId};
21+
use crate::FileId;
2122

2223
use ast::FormatSpecifier;
2324
pub(crate) use html::highlight_as_html;
@@ -123,6 +124,23 @@ pub(crate) fn highlight(
123124
_ => (),
124125
}
125126

127+
// Check for Rust code in documentation
128+
match &event {
129+
WalkEvent::Leave(NodeOrToken::Node(node)) => {
130+
if let Some((doctest, range_mapping, new_comments)) =
131+
injection::extract_doc_comments(node)
132+
{
133+
injection::highlight_doc_comment(
134+
doctest,
135+
range_mapping,
136+
new_comments,
137+
&mut stack,
138+
);
139+
}
140+
}
141+
_ => (),
142+
}
143+
126144
let element = match event {
127145
WalkEvent::Enter(it) => it,
128146
WalkEvent::Leave(_) => continue,
@@ -173,7 +191,7 @@ pub(crate) fn highlight(
173191

174192
if let Some(token) = element.as_token().cloned().and_then(ast::RawString::cast) {
175193
let expanded = element_to_highlight.as_token().unwrap().clone();
176-
if highlight_injection(&mut stack, &sema, token, expanded).is_some() {
194+
if injection::highlight_injection(&mut stack, &sema, token, expanded).is_some() {
177195
continue;
178196
}
179197
}
@@ -259,9 +277,8 @@ impl HighlightedRangeStack {
259277
let mut parent = prev.pop().unwrap();
260278
for ele in children {
261279
assert!(parent.range.contains_range(ele.range));
262-
let mut cloned = parent.clone();
263-
parent.range = TextRange::new(parent.range.start(), ele.range.start());
264-
cloned.range = TextRange::new(ele.range.end(), cloned.range.end());
280+
281+
let cloned = Self::intersect(&mut parent, &ele);
265282
if !parent.range.is_empty() {
266283
prev.push(parent);
267284
}
@@ -274,6 +291,62 @@ impl HighlightedRangeStack {
274291
}
275292
}
276293

294+
/// Intersects the `HighlightedRange` `parent` with `child`.
295+
/// `parent` is mutated in place, becoming the range before `child`.
296+
/// Returns the range (of the same type as `parent`) *after* `child`.
297+
fn intersect(parent: &mut HighlightedRange, child: &HighlightedRange) -> HighlightedRange {
298+
assert!(parent.range.contains_range(child.range));
299+
300+
let mut cloned = parent.clone();
301+
parent.range = TextRange::new(parent.range.start(), child.range.start());
302+
cloned.range = TextRange::new(child.range.end(), cloned.range.end());
303+
304+
cloned
305+
}
306+
307+
/// Similar to `pop`, but can modify arbitrary prior ranges (where `pop`)
308+
/// can only modify the last range currently on the stack.
309+
/// Can be used to do injections that span multiple ranges, like the
310+
/// doctest injection below.
311+
/// If `delete` is set to true, the parent range is deleted instead of
312+
/// intersected.
313+
///
314+
/// Note that `pop` can be simulated by `pop_and_inject(false)` but the
315+
/// latter is computationally more expensive.
316+
fn pop_and_inject(&mut self, delete: bool) {
317+
let mut children = self.stack.pop().unwrap();
318+
let prev = self.stack.last_mut().unwrap();
319+
children.sort_by_key(|range| range.range.start());
320+
prev.sort_by_key(|range| range.range.start());
321+
322+
for child in children {
323+
if let Some(idx) =
324+
prev.iter().position(|parent| parent.range.contains_range(child.range))
325+
{
326+
let cloned = Self::intersect(&mut prev[idx], &child);
327+
let insert_idx = if delete || prev[idx].range.is_empty() {
328+
prev.remove(idx);
329+
idx
330+
} else {
331+
idx + 1
332+
};
333+
prev.insert(insert_idx, child);
334+
if !delete && !cloned.range.is_empty() {
335+
prev.insert(insert_idx + 1, cloned);
336+
}
337+
} else if let Some(_idx) =
338+
prev.iter().position(|parent| parent.range.contains(child.range.start()))
339+
{
340+
unreachable!("child range should be completely contained in parent range");
341+
} else {
342+
let idx = prev
343+
.binary_search_by_key(&child.range.start(), |range| range.range.start())
344+
.unwrap_or_else(|x| x);
345+
prev.insert(idx, child);
346+
}
347+
}
348+
}
349+
277350
fn add(&mut self, range: HighlightedRange) {
278351
self.stack
279352
.last_mut()
@@ -539,42 +612,3 @@ fn highlight_name_by_syntax(name: ast::Name) -> Highlight {
539612

540613
tag.into()
541614
}
542-
543-
fn highlight_injection(
544-
acc: &mut HighlightedRangeStack,
545-
sema: &Semantics<RootDatabase>,
546-
literal: ast::RawString,
547-
expanded: SyntaxToken,
548-
) -> Option<()> {
549-
let active_parameter = ActiveParameter::at_token(&sema, expanded)?;
550-
if !active_parameter.name.starts_with("ra_fixture") {
551-
return None;
552-
}
553-
let value = literal.value()?;
554-
let (analysis, tmp_file_id) = Analysis::from_single_file(value);
555-
556-
if let Some(range) = literal.open_quote_text_range() {
557-
acc.add(HighlightedRange {
558-
range,
559-
highlight: HighlightTag::StringLiteral.into(),
560-
binding_hash: None,
561-
})
562-
}
563-
564-
for mut h in analysis.highlight(tmp_file_id).unwrap() {
565-
if let Some(r) = literal.map_range_up(h.range) {
566-
h.range = r;
567-
acc.add(h)
568-
}
569-
}
570-
571-
if let Some(range) = literal.close_quote_text_range() {
572-
acc.add(HighlightedRange {
573-
range,
574-
highlight: HighlightTag::StringLiteral.into(),
575-
binding_hash: None,
576-
})
577-
}
578-
579-
Some(())
580-
}

0 commit comments

Comments
 (0)