diff options
-rw-r--r-- | crates/hir_def/src/attr.rs | 95 | ||||
-rw-r--r-- | crates/ide/src/doc_links.rs | 132 | ||||
-rw-r--r-- | crates/ide/src/goto_definition.rs | 38 | ||||
-rw-r--r-- | crates/ide/src/hover.rs | 42 | ||||
-rw-r--r-- | crates/ide/src/syntax_highlighting/inject.rs | 90 | ||||
-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 |
7 files changed, 260 insertions, 157 deletions
diff --git a/crates/hir_def/src/attr.rs b/crates/hir_def/src/attr.rs index 442c5fb5b..ab77d924a 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 | convert::{TryFrom, TryInto}, | ||
5 | ops, | ||
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 | ||
@@ -452,6 +456,55 @@ impl AttrsWithOwner { | |||
452 | .collect(), | 456 | .collect(), |
453 | } | 457 | } |
454 | } | 458 | } |
459 | |||
460 | pub fn docs_with_rangemap( | ||
461 | &self, | ||
462 | db: &dyn DefDatabase, | ||
463 | ) -> Option<(Documentation, DocsRangeMap)> { | ||
464 | // FIXME: code duplication in `docs` above | ||
465 | let docs = self.by_key("doc").attrs().flat_map(|attr| match attr.input.as_ref()? { | ||
466 | AttrInput::Literal(s) => Some((s, attr.index)), | ||
467 | AttrInput::TokenTree(_) => None, | ||
468 | }); | ||
469 | let indent = docs | ||
470 | .clone() | ||
471 | .flat_map(|(s, _)| s.lines()) | ||
472 | .filter(|line| !line.chars().all(|c| c.is_whitespace())) | ||
473 | .map(|line| line.chars().take_while(|c| c.is_whitespace()).count()) | ||
474 | .min() | ||
475 | .unwrap_or(0); | ||
476 | let mut buf = String::new(); | ||
477 | let mut mapping = Vec::new(); | ||
478 | for (doc, idx) in docs { | ||
479 | // str::lines doesn't yield anything for the empty string | ||
480 | if !doc.is_empty() { | ||
481 | for line in doc.split('\n') { | ||
482 | let line = line.trim_end(); | ||
483 | let line_len = line.len(); | ||
484 | let (offset, line) = match line.char_indices().nth(indent) { | ||
485 | Some((offset, _)) => (offset, &line[offset..]), | ||
486 | None => (0, line), | ||
487 | }; | ||
488 | let buf_offset = buf.len(); | ||
489 | buf.push_str(line); | ||
490 | mapping.push(( | ||
491 | TextRange::new(buf_offset.try_into().ok()?, buf.len().try_into().ok()?), | ||
492 | idx, | ||
493 | TextRange::new(offset.try_into().ok()?, line_len.try_into().ok()?), | ||
494 | )); | ||
495 | buf.push('\n'); | ||
496 | } | ||
497 | } else { | ||
498 | buf.push('\n'); | ||
499 | } | ||
500 | } | ||
501 | buf.pop(); | ||
502 | if buf.is_empty() { | ||
503 | None | ||
504 | } else { | ||
505 | Some((Documentation(buf), DocsRangeMap { mapping, source: self.source_map(db).attrs })) | ||
506 | } | ||
507 | } | ||
455 | } | 508 | } |
456 | 509 | ||
457 | fn inner_attributes( | 510 | fn inner_attributes( |
@@ -508,6 +561,44 @@ impl AttrSourceMap { | |||
508 | } | 561 | } |
509 | } | 562 | } |
510 | 563 | ||
564 | /// A struct to map text ranges from [`Documentation`] back to TextRanges in the syntax tree. | ||
565 | pub struct DocsRangeMap { | ||
566 | source: Vec<InFile<Either<ast::Attr, ast::Comment>>>, | ||
567 | // (docstring-line-range, attr_index, attr-string-range) | ||
568 | // a mapping from the text range of a line of the [`Documentation`] to the attribute index and | ||
569 | // the original (untrimmed) syntax doc line | ||
570 | mapping: Vec<(TextRange, u32, TextRange)>, | ||
571 | } | ||
572 | |||
573 | impl DocsRangeMap { | ||
574 | pub fn map(&self, range: TextRange) -> Option<InFile<TextRange>> { | ||
575 | let found = self.mapping.binary_search_by(|(probe, ..)| probe.ordering(range)).ok()?; | ||
576 | let (line_docs_range, idx, original_line_src_range) = self.mapping[found].clone(); | ||
577 | if !line_docs_range.contains_range(range) { | ||
578 | return None; | ||
579 | } | ||
580 | |||
581 | let relative_range = range - line_docs_range.start(); | ||
582 | |||
583 | let &InFile { file_id, value: ref source } = &self.source[idx as usize]; | ||
584 | match source { | ||
585 | Either::Left(_) => None, // FIXME, figure out a nice way to handle doc attributes here | ||
586 | // as well as for whats done in syntax highlight doc injection | ||
587 | Either::Right(comment) => { | ||
588 | let text_range = comment.syntax().text_range(); | ||
589 | let range = TextRange::at( | ||
590 | text_range.start() | ||
591 | + TextSize::try_from(comment.prefix().len()).ok()? | ||
592 | + original_line_src_range.start() | ||
593 | + relative_range.start(), | ||
594 | text_range.len().min(range.len()), | ||
595 | ); | ||
596 | Some(InFile { file_id, value: range }) | ||
597 | } | ||
598 | } | ||
599 | } | ||
600 | } | ||
601 | |||
511 | #[derive(Debug, Clone, PartialEq, Eq)] | 602 | #[derive(Debug, Clone, PartialEq, Eq)] |
512 | pub struct Attr { | 603 | pub struct Attr { |
513 | index: u32, | 604 | index: u32, |
diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs index 67e2e5a1c..c5dc14a23 100644 --- a/crates/ide/src/doc_links.rs +++ b/crates/ide/src/doc_links.rs | |||
@@ -1,6 +1,9 @@ | |||
1 | //! Extracts, resolves and rewrites links and intra-doc links in markdown documentation. | 1 | //! Extracts, resolves and rewrites links and intra-doc links in markdown documentation. |
2 | 2 | ||
3 | use std::{convert::TryFrom, iter::once, ops::Range}; | 3 | use std::{ |
4 | convert::{TryFrom, TryInto}, | ||
5 | iter::once, | ||
6 | }; | ||
4 | 7 | ||
5 | use itertools::Itertools; | 8 | use itertools::Itertools; |
6 | use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag}; | 9 | use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag}; |
@@ -16,8 +19,7 @@ use ide_db::{ | |||
16 | RootDatabase, | 19 | RootDatabase, |
17 | }; | 20 | }; |
18 | use syntax::{ | 21 | use syntax::{ |
19 | ast, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxNode, SyntaxToken, TextRange, TextSize, | 22 | ast, match_ast, AstNode, SyntaxKind::*, SyntaxNode, SyntaxToken, TextRange, TokenAtOffset, T, |
20 | TokenAtOffset, T, | ||
21 | }; | 23 | }; |
22 | 24 | ||
23 | use crate::{FilePosition, Semantics}; | 25 | use crate::{FilePosition, Semantics}; |
@@ -26,12 +28,7 @@ pub(crate) type DocumentationLink = String; | |||
26 | 28 | ||
27 | /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) | 29 | /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) |
28 | pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String { | 30 | pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String { |
29 | let mut cb = |link: BrokenLink| { | 31 | let mut cb = broken_link_clone_cb; |
30 | Some(( | ||
31 | /*url*/ link.reference.to_owned().into(), | ||
32 | /*title*/ link.reference.to_owned().into(), | ||
33 | )) | ||
34 | }; | ||
35 | let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb)); | 32 | let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb)); |
36 | 33 | ||
37 | let doc = map_links(doc, |target, title: &str| { | 34 | let doc = map_links(doc, |target, title: &str| { |
@@ -123,74 +120,27 @@ pub(crate) fn external_docs( | |||
123 | /// Extracts all links from a given markdown text. | 120 | /// Extracts all links from a given markdown text. |
124 | pub(crate) fn extract_definitions_from_markdown( | 121 | pub(crate) fn extract_definitions_from_markdown( |
125 | markdown: &str, | 122 | markdown: &str, |
126 | ) -> Vec<(Range<usize>, String, Option<hir::Namespace>)> { | 123 | ) -> Vec<(TextRange, String, Option<hir::Namespace>)> { |
127 | let mut res = vec![]; | 124 | Parser::new_with_broken_link_callback( |
128 | let mut cb = |link: BrokenLink| { | 125 | markdown, |
129 | // These allocations are actually unnecessary but the lifetimes on BrokenLinkCallback are wrong | 126 | Options::empty(), |
130 | // this is fixed in the repo but not on the crates.io release yet | 127 | Some(&mut broken_link_clone_cb), |
131 | Some(( | 128 | ) |
132 | /*url*/ link.reference.to_owned().into(), | 129 | .into_offset_iter() |
133 | /*title*/ link.reference.to_owned().into(), | 130 | .filter_map(|(event, range)| { |
134 | )) | ||
135 | }; | ||
136 | let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb)); | ||
137 | for (event, range) in doc.into_offset_iter() { | ||
138 | if let Event::Start(Tag::Link(_, target, title)) = event { | 131 | if let Event::Start(Tag::Link(_, target, title)) = event { |
139 | let link = if target.is_empty() { title } else { target }; | 132 | let link = if target.is_empty() { title } else { target }; |
140 | let (link, ns) = parse_intra_doc_link(&link); | 133 | let (link, ns) = parse_intra_doc_link(&link); |
141 | res.push((range, link.to_string(), ns)); | 134 | Some(( |
142 | } | 135 | TextRange::new(range.start.try_into().ok()?, range.end.try_into().ok()?), |
143 | } | 136 | link.to_string(), |
144 | res | 137 | ns, |
145 | } | 138 | )) |
146 | 139 | } else { | |
147 | /// Extracts a link from a comment at the given position returning the spanning range, link and | 140 | None |
148 | /// optionally it's namespace. | ||
149 | pub(crate) fn extract_positioned_link_from_comment( | ||
150 | position: TextSize, | ||
151 | comment: &ast::Comment, | ||
152 | ) -> Option<(TextRange, String, Option<hir::Namespace>)> { | ||
153 | let doc_comment = comment.doc_comment()?; | ||
154 | let comment_start = | ||
155 | comment.syntax().text_range().start() + TextSize::from(comment.prefix().len() as u32); | ||
156 | let def_links = extract_definitions_from_markdown(doc_comment); | ||
157 | let (range, def_link, ns) = | ||
158 | def_links.into_iter().find_map(|(Range { start, end }, def_link, ns)| { | ||
159 | let range = TextRange::at( | ||
160 | comment_start + TextSize::from(start as u32), | ||
161 | TextSize::from((end - start) as u32), | ||
162 | ); | ||
163 | range.contains(position).then(|| (range, def_link, ns)) | ||
164 | })?; | ||
165 | Some((range, def_link, ns)) | ||
166 | } | ||
167 | |||
168 | /// Turns a syntax node into it's [`Definition`] if it can hold docs. | ||
169 | pub(crate) fn doc_owner_to_def( | ||
170 | sema: &Semantics<RootDatabase>, | ||
171 | item: &SyntaxNode, | ||
172 | ) -> Option<Definition> { | ||
173 | let res: hir::ModuleDef = match_ast! { | ||
174 | match item { | ||
175 | ast::SourceFile(_it) => sema.scope(item).module()?.into(), | ||
176 | ast::Fn(it) => sema.to_def(&it)?.into(), | ||
177 | ast::Struct(it) => sema.to_def(&it)?.into(), | ||
178 | ast::Enum(it) => sema.to_def(&it)?.into(), | ||
179 | ast::Union(it) => sema.to_def(&it)?.into(), | ||
180 | ast::Trait(it) => sema.to_def(&it)?.into(), | ||
181 | ast::Const(it) => sema.to_def(&it)?.into(), | ||
182 | ast::Static(it) => sema.to_def(&it)?.into(), | ||
183 | ast::TypeAlias(it) => sema.to_def(&it)?.into(), | ||
184 | ast::Variant(it) => sema.to_def(&it)?.into(), | ||
185 | ast::Trait(it) => sema.to_def(&it)?.into(), | ||
186 | ast::Impl(it) => return sema.to_def(&it).map(Definition::SelfType), | ||
187 | ast::Macro(it) => return sema.to_def(&it).map(Definition::Macro), | ||
188 | ast::TupleField(it) => return sema.to_def(&it).map(Definition::Field), | ||
189 | ast::RecordField(it) => return sema.to_def(&it).map(Definition::Field), | ||
190 | _ => return None, | ||
191 | } | 141 | } |
192 | }; | 142 | }) |
193 | Some(Definition::ModuleDef(res)) | 143 | .collect() |
194 | } | 144 | } |
195 | 145 | ||
196 | pub(crate) fn resolve_doc_path_for_def( | 146 | pub(crate) fn resolve_doc_path_for_def( |
@@ -220,6 +170,42 @@ pub(crate) fn resolve_doc_path_for_def( | |||
220 | } | 170 | } |
221 | } | 171 | } |
222 | 172 | ||
173 | pub(crate) fn doc_attributes( | ||
174 | sema: &Semantics<RootDatabase>, | ||
175 | node: &SyntaxNode, | ||
176 | ) -> Option<(hir::AttrsWithOwner, Definition)> { | ||
177 | match_ast! { | ||
178 | match node { | ||
179 | ast::SourceFile(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))), | ||
180 | ast::Module(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))), | ||
181 | ast::Fn(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Function(def)))), | ||
182 | ast::Struct(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Struct(def))))), | ||
183 | ast::Union(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Union(def))))), | ||
184 | ast::Enum(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(def))))), | ||
185 | ast::Variant(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Variant(def)))), | ||
186 | ast::Trait(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Trait(def)))), | ||
187 | ast::Static(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Static(def)))), | ||
188 | ast::Const(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Const(def)))), | ||
189 | ast::TypeAlias(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::TypeAlias(def)))), | ||
190 | ast::Impl(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::SelfType(def))), | ||
191 | ast::RecordField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))), | ||
192 | ast::TupleField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))), | ||
193 | ast::Macro(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Macro(def))), | ||
194 | // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))), | ||
195 | _ => return None | ||
196 | } | ||
197 | } | ||
198 | } | ||
199 | |||
200 | fn broken_link_clone_cb<'a, 'b>(link: BrokenLink<'a>) -> Option<(CowStr<'b>, CowStr<'b>)> { | ||
201 | // These allocations are actually unnecessary but the lifetimes on BrokenLinkCallback are wrong | ||
202 | // this is fixed in the repo but not on the crates.io release yet | ||
203 | Some(( | ||
204 | /*url*/ link.reference.to_owned().into(), | ||
205 | /*title*/ link.reference.to_owned().into(), | ||
206 | )) | ||
207 | } | ||
208 | |||
223 | // FIXME: | 209 | // FIXME: |
224 | // BUG: For Option::Some | 210 | // BUG: For Option::Some |
225 | // Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some | 211 | // Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some |
diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 8574d1e3f..ca8ccb2da 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs | |||
@@ -1,5 +1,5 @@ | |||
1 | use either::Either; | 1 | use either::Either; |
2 | use hir::Semantics; | 2 | use hir::{InFile, Semantics}; |
3 | use ide_db::{ | 3 | use ide_db::{ |
4 | defs::{NameClass, NameRefClass}, | 4 | defs::{NameClass, NameRefClass}, |
5 | RootDatabase, | 5 | RootDatabase, |
@@ -8,7 +8,7 @@ use syntax::{ast, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxToken, Toke | |||
8 | 8 | ||
9 | use crate::{ | 9 | use crate::{ |
10 | display::TryToNav, | 10 | display::TryToNav, |
11 | doc_links::{doc_owner_to_def, extract_positioned_link_from_comment, resolve_doc_path_for_def}, | 11 | doc_links::{doc_attributes, extract_definitions_from_markdown, resolve_doc_path_for_def}, |
12 | FilePosition, NavigationTarget, RangeInfo, | 12 | FilePosition, NavigationTarget, RangeInfo, |
13 | }; | 13 | }; |
14 | 14 | ||
@@ -32,9 +32,16 @@ pub(crate) fn goto_definition( | |||
32 | let original_token = pick_best(file.token_at_offset(position.offset))?; | 32 | let original_token = pick_best(file.token_at_offset(position.offset))?; |
33 | let token = sema.descend_into_macros(original_token.clone()); | 33 | let token = sema.descend_into_macros(original_token.clone()); |
34 | let parent = token.parent()?; | 34 | let parent = token.parent()?; |
35 | if let Some(comment) = ast::Comment::cast(token) { | 35 | if let Some(_) = ast::Comment::cast(token) { |
36 | let (_, link, ns) = extract_positioned_link_from_comment(position.offset, &comment)?; | 36 | let (attributes, def) = doc_attributes(&sema, &parent)?; |
37 | let def = doc_owner_to_def(&sema, &parent)?; | 37 | |
38 | let (docs, doc_mapping) = attributes.docs_with_rangemap(db)?; | ||
39 | let (_, link, ns) = | ||
40 | extract_definitions_from_markdown(docs.as_str()).into_iter().find(|(range, ..)| { | ||
41 | doc_mapping.map(range.clone()).map_or(false, |InFile { file_id, value: range }| { | ||
42 | file_id == position.file_id.into() && range.contains(position.offset) | ||
43 | }) | ||
44 | })?; | ||
38 | let nav = resolve_doc_path_for_def(db, def, &link, ns)?.try_to_nav(db)?; | 45 | let nav = resolve_doc_path_for_def(db, def, &link, ns)?.try_to_nav(db)?; |
39 | return Some(RangeInfo::new(original_token.text_range(), vec![nav])); | 46 | return Some(RangeInfo::new(original_token.text_range(), vec![nav])); |
40 | } | 47 | } |
@@ -1160,4 +1167,25 @@ fn fn_macro() {} | |||
1160 | "#, | 1167 | "#, |
1161 | ) | 1168 | ) |
1162 | } | 1169 | } |
1170 | |||
1171 | #[test] | ||
1172 | fn goto_intra_doc_links() { | ||
1173 | check( | ||
1174 | r#" | ||
1175 | |||
1176 | pub mod theitem { | ||
1177 | /// This is the item. Cool! | ||
1178 | pub struct TheItem; | ||
1179 | //^^^^^^^ | ||
1180 | } | ||
1181 | |||
1182 | /// Gives you a [`TheItem$0`]. | ||
1183 | /// | ||
1184 | /// [`TheItem`]: theitem::TheItem | ||
1185 | pub fn gimme() -> theitem::TheItem { | ||
1186 | theitem::TheItem | ||
1187 | } | ||
1188 | "#, | ||
1189 | ); | ||
1190 | } | ||
1163 | } | 1191 | } |
diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 614433417..9de653739 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs | |||
@@ -1,6 +1,6 @@ | |||
1 | use either::Either; | 1 | use either::Either; |
2 | use hir::{ | 2 | use hir::{ |
3 | AsAssocItem, AssocItemContainer, GenericParam, HasAttrs, HasSource, HirDisplay, Module, | 3 | AsAssocItem, AssocItemContainer, GenericParam, HasAttrs, HasSource, HirDisplay, InFile, Module, |
4 | ModuleDef, Semantics, | 4 | ModuleDef, Semantics, |
5 | }; | 5 | }; |
6 | use ide_db::{ | 6 | use ide_db::{ |
@@ -16,8 +16,8 @@ use syntax::{ast, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxToken, Toke | |||
16 | use crate::{ | 16 | use crate::{ |
17 | display::{macro_label, TryToNav}, | 17 | display::{macro_label, TryToNav}, |
18 | doc_links::{ | 18 | doc_links::{ |
19 | doc_owner_to_def, extract_positioned_link_from_comment, remove_links, | 19 | doc_attributes, extract_definitions_from_markdown, remove_links, resolve_doc_path_for_def, |
20 | resolve_doc_path_for_def, rewrite_links, | 20 | rewrite_links, |
21 | }, | 21 | }, |
22 | markdown_remove::remove_markdown, | 22 | markdown_remove::remove_markdown, |
23 | markup::Markup, | 23 | markup::Markup, |
@@ -116,11 +116,19 @@ pub(crate) fn hover( | |||
116 | ), | 116 | ), |
117 | 117 | ||
118 | _ => ast::Comment::cast(token.clone()) | 118 | _ => ast::Comment::cast(token.clone()) |
119 | .and_then(|comment| { | 119 | .and_then(|_| { |
120 | let (attributes, def) = doc_attributes(&sema, &node)?; | ||
121 | let (docs, doc_mapping) = attributes.docs_with_rangemap(db)?; | ||
120 | let (idl_range, link, ns) = | 122 | let (idl_range, link, ns) = |
121 | extract_positioned_link_from_comment(position.offset, &comment)?; | 123 | extract_definitions_from_markdown(docs.as_str()).into_iter().find_map(|(range, link, ns)| { |
124 | let InFile { file_id, value: range } = doc_mapping.map(range.clone())?; | ||
125 | if file_id == position.file_id.into() && range.contains(position.offset) { | ||
126 | Some((range, link, ns)) | ||
127 | } else { | ||
128 | None | ||
129 | } | ||
130 | })?; | ||
122 | range = Some(idl_range); | 131 | range = Some(idl_range); |
123 | let def = doc_owner_to_def(&sema, &node)?; | ||
124 | resolve_doc_path_for_def(db, def, &link, ns) | 132 | resolve_doc_path_for_def(db, def, &link, ns) |
125 | }) | 133 | }) |
126 | .map(Definition::ModuleDef), | 134 | .map(Definition::ModuleDef), |
@@ -3814,23 +3822,33 @@ fn main() { | |||
3814 | fn hover_intra_doc_links() { | 3822 | fn hover_intra_doc_links() { |
3815 | check( | 3823 | check( |
3816 | r#" | 3824 | r#" |
3817 | /// This is the [`foo`](foo$0) function. | 3825 | |
3818 | fn foo() {} | 3826 | pub mod theitem { |
3827 | /// This is the item. Cool! | ||
3828 | pub struct TheItem; | ||
3829 | } | ||
3830 | |||
3831 | /// Gives you a [`TheItem$0`]. | ||
3832 | /// | ||
3833 | /// [`TheItem`]: theitem::TheItem | ||
3834 | pub fn gimme() -> theitem::TheItem { | ||
3835 | theitem::TheItem | ||
3836 | } | ||
3819 | "#, | 3837 | "#, |
3820 | expect![[r#" | 3838 | expect![[r#" |
3821 | *[`foo`](foo)* | 3839 | *[`TheItem`]* |
3822 | 3840 | ||
3823 | ```rust | 3841 | ```rust |
3824 | test | 3842 | test::theitem |
3825 | ``` | 3843 | ``` |
3826 | 3844 | ||
3827 | ```rust | 3845 | ```rust |
3828 | fn foo() | 3846 | pub struct TheItem |
3829 | ``` | 3847 | ``` |
3830 | 3848 | ||
3831 | --- | 3849 | --- |
3832 | 3850 | ||
3833 | This is the [`foo`](https://docs.rs/test/*/test/fn.foo.html) function. | 3851 | This is the item. Cool! |
3834 | "#]], | 3852 | "#]], |
3835 | ); | 3853 | ); |
3836 | } | 3854 | } |
diff --git a/crates/ide/src/syntax_highlighting/inject.rs b/crates/ide/src/syntax_highlighting/inject.rs index b62d43256..04fafd244 100644 --- a/crates/ide/src/syntax_highlighting/inject.rs +++ b/crates/ide/src/syntax_highlighting/inject.rs | |||
@@ -1,17 +1,17 @@ | |||
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::{InFile, Semantics}; |
7 | use ide_db::{call_info::ActiveParameter, defs::Definition, SymbolKind}; | 7 | use ide_db::{call_info::ActiveParameter, SymbolKind}; |
8 | use syntax::{ | 8 | use syntax::{ |
9 | ast::{self, AstNode}, | 9 | ast::{self, AstNode}, |
10 | match_ast, AstToken, NodeOrToken, SyntaxNode, SyntaxToken, TextRange, TextSize, | 10 | AstToken, NodeOrToken, SyntaxNode, SyntaxToken, TextRange, TextSize, |
11 | }; | 11 | }; |
12 | 12 | ||
13 | use crate::{ | 13 | use crate::{ |
14 | doc_links::{extract_definitions_from_markdown, resolve_doc_path_for_def}, | 14 | doc_links::{doc_attributes, extract_definitions_from_markdown, resolve_doc_path_for_def}, |
15 | Analysis, HlMod, HlRange, HlTag, RootDatabase, | 15 | Analysis, HlMod, HlRange, HlTag, RootDatabase, |
16 | }; | 16 | }; |
17 | 17 | ||
@@ -90,33 +90,6 @@ const RUSTDOC_FENCE_TOKENS: &[&'static str] = &[ | |||
90 | "edition2021", | 90 | "edition2021", |
91 | ]; | 91 | ]; |
92 | 92 | ||
93 | fn doc_attributes<'node>( | ||
94 | sema: &Semantics<RootDatabase>, | ||
95 | node: &'node SyntaxNode, | ||
96 | ) -> Option<(hir::AttrsWithOwner, Definition)> { | ||
97 | match_ast! { | ||
98 | match node { | ||
99 | ast::SourceFile(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))), | ||
100 | ast::Module(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))), | ||
101 | ast::Fn(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Function(def)))), | ||
102 | ast::Struct(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Struct(def))))), | ||
103 | ast::Union(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Union(def))))), | ||
104 | ast::Enum(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(def))))), | ||
105 | ast::Variant(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Variant(def)))), | ||
106 | ast::Trait(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Trait(def)))), | ||
107 | ast::Static(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Static(def)))), | ||
108 | ast::Const(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Const(def)))), | ||
109 | ast::TypeAlias(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::TypeAlias(def)))), | ||
110 | ast::Impl(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::SelfType(def))), | ||
111 | ast::RecordField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))), | ||
112 | ast::TupleField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))), | ||
113 | ast::Macro(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Macro(def))), | ||
114 | // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))), | ||
115 | _ => return None | ||
116 | } | ||
117 | } | ||
118 | } | ||
119 | |||
120 | /// Injection of syntax highlighting of doctests. | 93 | /// Injection of syntax highlighting of doctests. |
121 | pub(super) fn doc_comment( | 94 | pub(super) fn doc_comment( |
122 | hl: &mut Highlights, | 95 | hl: &mut Highlights, |
@@ -139,8 +112,28 @@ pub(super) fn doc_comment( | |||
139 | // Replace the original, line-spanning comment ranges by new, only comment-prefix | 112 | // Replace the original, line-spanning comment ranges by new, only comment-prefix |
140 | // spanning comment ranges. | 113 | // spanning comment ranges. |
141 | let mut new_comments = Vec::new(); | 114 | let mut new_comments = Vec::new(); |
142 | let mut intra_doc_links = Vec::new(); | ||
143 | let mut string; | 115 | let mut string; |
116 | |||
117 | if let Some((docs, doc_mapping)) = attributes.docs_with_rangemap(sema.db) { | ||
118 | extract_definitions_from_markdown(docs.as_str()) | ||
119 | .into_iter() | ||
120 | .filter_map(|(range, link, ns)| { | ||
121 | let def = resolve_doc_path_for_def(sema.db, def, &link, ns)?; | ||
122 | let InFile { file_id, value: range } = doc_mapping.map(range)?; | ||
123 | (file_id == node.file_id).then(|| (range, def)) | ||
124 | }) | ||
125 | .for_each(|(range, def)| { | ||
126 | hl.add(HlRange { | ||
127 | range, | ||
128 | highlight: module_def_to_hl_tag(def) | ||
129 | | HlMod::Documentation | ||
130 | | HlMod::Injected | ||
131 | | HlMod::IntraDocLink, | ||
132 | binding_hash: None, | ||
133 | }) | ||
134 | }); | ||
135 | } | ||
136 | |||
144 | for attr in attributes.by_key("doc").attrs() { | 137 | for attr in attributes.by_key("doc").attrs() { |
145 | let InFile { file_id, value: src } = attrs_source_map.source_of(&attr); | 138 | let InFile { file_id, value: src } = attrs_source_map.source_of(&attr); |
146 | if file_id != node.file_id { | 139 | if file_id != node.file_id { |
@@ -186,25 +179,7 @@ pub(super) fn doc_comment( | |||
186 | is_doctest = is_codeblock && is_rust; | 179 | is_doctest = is_codeblock && is_rust; |
187 | continue; | 180 | continue; |
188 | } | 181 | } |
189 | None if !is_doctest => { | 182 | 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 => (), | 183 | None => (), |
209 | } | 184 | } |
210 | 185 | ||
@@ -223,17 +198,6 @@ pub(super) fn doc_comment( | |||
223 | } | 198 | } |
224 | } | 199 | } |
225 | 200 | ||
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() { | 201 | if new_comments.is_empty() { |
238 | return; // no need to run an analysis on an empty file | 202 | return; // no need to run an analysis on an empty file |
239 | } | 203 | } |
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 | /// ``` |