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 | 301 |
1 files changed, 99 insertions, 202 deletions
diff --git a/crates/ide/src/syntax_highlighting/inject.rs b/crates/ide/src/syntax_highlighting/inject.rs index 4bb7308024..26d2bb5e02 100644 --- a/crates/ide/src/syntax_highlighting/inject.rs +++ b/crates/ide/src/syntax_highlighting/inject.rs @@ -1,100 +1,68 @@ //! "Recursive" Syntax highlighting for code in doctests and fixtures. -use std::mem; - -use either::Either; -use hir::{EditionedFileId, HirFileId, InFile, Semantics, sym}; +use hir::{EditionedFileId, HirFileId, InFile, Semantics}; use ide_db::{ - SymbolKind, active_parameter::ActiveParameter, base_db::salsa, defs::Definition, - documentation::docs_with_rangemap, rust_doc::is_rust_fence, + SymbolKind, defs::Definition, documentation::Documentation, range_mapper::RangeMapper, + rust_doc::is_rust_fence, }; use syntax::{ - AstToken, NodeOrToken, SyntaxNode, TextRange, TextSize, - ast::{self, AstNode, IsString, QuoteOffsets}, + SyntaxNode, TextRange, TextSize, + ast::{self, IsString}, }; use crate::{ Analysis, HlMod, HlRange, HlTag, RootDatabase, doc_links::{doc_attributes, extract_definitions_from_docs, resolve_doc_path_for_def}, - syntax_highlighting::{HighlightConfig, highlights::Highlights, injector::Injector}, + syntax_highlighting::{HighlightConfig, highlights::Highlights}, }; pub(super) fn ra_fixture( hl: &mut Highlights, sema: &Semantics<'_, RootDatabase>, - config: HighlightConfig, + config: &HighlightConfig<'_>, literal: &ast::String, expanded: &ast::String, ) -> Option<()> { - let active_parameter = - salsa::attach(sema.db, || ActiveParameter::at_token(sema, expanded.syntax().clone()))?; - let has_rust_fixture_attr = active_parameter.attrs().is_some_and(|attrs| { - attrs.filter_map(|attr| attr.as_simple_path()).any(|path| { - path.segments() - .zip(["rust_analyzer", "rust_fixture"]) - .all(|(seg, name)| seg.name_ref().map_or(false, |nr| nr.text() == name)) - }) - }); - if !has_rust_fixture_attr { - return None; - } - let value = literal.value().ok()?; + let (analysis, fixture_analysis) = Analysis::from_ra_fixture_with_on_cursor( + sema, + literal.clone(), + expanded, + config.minicore, + &mut |range| { + hl.add(HlRange { + range, + highlight: HlTag::Keyword | HlMod::Injected, + binding_hash: None, + }); + }, + )?; if let Some(range) = literal.open_quote_text_range() { hl.add(HlRange { range, highlight: HlTag::StringLiteral.into(), binding_hash: None }) } - let mut inj = Injector::default(); - - let mut text = &*value; - let mut offset: TextSize = 0.into(); - - while !text.is_empty() { - let marker = "$0"; - let idx = text.find(marker).unwrap_or(text.len()); - let (chunk, next) = text.split_at(idx); - inj.add(chunk, TextRange::at(offset, TextSize::of(chunk))); - - text = next; - offset += TextSize::of(chunk); - - if let Some(next) = text.strip_prefix(marker) { - if let Some(range) = literal.map_range_up(TextRange::at(offset, TextSize::of(marker))) { - hl.add(HlRange { - range, - highlight: HlTag::Keyword | HlMod::Injected, - binding_hash: None, - }); - } - - text = next; - - let marker_len = TextSize::of(marker); - offset += marker_len; - } - } - - let (analysis, tmp_file_id) = Analysis::from_single_file(inj.take_text()); - - for mut hl_range in analysis - .highlight( - HighlightConfig { - syntactic_name_ref_highlighting: false, - comments: true, - punctuation: true, - operator: true, - strings: true, - specialize_punctuation: config.specialize_punctuation, - specialize_operator: config.operator, - inject_doc_comment: config.inject_doc_comment, - macro_bang: config.macro_bang, - }, - tmp_file_id, - ) - .unwrap() - { - for range in inj.map_range_up(hl_range.range) { - if let Some(range) = literal.map_range_up(range) { + for tmp_file_id in fixture_analysis.files() { + for mut hl_range in analysis + .highlight( + HighlightConfig { + syntactic_name_ref_highlighting: false, + comments: true, + punctuation: true, + operator: true, + strings: true, + specialize_punctuation: config.specialize_punctuation, + specialize_operator: config.operator, + inject_doc_comment: config.inject_doc_comment, + macro_bang: config.macro_bang, + // What if there is a fixture inside a fixture? It's fixtures all the way down. + // (In fact, we have a fixture inside a fixture in our test suite!) + minicore: config.minicore, + }, + tmp_file_id, + ) + .unwrap() + { + for range in fixture_analysis.map_range_up(tmp_file_id, hl_range.range) { hl_range.range = range; hl_range.highlight |= HlMod::Injected; hl.add(hl_range); @@ -116,7 +84,7 @@ const RUSTDOC_FENCES: [&str; 2] = ["```", "~~~"]; pub(super) fn doc_comment( hl: &mut Highlights, sema: &Semantics<'_, RootDatabase>, - config: HighlightConfig, + config: &HighlightConfig<'_>, src_file_id: EditionedFileId, node: &SyntaxNode, ) { @@ -125,120 +93,79 @@ 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. - if let Some((docs, doc_mapping)) = docs_with_rangemap(sema.db, &attributes) { - salsa::attach(sema.db, || { - 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, - }) + 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)) }) + }) + .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 = Injector::default(); + 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 new_comments = Vec::new(); - let mut string; + 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()..]; + } - for attr in attributes.by_key(sym::doc).attrs() { - let InFile { file_id, value: src } = attrs_source_map.source_of(attr); + let Some((InFile { file_id, value: mapped_range }, _)) = + docs.find_ast_range(TextRange::at(line_docs_offset, TextSize::of(line))) + else { + continue; + }; 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 => (), - } - - // 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"); - } + has_doctests = true; + inj.add(line, mapped_range); + inj.add_unmapped("\n"); } - if new_comments.is_empty() { + if !has_doctests { return; // no need to run an analysis on an empty file } @@ -249,7 +176,7 @@ pub(super) fn doc_comment( if let Ok(ranges) = analysis.with_db(|db| { super::highlight( db, - HighlightConfig { + &HighlightConfig { syntactic_name_ref_highlighting: true, comments: true, punctuation: true, @@ -259,6 +186,7 @@ pub(super) fn doc_comment( specialize_operator: config.operator, inject_doc_comment: config.inject_doc_comment, macro_bang: config.macro_bang, + minicore: config.minicore, }, tmp_file_id, None, @@ -270,37 +198,6 @@ 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 { |