1 //! "Recursive" Syntax highlighting for code in doctests and fixtures.
6 use hir
::{InFile, Semantics}
;
8 active_parameter
::ActiveParameter
, base_db
::FileId
, defs
::Definition
,
9 documentation
::docs_with_rangemap
, rust_doc
::is_rust_fence
, SymbolKind
,
12 ast
::{self, AstNode, IsString, QuoteOffsets}
,
13 AstToken
, NodeOrToken
, SyntaxNode
, TextRange
, TextSize
,
17 doc_links
::{doc_attributes, extract_definitions_from_docs, resolve_doc_path_for_def}
,
18 syntax_highlighting
::{highlights::Highlights, injector::Injector, HighlightConfig}
,
19 Analysis
, HlMod
, HlRange
, HlTag
, RootDatabase
,
22 pub(super) fn ra_fixture(
24 sema
: &Semantics
<'_
, RootDatabase
>,
25 config
: HighlightConfig
,
26 literal
: &ast
::String
,
27 expanded
: &ast
::String
,
29 let active_parameter
= ActiveParameter
::at_token(sema
, expanded
.syntax().clone())?
;
30 if !active_parameter
.ident().map_or(false, |name
| name
.text().starts_with("ra_fixture")) {
33 let value
= literal
.value()?
;
35 if let Some(range
) = literal
.open_quote_text_range() {
36 hl
.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None }
)
39 let mut inj
= Injector
::default();
41 let mut text
= &*value
;
42 let mut offset
: TextSize
= 0.into
();
44 while !text
.is_empty() {
46 let idx
= text
.find(marker
).unwrap_or(text
.len());
47 let (chunk
, next
) = text
.split_at(idx
);
48 inj
.add(chunk
, TextRange
::at(offset
, TextSize
::of(chunk
)));
51 offset
+= TextSize
::of(chunk
);
53 if let Some(next
) = text
.strip_prefix(marker
) {
54 if let Some(range
) = literal
.map_range_up(TextRange
::at(offset
, TextSize
::of(marker
))) {
57 highlight
: HlTag
::Keyword
| HlMod
::Injected
,
64 let marker_len
= TextSize
::of(marker
);
69 let (analysis
, tmp_file_id
) = Analysis
::from_single_file(inj
.take_text());
71 for mut hl_range
in analysis
74 syntactic_name_ref_highlighting
: false,
78 specialize_punctuation
: config
.specialize_punctuation
,
79 specialize_operator
: config
.operator
,
80 inject_doc_comment
: config
.inject_doc_comment
,
81 macro_bang
: config
.macro_bang
,
87 for range
in inj
.map_range_up(hl_range
.range
) {
88 if let Some(range
) = literal
.map_range_up(range
) {
89 hl_range
.range
= range
;
90 hl_range
.highlight
|= HlMod
::Injected
;
96 if let Some(range
) = literal
.close_quote_text_range() {
97 hl
.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None }
)
103 const RUSTDOC_FENCE_LENGTH
: usize = 3;
104 const RUSTDOC_FENCES
: [&str; 2] = ["```", "~~~"];
106 /// Injection of syntax highlighting of doctests and intra doc links.
107 pub(super) fn doc_comment(
109 sema
: &Semantics
<'_
, RootDatabase
>,
110 config
: HighlightConfig
,
114 let (attributes
, def
) = match doc_attributes(sema
, node
) {
118 let src_file_id
= src_file_id
.into();
120 // Extract intra-doc links and emit highlights for them.
121 if let Some((docs
, doc_mapping
)) = docs_with_rangemap(sema
.db
, &attributes
) {
122 extract_definitions_from_docs(&docs
)
124 .filter_map(|(range
, link
, ns
)| {
125 doc_mapping
.map(range
).filter(|mapping
| mapping
.file_id
== src_file_id
).and_then(
126 |InFile { value: mapped_range, .. }
| {
127 Some(mapped_range
).zip(resolve_doc_path_for_def(sema
.db
, def
, &link
, ns
))
131 .for_each(|(range
, def
)| {
134 highlight
: module_def_to_hl_tag(def
)
135 | HlMod
::Documentation
137 | HlMod
::IntraDocLink
,
143 // Extract doc-test sources from the docs and calculate highlighting for them.
145 let mut inj
= Injector
::default();
146 inj
.add_unmapped("fn doctest() {\n");
148 let attrs_source_map
= attributes
.source_map(sema
.db
);
150 let mut is_codeblock
= false;
151 let mut is_doctest
= false;
153 let mut new_comments
= Vec
::new();
156 for attr
in attributes
.by_key("doc").attrs() {
157 let InFile { file_id, value: src }
= attrs_source_map
.source_of(attr
);
158 if file_id
!= src_file_id
{
161 let (line
, range
) = match &src
{
162 Either
::Left(it
) => {
163 string
= match find_doc_string_in_attr(attr
, it
) {
167 let text
= string
.text();
168 let text_range
= string
.syntax().text_range();
169 match string
.quote_offsets() {
170 Some(QuoteOffsets { contents, .. }
) => {
171 (&text
[contents
- text_range
.start()], contents
)
173 None
=> (text
, text_range
),
176 Either
::Right(comment
) => {
177 let value
= comment
.prefix().len();
178 let range
= comment
.syntax().text_range();
180 &comment
.text()[value
..],
181 TextRange
::new(range
.start() + TextSize
::try_from(value
).unwrap(), range
.end()),
186 let mut range_start
= range
.start();
187 for line
in line
.split('
\n'
) {
188 let line_len
= TextSize
::from(line
.len() as u32);
189 let prev_range_start
= {
190 let next_range_start
= range_start
+ line_len
+ TextSize
::from(1);
191 mem
::replace(&mut range_start
, next_range_start
)
193 let mut pos
= TextSize
::from(0);
195 match RUSTDOC_FENCES
.into_iter().find_map(|fence
| line
.find(fence
)) {
197 is_codeblock
= !is_codeblock
;
198 // Check whether code is rust by inspecting fence guards
199 let guards
= &line
[idx
+ RUSTDOC_FENCE_LENGTH
..];
200 let is_rust
= is_rust_fence(guards
);
201 is_doctest
= is_codeblock
&& is_rust
;
204 None
if !is_doctest
=> continue,
208 // whitespace after comment is ignored
209 if let Some(ws
) = line
[pos
.into()..].chars().next().filter(|c
| c
.is_whitespace()) {
210 pos
+= TextSize
::of(ws
);
212 // lines marked with `#` should be ignored in output, we skip the `#` char
213 if line
[pos
.into()..].starts_with('
#') {
214 pos
+= TextSize
::of('
#');
217 new_comments
.push(TextRange
::at(prev_range_start
, pos
));
218 inj
.add(&line
[pos
.into()..], TextRange
::new(pos
, line_len
) + prev_range_start
);
219 inj
.add_unmapped("\n");
223 if new_comments
.is_empty() {
224 return; // no need to run an analysis on an empty file
227 inj
.add_unmapped("\n}");
229 let (analysis
, tmp_file_id
) = Analysis
::from_single_file(inj
.take_text());
231 if let Ok(ranges
) = analysis
.with_db(|db
| {
235 syntactic_name_ref_highlighting
: true,
239 specialize_punctuation
: config
.specialize_punctuation
,
240 specialize_operator
: config
.operator
,
241 inject_doc_comment
: config
.inject_doc_comment
,
242 macro_bang
: config
.macro_bang
,
248 for HlRange { range, highlight, binding_hash }
in ranges
{
249 for range
in inj
.map_range_up(range
) {
250 hl
.add(HlRange { range, highlight: highlight | HlMod::Injected, binding_hash }
);
255 for range
in new_comments
{
258 highlight
: HlTag
::Comment
| HlMod
::Documentation
,
264 fn find_doc_string_in_attr(attr
: &hir
::Attr
, it
: &ast
::Attr
) -> Option
<ast
::String
> {
267 Some(ast
::Expr
::Literal(lit
)) => match lit
.kind() {
268 ast
::LiteralKind
::String(it
) => Some(it
),
271 // #[cfg_attr(..., doc = "", ...)]
273 // We gotta hunt the string token manually here
274 let text
= attr
.string_value()?
;
275 // FIXME: We just pick the first string literal that has the same text as the doc attribute
276 // This means technically we might highlight the wrong one
278 .descendants_with_tokens()
279 .filter_map(NodeOrToken
::into_token
)
280 .filter_map(ast
::String
::cast
)
282 string
.text().get(1..string
.text().len() - 1).map_or(false, |it
| it
== text
)
289 fn module_def_to_hl_tag(def
: Definition
) -> HlTag
{
290 let symbol
= match def
{
291 Definition
::Module(_
) | Definition
::ExternCrateDecl(_
) => SymbolKind
::Module
,
292 Definition
::Function(_
) => SymbolKind
::Function
,
293 Definition
::Adt(hir
::Adt
::Struct(_
)) => SymbolKind
::Struct
,
294 Definition
::Adt(hir
::Adt
::Enum(_
)) => SymbolKind
::Enum
,
295 Definition
::Adt(hir
::Adt
::Union(_
)) => SymbolKind
::Union
,
296 Definition
::Variant(_
) => SymbolKind
::Variant
,
297 Definition
::Const(_
) => SymbolKind
::Const
,
298 Definition
::Static(_
) => SymbolKind
::Static
,
299 Definition
::Trait(_
) => SymbolKind
::Trait
,
300 Definition
::TraitAlias(_
) => SymbolKind
::TraitAlias
,
301 Definition
::TypeAlias(_
) => SymbolKind
::TypeAlias
,
302 Definition
::BuiltinType(_
) => return HlTag
::BuiltinType
,
303 Definition
::Macro(_
) => SymbolKind
::Macro
,
304 Definition
::Field(_
) => SymbolKind
::Field
,
305 Definition
::SelfType(_
) => SymbolKind
::Impl
,
306 Definition
::Local(_
) => SymbolKind
::Local
,
307 Definition
::GenericParam(gp
) => match gp
{
308 hir
::GenericParam
::TypeParam(_
) => SymbolKind
::TypeParam
,
309 hir
::GenericParam
::ConstParam(_
) => SymbolKind
::ConstParam
,
310 hir
::GenericParam
::LifetimeParam(_
) => SymbolKind
::LifetimeParam
,
312 Definition
::Label(_
) => SymbolKind
::Label
,
313 Definition
::BuiltinAttr(_
) => SymbolKind
::BuiltinAttr
,
314 Definition
::ToolModule(_
) => SymbolKind
::ToolModule
,
315 Definition
::DeriveHelper(_
) => SymbolKind
::DeriveHelper
,
317 HlTag
::Symbol(symbol
)