Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide/src/syntax_highlighting/inject.rs')
| -rw-r--r-- | crates/ide/src/syntax_highlighting/inject.rs | 189 |
1 files changed, 131 insertions, 58 deletions
diff --git a/crates/ide/src/syntax_highlighting/inject.rs b/crates/ide/src/syntax_highlighting/inject.rs index 26d2bb5e02..7955f5ac0d 100644 --- a/crates/ide/src/syntax_highlighting/inject.rs +++ b/crates/ide/src/syntax_highlighting/inject.rs @@ -1,13 +1,16 @@ //! "Recursive" Syntax highlighting for code in doctests and fixtures. -use hir::{EditionedFileId, HirFileId, InFile, Semantics}; +use std::mem; + +use either::Either; +use hir::{EditionedFileId, HirFileId, InFile, Semantics, sym}; +use ide_db::range_mapper::RangeMapper; use ide_db::{ - SymbolKind, defs::Definition, documentation::Documentation, range_mapper::RangeMapper, - rust_doc::is_rust_fence, + SymbolKind, defs::Definition, documentation::docs_with_rangemap, rust_doc::is_rust_fence, }; use syntax::{ - SyntaxNode, TextRange, TextSize, - ast::{self, IsString}, + AstToken, NodeOrToken, SyntaxNode, TextRange, TextSize, + ast::{self, AstNode, IsString, QuoteOffsets}, }; use crate::{ @@ -93,79 +96,118 @@ pub(super) fn doc_comment( None => return, }; let src_file_id: HirFileId = src_file_id.into(); - let Some(docs) = attributes.hir_docs(sema.db) else { return }; // Extract intra-doc links and emit highlights for them. - extract_definitions_from_docs(&Documentation::new_borrowed(docs.docs())) - .into_iter() - .filter_map(|(range, link, ns)| { - docs.find_ast_range(range) - .filter(|(mapping, _)| mapping.file_id == src_file_id) - .and_then(|(InFile { value: mapped_range, .. }, is_inner)| { - Some(mapped_range) - .zip(resolve_doc_path_for_def(sema.db, def, &link, ns, is_inner)) + if let Some((docs, doc_mapping)) = docs_with_rangemap(sema.db, &attributes) { + extract_definitions_from_docs(&docs) + .into_iter() + .filter_map(|(range, link, ns)| { + doc_mapping + .map(range) + .filter(|(mapping, _)| mapping.file_id == src_file_id) + .and_then(|(InFile { value: mapped_range, .. }, attr_id)| { + Some(mapped_range).zip(resolve_doc_path_for_def( + sema.db, + def, + &link, + ns, + attr_id.is_inner_attr(), + )) + }) + }) + .for_each(|(range, def)| { + hl.add(HlRange { + range, + highlight: module_def_to_hl_tag(def) + | HlMod::Documentation + | HlMod::Injected + | HlMod::IntraDocLink, + binding_hash: None, }) - }) - .for_each(|(range, def)| { - hl.add(HlRange { - range, - highlight: module_def_to_hl_tag(def) - | HlMod::Documentation - | HlMod::Injected - | HlMod::IntraDocLink, - binding_hash: None, }) - }); + } // Extract doc-test sources from the docs and calculate highlighting for them. let mut inj = RangeMapper::default(); inj.add_unmapped("fn doctest() {\n"); + let attrs_source_map = attributes.source_map(sema.db); + let mut is_codeblock = false; let mut is_doctest = false; - let mut has_doctests = false; - - let mut docs_offset = TextSize::new(0); - for mut line in docs.docs().split('\n') { - let mut line_docs_offset = docs_offset; - docs_offset += TextSize::of(line) + TextSize::of("\n"); - - match RUSTDOC_FENCES.into_iter().find_map(|fence| line.find(fence)) { - Some(idx) => { - is_codeblock = !is_codeblock; - // Check whether code is rust by inspecting fence guards - let guards = &line[idx + RUSTDOC_FENCE_LENGTH..]; - let is_rust = is_rust_fence(guards); - is_doctest = is_codeblock && is_rust; - continue; - } - None if !is_doctest => continue, - None => (), - } - - // lines marked with `#` should be ignored in output, we skip the `#` char - if line.starts_with('#') { - line_docs_offset += TextSize::of("#"); - line = &line["#".len()..]; - } + let mut new_comments = Vec::new(); + let mut string; - let Some((InFile { file_id, value: mapped_range }, _)) = - docs.find_ast_range(TextRange::at(line_docs_offset, TextSize::of(line))) - else { - continue; - }; + for attr in attributes.by_key(sym::doc).attrs() { + let InFile { file_id, value: src } = attrs_source_map.source_of(attr); if file_id != src_file_id { continue; } + let (line, range) = match &src { + Either::Left(it) => { + string = match find_doc_string_in_attr(attr, it) { + Some(it) => it, + None => continue, + }; + let text = string.text(); + let text_range = string.syntax().text_range(); + match string.quote_offsets() { + Some(QuoteOffsets { contents, .. }) => { + (&text[contents - text_range.start()], contents) + } + None => (text, text_range), + } + } + Either::Right(comment) => { + let value = comment.prefix().len(); + let range = comment.syntax().text_range(); + ( + &comment.text()[value..], + TextRange::new(range.start() + TextSize::try_from(value).unwrap(), range.end()), + ) + } + }; + + let mut range_start = range.start(); + for line in line.split('\n') { + let line_len = TextSize::from(line.len() as u32); + let prev_range_start = { + let next_range_start = range_start + line_len + TextSize::from(1); + mem::replace(&mut range_start, next_range_start) + }; + let mut pos = TextSize::from(0); + + match RUSTDOC_FENCES.into_iter().find_map(|fence| line.find(fence)) { + Some(idx) => { + is_codeblock = !is_codeblock; + // Check whether code is rust by inspecting fence guards + let guards = &line[idx + RUSTDOC_FENCE_LENGTH..]; + let is_rust = is_rust_fence(guards); + is_doctest = is_codeblock && is_rust; + continue; + } + None if !is_doctest => continue, + None => (), + } - has_doctests = true; - inj.add(line, mapped_range); - inj.add_unmapped("\n"); + // whitespace after comment is ignored + if let Some(ws) = line[pos.into()..].chars().next().filter(|c| c.is_whitespace()) { + pos += TextSize::of(ws); + } + // lines marked with `#` should be ignored in output, we skip the `#` char + if line[pos.into()..].starts_with('#') { + pos += TextSize::of('#'); + } + + new_comments.push(TextRange::at(prev_range_start, pos)); + inj.add(&line[pos.into()..], TextRange::new(pos, line_len) + prev_range_start); + inj.add_unmapped("\n"); + } } - if !has_doctests { + if new_comments.is_empty() { return; // no need to run an analysis on an empty file } @@ -198,6 +240,37 @@ pub(super) fn doc_comment( } } } + + for range in new_comments { + hl.add(HlRange { + range, + highlight: HlTag::Comment | HlMod::Documentation, + binding_hash: None, + }); + } +} + +fn find_doc_string_in_attr(attr: &hir::Attr, it: &ast::Attr) -> Option<ast::String> { + match it.expr() { + // #[doc = lit] + Some(ast::Expr::Literal(lit)) => match lit.kind() { + ast::LiteralKind::String(it) => Some(it), + _ => None, + }, + // #[cfg_attr(..., doc = "", ...)] + None => { + // We gotta hunt the string token manually here + let text = attr.string_value()?.as_str(); + // FIXME: We just pick the first string literal that has the same text as the doc attribute + // This means technically we might highlight the wrong one + it.syntax() + .descendants_with_tokens() + .filter_map(NodeOrToken::into_token) + .filter_map(ast::String::cast) + .find(|string| string.text().get(1..string.text().len() - 1) == Some(text)) + } + _ => None, + } } fn module_def_to_hl_tag(def: Definition) -> HlTag { |