aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/syntax_highlighting/inject.rs
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2021-03-17 11:13:54 +0000
committerGitHub <[email protected]>2021-03-17 11:13:54 +0000
commit0fbfab3b45af7a02b1224bca4a53e9b8f6ec049e (patch)
tree6ba39c051e758d64c75948716d599a9a2a427ee3 /crates/ide/src/syntax_highlighting/inject.rs
parentf7fbea509f1e5f840e715c912ee38aa997d1bfbc (diff)
parentcdfb5c353f09138540ae66a2eb80a6a81802bbd6 (diff)
Merge #8059
8059: Move doc-comment highlight injection from AST to HIR r=matklad,jonas-schievink a=Veykril Fixes #5016 Co-authored-by: Lukas Wirth <[email protected]>
Diffstat (limited to 'crates/ide/src/syntax_highlighting/inject.rs')
-rw-r--r--crates/ide/src/syntax_highlighting/inject.rs135
1 files changed, 120 insertions, 15 deletions
diff --git a/crates/ide/src/syntax_highlighting/inject.rs b/crates/ide/src/syntax_highlighting/inject.rs
index 4f825523c..d57ce4027 100644
--- a/crates/ide/src/syntax_highlighting/inject.rs
+++ b/crates/ide/src/syntax_highlighting/inject.rs
@@ -1,8 +1,12 @@
1//! "Recursive" Syntax highlighting for code in doctests and fixtures. 1//! "Recursive" Syntax highlighting for code in doctests and fixtures.
2 2
3use hir::Semantics; 3use either::Either;
4use hir::{HasAttrs, Semantics};
4use ide_db::call_info::ActiveParameter; 5use ide_db::call_info::ActiveParameter;
5use syntax::{ast, AstToken, SyntaxNode, SyntaxToken, TextRange, TextSize}; 6use syntax::{
7 ast::{self, AstNode, AttrsOwner, DocCommentsOwner},
8 match_ast, AstToken, NodeOrToken, SyntaxNode, SyntaxToken, TextRange, TextSize,
9};
6 10
7use crate::{Analysis, HlMod, HlRange, HlTag, RootDatabase}; 11use crate::{Analysis, HlMod, HlRange, HlTag, RootDatabase};
8 12
@@ -81,16 +85,75 @@ const RUSTDOC_FENCE_TOKENS: &[&'static str] = &[
81 "edition2021", 85 "edition2021",
82]; 86];
83 87
88// Basically an owned dyn AttrsOwner without extra Boxing
89struct AttrsOwnerNode {
90 node: SyntaxNode,
91}
92
93impl AttrsOwnerNode {
94 fn new<N: DocCommentsOwner>(node: N) -> Self {
95 AttrsOwnerNode { node: node.syntax().clone() }
96 }
97}
98
99impl AttrsOwner for AttrsOwnerNode {}
100impl AstNode for AttrsOwnerNode {
101 fn can_cast(_: syntax::SyntaxKind) -> bool
102 where
103 Self: Sized,
104 {
105 false
106 }
107 fn cast(_: SyntaxNode) -> Option<Self>
108 where
109 Self: Sized,
110 {
111 None
112 }
113 fn syntax(&self) -> &SyntaxNode {
114 &self.node
115 }
116}
117
118fn doc_attributes<'node>(
119 sema: &Semantics<RootDatabase>,
120 node: &'node SyntaxNode,
121) -> Option<(AttrsOwnerNode, hir::Attrs)> {
122 match_ast! {
123 match node {
124 ast::SourceFile(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
125 ast::Fn(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
126 ast::Struct(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
127 ast::Union(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
128 ast::RecordField(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
129 ast::TupleField(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
130 ast::Enum(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
131 ast::Variant(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
132 ast::Trait(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
133 ast::Module(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
134 ast::Static(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
135 ast::Const(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
136 ast::TypeAlias(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
137 ast::Impl(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
138 ast::MacroRules(it) => sema.to_def(&it).map(|def| (AttrsOwnerNode::new(it), def.attrs(sema.db))),
139 // ast::MacroDef(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
140 // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
141 _ => return None
142 }
143 }
144}
145
84/// Injection of syntax highlighting of doctests. 146/// Injection of syntax highlighting of doctests.
85pub(super) fn doc_comment(hl: &mut Highlights, node: &SyntaxNode) { 147pub(super) fn doc_comment(hl: &mut Highlights, sema: &Semantics<RootDatabase>, node: &SyntaxNode) {
86 let doc_comments = node 148 let (owner, attributes) = match doc_attributes(sema, node) {
87 .children_with_tokens() 149 Some(it) => it,
88 .filter_map(|it| it.into_token().and_then(ast::Comment::cast)) 150 None => return,
89 .filter(|it| it.kind().doc.is_some()); 151 };
90 152
91 if !doc_comments.clone().any(|it| it.text().contains(RUSTDOC_FENCE)) { 153 if attributes.docs().map_or(true, |docs| !String::from(docs).contains(RUSTDOC_FENCE)) {
92 return; 154 return;
93 } 155 }
156 let attrs_source_map = attributes.source_map(&owner);
94 157
95 let mut inj = Injector::default(); 158 let mut inj = Injector::default();
96 inj.add_unmapped("fn doctest() {\n"); 159 inj.add_unmapped("fn doctest() {\n");
@@ -101,12 +164,33 @@ pub(super) fn doc_comment(hl: &mut Highlights, node: &SyntaxNode) {
101 // Replace the original, line-spanning comment ranges by new, only comment-prefix 164 // Replace the original, line-spanning comment ranges by new, only comment-prefix
102 // spanning comment ranges. 165 // spanning comment ranges.
103 let mut new_comments = Vec::new(); 166 let mut new_comments = Vec::new();
104 for comment in doc_comments { 167 let mut string;
105 match comment.text().find(RUSTDOC_FENCE) { 168 for attr in attributes.by_key("doc").attrs() {
169 let src = attrs_source_map.source_of(&attr);
170 let (line, range, prefix) = match &src {
171 Either::Left(it) => {
172 string = match find_doc_string_in_attr(attr, it) {
173 Some(it) => it,
174 None => continue,
175 };
176 let text_range = string.syntax().text_range();
177 let text_range = TextRange::new(
178 text_range.start() + TextSize::from(1),
179 text_range.end() - TextSize::from(1),
180 );
181 let text = string.text();
182 (&text[1..text.len() - 1], text_range, "")
183 }
184 Either::Right(comment) => {
185 (comment.text(), comment.syntax().text_range(), comment.prefix())
186 }
187 };
188
189 match line.find(RUSTDOC_FENCE) {
106 Some(idx) => { 190 Some(idx) => {
107 is_codeblock = !is_codeblock; 191 is_codeblock = !is_codeblock;
108 // Check whether code is rust by inspecting fence guards 192 // Check whether code is rust by inspecting fence guards
109 let guards = &comment.text()[idx + RUSTDOC_FENCE.len()..]; 193 let guards = &line[idx + RUSTDOC_FENCE.len()..];
110 let is_rust = 194 let is_rust =
111 guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim())); 195 guards.split(',').all(|sub| RUSTDOC_FENCE_TOKENS.contains(&sub.trim()));
112 is_doctest = is_codeblock && is_rust; 196 is_doctest = is_codeblock && is_rust;
@@ -116,10 +200,7 @@ pub(super) fn doc_comment(hl: &mut Highlights, node: &SyntaxNode) {
116 None => (), 200 None => (),
117 } 201 }
118 202
119 let line: &str = comment.text(); 203 let mut pos = TextSize::of(prefix);
120 let range = comment.syntax().text_range();
121
122 let mut pos = TextSize::of(comment.prefix());
123 // whitespace after comment is ignored 204 // whitespace after comment is ignored
124 if let Some(ws) = line[pos.into()..].chars().next().filter(|c| c.is_whitespace()) { 205 if let Some(ws) = line[pos.into()..].chars().next().filter(|c| c.is_whitespace()) {
125 pos += TextSize::of(ws); 206 pos += TextSize::of(ws);
@@ -156,3 +237,27 @@ pub(super) fn doc_comment(hl: &mut Highlights, node: &SyntaxNode) {
156 }); 237 });
157 } 238 }
158} 239}
240
241fn find_doc_string_in_attr(attr: &hir::Attr, it: &ast::Attr) -> Option<ast::String> {
242 match it.literal() {
243 // #[doc = lit]
244 Some(lit) => match lit.kind() {
245 ast::LiteralKind::String(it) => Some(it),
246 _ => None,
247 },
248 // #[cfg_attr(..., doc = "", ...)]
249 None => {
250 // We gotta hunt the string token manually here
251 let text = attr.string_value()?;
252 // FIXME: We just pick the first string literal that has the same text as the doc attribute
253 // This means technically we might highlight the wrong one
254 it.syntax()
255 .descendants_with_tokens()
256 .filter_map(NodeOrToken::into_token)
257 .filter_map(ast::String::cast)
258 .find(|string| {
259 string.text().get(1..string.text().len() - 1).map_or(false, |it| it == text)
260 })
261 }
262 }
263}