diff options
-rw-r--r-- | crates/hir_def/src/attr.rs | 109 | ||||
-rw-r--r-- | crates/ide/src/goto_definition.rs | 1 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/inject.rs | 55 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html | 10 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/tests.rs | 10 |
5 files changed, 149 insertions, 36 deletions
diff --git a/crates/hir_def/src/attr.rs b/crates/hir_def/src/attr.rs index 52a2bce9b..7791402c9 100644 --- a/crates/hir_def/src/attr.rs +++ b/crates/hir_def/src/attr.rs | |||
@@ -1,6 +1,10 @@ | |||
1 | //! A higher level attributes based on TokenTree, with also some shortcuts. | 1 | //! A higher level attributes based on TokenTree, with also some shortcuts. |
2 | 2 | ||
3 | use std::{ops, sync::Arc}; | 3 | use std::{ |
4 | cmp::Ordering, | ||
5 | ops::{self, Range}, | ||
6 | sync::Arc, | ||
7 | }; | ||
4 | 8 | ||
5 | use base_db::CrateId; | 9 | use base_db::CrateId; |
6 | use cfg::{CfgExpr, CfgOptions}; | 10 | use cfg::{CfgExpr, CfgOptions}; |
@@ -12,7 +16,7 @@ use mbe::ast_to_token_tree; | |||
12 | use smallvec::{smallvec, SmallVec}; | 16 | use smallvec::{smallvec, SmallVec}; |
13 | use syntax::{ | 17 | use syntax::{ |
14 | ast::{self, AstNode, AttrsOwner}, | 18 | ast::{self, AstNode, AttrsOwner}, |
15 | match_ast, AstToken, SmolStr, SyntaxNode, | 19 | match_ast, AstToken, SmolStr, SyntaxNode, TextRange, TextSize, |
16 | }; | 20 | }; |
17 | use tt::Subtree; | 21 | use tt::Subtree; |
18 | 22 | ||
@@ -451,6 +455,54 @@ impl AttrsWithOwner { | |||
451 | .collect(), | 455 | .collect(), |
452 | } | 456 | } |
453 | } | 457 | } |
458 | |||
459 | pub fn docs_with_rangemap( | ||
460 | &self, | ||
461 | db: &dyn DefDatabase, | ||
462 | ) -> Option<(Documentation, DocsRangeMap)> { | ||
463 | // FIXME: code duplication in `docs` above | ||
464 | let docs = self.by_key("doc").attrs().flat_map(|attr| match attr.input.as_ref()? { | ||
465 | AttrInput::Literal(s) => Some((s, attr.index)), | ||
466 | AttrInput::TokenTree(_) => None, | ||
467 | }); | ||
468 | let indent = docs | ||
469 | .clone() | ||
470 | .flat_map(|(s, _)| s.lines()) | ||
471 | .filter(|line| !line.chars().all(|c| c.is_whitespace())) | ||
472 | .map(|line| line.chars().take_while(|c| c.is_whitespace()).count()) | ||
473 | .min() | ||
474 | .unwrap_or(0); | ||
475 | let mut buf = String::new(); | ||
476 | let mut mapping = Vec::new(); | ||
477 | for (doc, idx) in docs { | ||
478 | // str::lines doesn't yield anything for the empty string | ||
479 | if !doc.is_empty() { | ||
480 | for line in doc.split('\n') { | ||
481 | let line = line.trim_end(); | ||
482 | let (offset, line) = match line.char_indices().nth(indent) { | ||
483 | Some((offset, _)) => (offset, &line[offset..]), | ||
484 | None => (0, line), | ||
485 | }; | ||
486 | let buf_offset = buf.len(); | ||
487 | buf.push_str(line); | ||
488 | mapping.push(( | ||
489 | Range { start: buf_offset, end: buf.len() }, | ||
490 | idx, | ||
491 | Range { start: offset, end: line.len() }, | ||
492 | )); | ||
493 | buf.push('\n'); | ||
494 | } | ||
495 | } else { | ||
496 | buf.push('\n'); | ||
497 | } | ||
498 | } | ||
499 | buf.pop(); | ||
500 | if buf.is_empty() { | ||
501 | None | ||
502 | } else { | ||
503 | Some((Documentation(buf), DocsRangeMap { mapping, source: self.source_map(db).attrs })) | ||
504 | } | ||
505 | } | ||
454 | } | 506 | } |
455 | 507 | ||
456 | fn inner_attributes( | 508 | fn inner_attributes( |
@@ -507,6 +559,59 @@ impl AttrSourceMap { | |||
507 | } | 559 | } |
508 | } | 560 | } |
509 | 561 | ||
562 | /// A struct to map text ranges from [`Documentation`] back to TextRanges in the syntax tree. | ||
563 | pub struct DocsRangeMap { | ||
564 | source: Vec<InFile<Either<ast::Attr, ast::Comment>>>, | ||
565 | // (docstring-line-range, attr_index, attr-string-range) | ||
566 | // a mapping from the text range of a line of the [`Documentation`] to the attribute index and | ||
567 | // the original (untrimmed) syntax doc line | ||
568 | mapping: Vec<(Range<usize>, u32, Range<usize>)>, | ||
569 | } | ||
570 | |||
571 | impl DocsRangeMap { | ||
572 | pub fn map(&self, range: Range<usize>) -> Option<InFile<TextRange>> { | ||
573 | let found = self | ||
574 | .mapping | ||
575 | .binary_search_by(|(probe, ..)| { | ||
576 | if probe.contains(&range.start) { | ||
577 | Ordering::Equal | ||
578 | } else { | ||
579 | probe.start.cmp(&range.end) | ||
580 | } | ||
581 | }) | ||
582 | .ok()?; | ||
583 | let (line_docs_range, idx, original_line_src_range) = self.mapping[found].clone(); | ||
584 | if range.end > line_docs_range.end { | ||
585 | return None; | ||
586 | } | ||
587 | |||
588 | let relative_range = Range { | ||
589 | start: range.start - line_docs_range.start, | ||
590 | end: range.end - line_docs_range.start, | ||
591 | }; | ||
592 | let range_len = TextSize::from((range.end - range.start) as u32); | ||
593 | |||
594 | let &InFile { file_id, value: ref source } = &self.source[idx as usize]; | ||
595 | match source { | ||
596 | Either::Left(_) => None, // FIXME, figure out a nice way to handle doc attributes here | ||
597 | // as well as for whats done in syntax highlight doc injection | ||
598 | Either::Right(comment) => { | ||
599 | let text_range = comment.syntax().text_range(); | ||
600 | let range = TextRange::at( | ||
601 | text_range.start() | ||
602 | + TextSize::from( | ||
603 | (comment.prefix().len() | ||
604 | + original_line_src_range.start | ||
605 | + relative_range.start) as u32, | ||
606 | ), | ||
607 | text_range.len().min(range_len), | ||
608 | ); | ||
609 | Some(InFile { file_id, value: range }) | ||
610 | } | ||
611 | } | ||
612 | } | ||
613 | } | ||
614 | |||
510 | #[derive(Debug, Clone, PartialEq, Eq)] | 615 | #[derive(Debug, Clone, PartialEq, Eq)] |
511 | pub struct Attr { | 616 | pub struct Attr { |
512 | index: u32, | 617 | index: u32, |
diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 4e4d1b200..1951c599f 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs | |||
@@ -32,6 +32,7 @@ pub(crate) fn goto_definition( | |||
32 | let parent = token.parent()?; | 32 | let parent = token.parent()?; |
33 | if let Some(comment) = ast::Comment::cast(token) { | 33 | if let Some(comment) = ast::Comment::cast(token) { |
34 | let docs = doc_owner_to_def(&sema, &parent)?.docs(db)?; | 34 | let docs = doc_owner_to_def(&sema, &parent)?.docs(db)?; |
35 | |||
35 | let (_, link, ns) = extract_positioned_link_from_comment(position.offset, &comment, docs)?; | 36 | let (_, link, ns) = extract_positioned_link_from_comment(position.offset, &comment, docs)?; |
36 | let def = doc_owner_to_def(&sema, &parent)?; | 37 | let def = doc_owner_to_def(&sema, &parent)?; |
37 | let nav = resolve_doc_path_for_def(db, def, &link, ns)?.try_to_nav(db)?; | 38 | let nav = resolve_doc_path_for_def(db, def, &link, ns)?.try_to_nav(db)?; |
diff --git a/crates/ide/src/syntax_highlighting/inject.rs b/crates/ide/src/syntax_highlighting/inject.rs index b62d43256..504783f31 100644 --- a/crates/ide/src/syntax_highlighting/inject.rs +++ b/crates/ide/src/syntax_highlighting/inject.rs | |||
@@ -1,6 +1,6 @@ | |||
1 | //! "Recursive" Syntax highlighting for code in doctests and fixtures. | 1 | //! "Recursive" Syntax highlighting for code in doctests and fixtures. |
2 | 2 | ||
3 | use std::{mem, ops::Range}; | 3 | use std::mem; |
4 | 4 | ||
5 | use either::Either; | 5 | use either::Either; |
6 | use hir::{HasAttrs, InFile, Semantics}; | 6 | use hir::{HasAttrs, InFile, Semantics}; |
@@ -139,8 +139,28 @@ pub(super) fn doc_comment( | |||
139 | // Replace the original, line-spanning comment ranges by new, only comment-prefix | 139 | // Replace the original, line-spanning comment ranges by new, only comment-prefix |
140 | // spanning comment ranges. | 140 | // spanning comment ranges. |
141 | let mut new_comments = Vec::new(); | 141 | let mut new_comments = Vec::new(); |
142 | let mut intra_doc_links = Vec::new(); | ||
143 | let mut string; | 142 | let mut string; |
143 | |||
144 | if let Some((docs, doc_mapping)) = attributes.docs_with_rangemap(sema.db) { | ||
145 | extract_definitions_from_markdown(docs.as_str()) | ||
146 | .into_iter() | ||
147 | .filter_map(|(range, link, ns)| { | ||
148 | let def = resolve_doc_path_for_def(sema.db, def, &link, ns)?; | ||
149 | let InFile { file_id, value: range } = doc_mapping.map(range)?; | ||
150 | (file_id == node.file_id).then(|| (range, def)) | ||
151 | }) | ||
152 | .for_each(|(range, def)| { | ||
153 | hl.add(HlRange { | ||
154 | range, | ||
155 | highlight: module_def_to_hl_tag(def) | ||
156 | | HlMod::Documentation | ||
157 | | HlMod::Injected | ||
158 | | HlMod::IntraDocLink, | ||
159 | binding_hash: None, | ||
160 | }) | ||
161 | }); | ||
162 | } | ||
163 | |||
144 | for attr in attributes.by_key("doc").attrs() { | 164 | for attr in attributes.by_key("doc").attrs() { |
145 | let InFile { file_id, value: src } = attrs_source_map.source_of(&attr); | 165 | let InFile { file_id, value: src } = attrs_source_map.source_of(&attr); |
146 | if file_id != node.file_id { | 166 | if file_id != node.file_id { |
@@ -186,25 +206,7 @@ pub(super) fn doc_comment( | |||
186 | is_doctest = is_codeblock && is_rust; | 206 | is_doctest = is_codeblock && is_rust; |
187 | continue; | 207 | continue; |
188 | } | 208 | } |
189 | None if !is_doctest => { | 209 | None if !is_doctest => continue, |
190 | intra_doc_links.extend( | ||
191 | extract_definitions_from_markdown(line) | ||
192 | .into_iter() | ||
193 | .filter_map(|(range, link, ns)| { | ||
194 | Some(range).zip(resolve_doc_path_for_def(sema.db, def, &link, ns)) | ||
195 | }) | ||
196 | .map(|(Range { start, end }, def)| { | ||
197 | ( | ||
198 | def, | ||
199 | TextRange::at( | ||
200 | prev_range_start + TextSize::from(start as u32), | ||
201 | TextSize::from((end - start) as u32), | ||
202 | ), | ||
203 | ) | ||
204 | }), | ||
205 | ); | ||
206 | continue; | ||
207 | } | ||
208 | None => (), | 210 | None => (), |
209 | } | 211 | } |
210 | 212 | ||
@@ -223,17 +225,6 @@ pub(super) fn doc_comment( | |||
223 | } | 225 | } |
224 | } | 226 | } |
225 | 227 | ||
226 | for (def, range) in intra_doc_links { | ||
227 | hl.add(HlRange { | ||
228 | range, | ||
229 | highlight: module_def_to_hl_tag(def) | ||
230 | | HlMod::Documentation | ||
231 | | HlMod::Injected | ||
232 | | HlMod::IntraDocLink, | ||
233 | binding_hash: None, | ||
234 | }); | ||
235 | } | ||
236 | |||
237 | if new_comments.is_empty() { | 228 | if new_comments.is_empty() { |
238 | return; // no need to run an analysis on an empty file | 229 | return; // no need to run an analysis on an empty file |
239 | } | 230 | } |
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html b/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html index 045162eb8..b6d1cac4e 100644 --- a/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html +++ b/crates/ide/src/syntax_highlighting/test_data/highlight_doctest.html | |||
@@ -100,10 +100,18 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd | |||
100 | <span class="brace">}</span> | 100 | <span class="brace">}</span> |
101 | 101 | ||
102 | <span class="comment documentation">/// </span><span class="struct documentation intra_doc_link injected">[`Foo`](Foo)</span><span class="comment documentation"> is a struct</span> | 102 | <span class="comment documentation">/// </span><span class="struct documentation intra_doc_link injected">[`Foo`](Foo)</span><span class="comment documentation"> is a struct</span> |
103 | <span class="comment documentation">/// </span><span class="function documentation intra_doc_link injected">[`all_the_links`](all_the_links)</span><span class="comment documentation"> is this function</span> | 103 | <span class="comment documentation">/// This function is > </span><span class="function documentation intra_doc_link injected">[`all_the_links`](all_the_links)</span><span class="comment documentation"> <</span> |
104 | <span class="comment documentation">/// [`noop`](noop) is a macro below</span> | 104 | <span class="comment documentation">/// [`noop`](noop) is a macro below</span> |
105 | <span class="comment documentation">/// </span><span class="struct documentation intra_doc_link injected">[`Item`]</span><span class="comment documentation"> is a struct in the module </span><span class="module documentation intra_doc_link injected">[`module`]</span> | ||
106 | <span class="comment documentation">///</span> | ||
107 | <span class="comment documentation">/// [`Item`]: module::Item</span> | ||
108 | <span class="comment documentation">/// [mix_and_match]: ThisShouldntResolve</span> | ||
105 | <span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function declaration">all_the_links</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> | 109 | <span class="keyword">pub</span> <span class="keyword">fn</span> <span class="function declaration">all_the_links</span><span class="parenthesis">(</span><span class="parenthesis">)</span> <span class="brace">{</span><span class="brace">}</span> |
106 | 110 | ||
111 | <span class="keyword">pub</span> <span class="keyword">mod</span> <span class="module declaration">module</span> <span class="brace">{</span> | ||
112 | <span class="keyword">pub</span> <span class="keyword">struct</span> <span class="struct declaration">Item</span><span class="semicolon">;</span> | ||
113 | <span class="brace">}</span> | ||
114 | |||
107 | <span class="comment documentation">/// ```</span> | 115 | <span class="comment documentation">/// ```</span> |
108 | <span class="comment documentation">/// </span><span class="macro injected">noop!</span><span class="parenthesis injected">(</span><span class="numeric_literal injected">1</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span> | 116 | <span class="comment documentation">/// </span><span class="macro injected">noop!</span><span class="parenthesis injected">(</span><span class="numeric_literal injected">1</span><span class="parenthesis injected">)</span><span class="semicolon injected">;</span> |
109 | <span class="comment documentation">/// ```</span> | 117 | <span class="comment documentation">/// ```</span> |
diff --git a/crates/ide/src/syntax_highlighting/tests.rs b/crates/ide/src/syntax_highlighting/tests.rs index 369ae0972..1b02857ec 100644 --- a/crates/ide/src/syntax_highlighting/tests.rs +++ b/crates/ide/src/syntax_highlighting/tests.rs | |||
@@ -544,10 +544,18 @@ impl Foo { | |||
544 | } | 544 | } |
545 | 545 | ||
546 | /// [`Foo`](Foo) is a struct | 546 | /// [`Foo`](Foo) is a struct |
547 | /// [`all_the_links`](all_the_links) is this function | 547 | /// This function is > [`all_the_links`](all_the_links) < |
548 | /// [`noop`](noop) is a macro below | 548 | /// [`noop`](noop) is a macro below |
549 | /// [`Item`] is a struct in the module [`module`] | ||
550 | /// | ||
551 | /// [`Item`]: module::Item | ||
552 | /// [mix_and_match]: ThisShouldntResolve | ||
549 | pub fn all_the_links() {} | 553 | pub fn all_the_links() {} |
550 | 554 | ||
555 | pub mod module { | ||
556 | pub struct Item; | ||
557 | } | ||
558 | |||
551 | /// ``` | 559 | /// ``` |
552 | /// noop!(1); | 560 | /// noop!(1); |
553 | /// ``` | 561 | /// ``` |