diff --git a/crates/hir-def/src/attr.rs b/crates/hir-def/src/attr.rs index a5db75a91e..c6454eb9ea 100644 --- a/crates/hir-def/src/attr.rs +++ b/crates/hir-def/src/attr.rs @@ -5,7 +5,7 @@ pub mod builtin; #[cfg(test)] mod tests; -use std::{hash::Hash, ops}; +use std::{hash::Hash, ops, slice::Iter as SliceIter}; use base_db::CrateId; use cfg::{CfgExpr, CfgOptions}; @@ -14,12 +14,11 @@ use hir_expand::{ attrs::{collect_attrs, Attr, AttrId, RawAttrs}, HirFileId, InFile, }; -use itertools::Itertools; use la_arena::{ArenaMap, Idx, RawIdx}; use mbe::DelimiterKind; use syntax::{ - ast::{self, HasAttrs, IsString}, - AstPtr, AstToken, SmolStr, TextRange, TextSize, + ast::{self, HasAttrs}, + AstPtr, SmolStr, }; use triomphe::Arc; @@ -33,26 +32,6 @@ use crate::{ LocalFieldId, Lookup, MacroId, VariantId, }; -/// Holds documentation -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Documentation(String); - -impl Documentation { - pub fn new(s: String) -> Self { - Documentation(s) - } - - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl From for String { - fn from(Documentation(string): Documentation) -> Self { - string - } -} - #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct Attrs(RawAttrs); @@ -221,33 +200,6 @@ impl Attrs { self.by_key("lang").string_value().and_then(|it| LangItem::from_str(it)) } - pub fn docs(&self) -> Option { - let docs = self.by_key("doc").attrs().filter_map(|attr| attr.string_value()); - let indent = doc_indent(self); - let mut buf = String::new(); - for doc in docs { - // str::lines doesn't yield anything for the empty string - if !doc.is_empty() { - buf.extend(Itertools::intersperse( - doc.lines().map(|line| { - line.char_indices() - .nth(indent) - .map_or(line, |(offset, _)| &line[offset..]) - .trim_end() - }), - "\n", - )); - } - buf.push('\n'); - } - buf.pop(); - if buf.is_empty() { - None - } else { - Some(Documentation(buf)) - } - } - pub fn has_doc_hidden(&self) -> bool { self.by_key("doc").tt_values().any(|tt| { tt.delimiter.kind == DelimiterKind::Parenthesis && @@ -299,7 +251,6 @@ impl Attrs { } } -use std::slice::Iter as SliceIter; #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] pub enum DocAtom { /// eg. `#[doc(hidden)]` @@ -313,7 +264,6 @@ pub enum DocAtom { // Adapted from `CfgExpr` parsing code #[derive(Debug, Clone, PartialEq, Eq, Hash)] -// #[cfg_attr(test, derive(derive_arbitrary::Arbitrary))] pub enum DocExpr { Invalid, /// eg. `#[doc(hidden)]`, `#[doc(alias = "x")]` @@ -574,62 +524,6 @@ impl AttrsWithOwner { AttrSourceMap::new(owner.as_ref().map(|node| node as &dyn HasAttrs)) } - - pub fn docs_with_rangemap( - &self, - db: &dyn DefDatabase, - ) -> Option<(Documentation, DocsRangeMap)> { - let docs = - self.by_key("doc").attrs().filter_map(|attr| attr.string_value().map(|s| (s, attr.id))); - let indent = doc_indent(self); - let mut buf = String::new(); - let mut mapping = Vec::new(); - for (doc, idx) in docs { - if !doc.is_empty() { - let mut base_offset = 0; - for raw_line in doc.split('\n') { - let line = raw_line.trim_end(); - let line_len = line.len(); - let (offset, line) = match line.char_indices().nth(indent) { - Some((offset, _)) => (offset, &line[offset..]), - None => (0, line), - }; - let buf_offset = buf.len(); - buf.push_str(line); - mapping.push(( - TextRange::new(buf_offset.try_into().ok()?, buf.len().try_into().ok()?), - idx, - TextRange::at( - (base_offset + offset).try_into().ok()?, - line_len.try_into().ok()?, - ), - )); - buf.push('\n'); - base_offset += raw_line.len() + 1; - } - } else { - buf.push('\n'); - } - } - buf.pop(); - if buf.is_empty() { - None - } else { - Some((Documentation(buf), DocsRangeMap { mapping, source_map: self.source_map(db) })) - } - } -} - -fn doc_indent(attrs: &Attrs) -> usize { - attrs - .by_key("doc") - .attrs() - .filter_map(|attr| attr.string_value()) - .flat_map(|s| s.lines()) - .filter(|line| !line.chars().all(|c| c.is_whitespace())) - .map(|line| line.chars().take_while(|c| c.is_whitespace()).count()) - .min() - .unwrap_or(0) } #[derive(Debug)] @@ -673,7 +567,7 @@ impl AttrSourceMap { self.source_of_id(attr.id) } - fn source_of_id(&self, id: AttrId) -> InFile<&Either> { + pub fn source_of_id(&self, id: AttrId) -> InFile<&Either> { let ast_idx = id.ast_index(); let file_id = match self.mod_def_site_file_id { Some((file_id, def_site_cut)) if def_site_cut <= ast_idx => file_id, @@ -687,69 +581,6 @@ impl AttrSourceMap { } } -/// A struct to map text ranges from [`Documentation`] back to TextRanges in the syntax tree. -#[derive(Debug)] -pub struct DocsRangeMap { - source_map: AttrSourceMap, - // (docstring-line-range, attr_index, attr-string-range) - // a mapping from the text range of a line of the [`Documentation`] to the attribute index and - // the original (untrimmed) syntax doc line - mapping: Vec<(TextRange, AttrId, TextRange)>, -} - -impl DocsRangeMap { - /// Maps a [`TextRange`] relative to the documentation string back to its AST range - pub fn map(&self, range: TextRange) -> Option> { - let found = self.mapping.binary_search_by(|(probe, ..)| probe.ordering(range)).ok()?; - let (line_docs_range, idx, original_line_src_range) = self.mapping[found]; - if !line_docs_range.contains_range(range) { - return None; - } - - let relative_range = range - line_docs_range.start(); - - let InFile { file_id, value: source } = self.source_map.source_of_id(idx); - match source { - Either::Left(attr) => { - let string = get_doc_string_in_attr(attr)?; - let text_range = string.open_quote_text_range()?; - let range = TextRange::at( - text_range.end() + original_line_src_range.start() + relative_range.start(), - string.syntax().text_range().len().min(range.len()), - ); - Some(InFile { file_id, value: range }) - } - Either::Right(comment) => { - let text_range = comment.syntax().text_range(); - let range = TextRange::at( - text_range.start() - + TextSize::try_from(comment.prefix().len()).ok()? - + original_line_src_range.start() - + relative_range.start(), - text_range.len().min(range.len()), - ); - Some(InFile { file_id, value: range }) - } - } - } -} - -fn get_doc_string_in_attr(it: &ast::Attr) -> Option { - match it.expr() { - // #[doc = lit] - Some(ast::Expr::Literal(lit)) => match lit.kind() { - ast::LiteralKind::String(it) => Some(it), - _ => None, - }, - // #[cfg_attr(..., doc = "", ...)] - None => { - // FIXME: See highlight injection for what to do here - None - } - _ => None, - } -} - #[derive(Debug, Clone, Copy)] pub struct AttrQuery<'attr> { attrs: &'attr Attrs, diff --git a/crates/hir/src/attrs.rs b/crates/hir/src/attrs.rs index 121e5a0a24..796490abd7 100644 --- a/crates/hir/src/attrs.rs +++ b/crates/hir/src/attrs.rs @@ -1,39 +1,27 @@ //! Attributes & documentation for hir types. use hir_def::{ - attr::{AttrsWithOwner, Documentation}, + attr::AttrsWithOwner, item_scope::ItemInNs, path::{ModPath, Path}, per_ns::Namespace, resolver::{HasResolver, Resolver, TypeNs}, - AssocItemId, AttrDefId, GenericParamId, ModuleDefId, + AssocItemId, AttrDefId, ModuleDefId, }; use hir_expand::{hygiene::Hygiene, name::Name}; use hir_ty::db::HirDatabase; use syntax::{ast, AstNode}; use crate::{ - Adt, AsAssocItem, AssocItem, BuiltinType, Const, ConstParam, Enum, ExternCrateDecl, Field, - Function, GenericParam, Impl, LifetimeParam, Macro, Module, ModuleDef, Static, Struct, Trait, - TraitAlias, TypeAlias, TypeParam, Union, Variant, VariantDef, + Adt, AsAssocItem, AssocItem, BuiltinType, Const, ConstParam, DocLinkDef, Enum, ExternCrateDecl, + Field, Function, GenericParam, Impl, LifetimeParam, Macro, Module, ModuleDef, Static, Struct, + Trait, TraitAlias, TypeAlias, TypeParam, Union, Variant, VariantDef, }; pub trait HasAttrs { fn attrs(self, db: &dyn HirDatabase) -> AttrsWithOwner; - fn docs(self, db: &dyn HirDatabase) -> Option; - fn resolve_doc_path( - self, - db: &dyn HirDatabase, - link: &str, - ns: Option, - ) -> Option; -} - -/// Subset of `ide_db::Definition` that doc links can resolve to. -pub enum DocLinkDef { - ModuleDef(ModuleDef), - Field(Field), - SelfType(Trait), + #[doc(hidden)] + fn attr_id(self) -> AttrDefId; } macro_rules! impl_has_attrs { @@ -43,18 +31,8 @@ macro_rules! impl_has_attrs { let def = AttrDefId::$def_id(self.into()); db.attrs_with_owner(def) } - fn docs(self, db: &dyn HirDatabase) -> Option { - let def = AttrDefId::$def_id(self.into()); - db.attrs(def).docs() - } - fn resolve_doc_path( - self, - db: &dyn HirDatabase, - link: &str, - ns: Option - ) -> Option { - let def = AttrDefId::$def_id(self.into()); - resolve_doc_path(db, def, link, ns) + fn attr_id(self) -> AttrDefId { + AttrDefId::$def_id(self.into()) } } )*}; @@ -74,6 +52,7 @@ impl_has_attrs![ (Module, ModuleId), (GenericParam, GenericParamId), (Impl, ImplId), + (ExternCrateDecl, ExternCrateId), ]; macro_rules! impl_has_attrs_enum { @@ -82,16 +61,8 @@ macro_rules! impl_has_attrs_enum { fn attrs(self, db: &dyn HirDatabase) -> AttrsWithOwner { $enum::$variant(self).attrs(db) } - fn docs(self, db: &dyn HirDatabase) -> Option { - $enum::$variant(self).docs(db) - } - fn resolve_doc_path( - self, - db: &dyn HirDatabase, - link: &str, - ns: Option - ) -> Option { - $enum::$variant(self).resolve_doc_path(db, link, ns) + fn attr_id(self) -> AttrDefId { + $enum::$variant(self).attr_id() } } )*}; @@ -108,70 +79,35 @@ impl HasAttrs for AssocItem { AssocItem::TypeAlias(it) => it.attrs(db), } } - - fn docs(self, db: &dyn HirDatabase) -> Option { + fn attr_id(self) -> AttrDefId { match self { - AssocItem::Function(it) => it.docs(db), - AssocItem::Const(it) => it.docs(db), - AssocItem::TypeAlias(it) => it.docs(db), + AssocItem::Function(it) => it.attr_id(), + AssocItem::Const(it) => it.attr_id(), + AssocItem::TypeAlias(it) => it.attr_id(), } } - - fn resolve_doc_path( - self, - db: &dyn HirDatabase, - link: &str, - ns: Option, - ) -> Option { - match self { - AssocItem::Function(it) => it.resolve_doc_path(db, link, ns), - AssocItem::Const(it) => it.resolve_doc_path(db, link, ns), - AssocItem::TypeAlias(it) => it.resolve_doc_path(db, link, ns), - } - } -} - -impl HasAttrs for ExternCrateDecl { - fn attrs(self, db: &dyn HirDatabase) -> AttrsWithOwner { - let def = AttrDefId::ExternCrateId(self.into()); - db.attrs_with_owner(def) - } - fn docs(self, db: &dyn HirDatabase) -> Option { - let crate_docs = self.resolved_crate(db)?.root_module().attrs(db).docs().map(String::from); - let def = AttrDefId::ExternCrateId(self.into()); - let decl_docs = db.attrs(def).docs().map(String::from); - match (decl_docs, crate_docs) { - (None, None) => None, - (Some(decl_docs), None) => Some(decl_docs), - (None, Some(crate_docs)) => Some(crate_docs), - (Some(mut decl_docs), Some(crate_docs)) => { - decl_docs.push('\n'); - decl_docs.push('\n'); - decl_docs += &crate_docs; - Some(decl_docs) - } - } - .map(Documentation::new) - } - fn resolve_doc_path( - self, - db: &dyn HirDatabase, - link: &str, - ns: Option, - ) -> Option { - let def = AttrDefId::ExternCrateId(self.into()); - resolve_doc_path(db, def, link, ns) - } } /// Resolves the item `link` points to in the scope of `def`. -fn resolve_doc_path( +pub fn resolve_doc_path_on( db: &dyn HirDatabase, - def: AttrDefId, + def: impl HasAttrs, link: &str, ns: Option, ) -> Option { - let resolver = match def { + // AttrDefId::FieldId(it) => it.parent.resolver(db.upcast()), + // AttrDefId::EnumVariantId(it) => it.parent.resolver(db.upcast()), + + resolve_doc_path_on_(db, link, def.attr_id(), ns) +} + +fn resolve_doc_path_on_( + db: &dyn HirDatabase, + link: &str, + attr_id: AttrDefId, + ns: Option, +) -> Option { + let resolver = match attr_id { AttrDefId::ModuleId(it) => it.resolver(db.upcast()), AttrDefId::FieldId(it) => it.parent.resolver(db.upcast()), AttrDefId::AdtId(it) => it.resolver(db.upcast()), @@ -187,12 +123,7 @@ fn resolve_doc_path( AttrDefId::UseId(it) => it.resolver(db.upcast()), AttrDefId::MacroId(it) => it.resolver(db.upcast()), AttrDefId::ExternCrateId(it) => it.resolver(db.upcast()), - AttrDefId::GenericParamId(it) => match it { - GenericParamId::TypeParamId(it) => it.parent(), - GenericParamId::ConstParamId(it) => it.parent(), - GenericParamId::LifetimeParamId(it) => it.parent, - } - .resolver(db.upcast()), + AttrDefId::GenericParamId(_) => return None, }; let mut modpath = modpath_from_str(db, link)?; diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index f113b772cf..91443238c6 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -88,7 +88,7 @@ use triomphe::Arc; use crate::db::{DefDatabase, HirDatabase}; pub use crate::{ - attrs::{DocLinkDef, HasAttrs}, + attrs::{resolve_doc_path_on, HasAttrs}, diagnostics::{ AnyDiagnostic, BreakOutsideOfLoop, CaseType, ExpectedFunction, InactiveCode, IncoherentImpl, IncorrectCase, InvalidDeriveTarget, MacroDefError, MacroError, @@ -115,7 +115,7 @@ pub use crate::{ pub use { cfg::{CfgAtom, CfgExpr, CfgOptions}, hir_def::{ - attr::{builtin::AttributeTemplate, Attrs, AttrsWithOwner, Documentation}, + attr::{builtin::AttributeTemplate, AttrSourceMap, Attrs, AttrsWithOwner}, data::adt::StructKind, find_path::PrefixKind, import_map, @@ -130,7 +130,7 @@ pub use { {AdtId, ModuleDefId}, }, hir_expand::{ - attrs::Attr, + attrs::{Attr, AttrId}, name::{known, Name}, ExpandResult, HirFileId, InFile, MacroFile, Origin, }, @@ -4839,3 +4839,10 @@ pub enum ItemContainer { ExternBlock(), Crate(CrateId), } + +/// Subset of `ide_db::Definition` that doc links can resolve to. +pub enum DocLinkDef { + ModuleDef(ModuleDef), + Field(Field), + SelfType(Trait), +} diff --git a/crates/ide-completion/src/completions/attribute/derive.rs b/crates/ide-completion/src/completions/attribute/derive.rs index 9447bc7db0..90dac1902a 100644 --- a/crates/ide-completion/src/completions/attribute/derive.rs +++ b/crates/ide-completion/src/completions/attribute/derive.rs @@ -1,6 +1,6 @@ //! Completion for derives -use hir::{HasAttrs, ScopeDef}; -use ide_db::SymbolKind; +use hir::ScopeDef; +use ide_db::{documentation::HasDocs, SymbolKind}; use itertools::Itertools; use syntax::SmolStr; diff --git a/crates/ide-completion/src/completions/attribute/lint.rs b/crates/ide-completion/src/completions/attribute/lint.rs index 6bc6f34ed4..f9dec53806 100644 --- a/crates/ide-completion/src/completions/attribute/lint.rs +++ b/crates/ide-completion/src/completions/attribute/lint.rs @@ -1,5 +1,5 @@ //! Completion for lints -use ide_db::{generated::lints::Lint, SymbolKind}; +use ide_db::{documentation::Documentation, generated::lints::Lint, SymbolKind}; use syntax::ast; use crate::{context::CompletionContext, item::CompletionItem, Completions}; @@ -55,7 +55,7 @@ pub(super) fn complete_lint( _ => name.to_owned(), }; let mut item = CompletionItem::new(SymbolKind::Attribute, ctx.source_range(), label); - item.documentation(hir::Documentation::new(description.to_owned())); + item.documentation(Documentation::new(description.to_owned())); item.add_to(acc, ctx.db) } } diff --git a/crates/ide-completion/src/completions/extern_crate.rs b/crates/ide-completion/src/completions/extern_crate.rs index 0d0e143f5f..f9cde44667 100644 --- a/crates/ide-completion/src/completions/extern_crate.rs +++ b/crates/ide-completion/src/completions/extern_crate.rs @@ -1,7 +1,7 @@ //! Completion for extern crates -use hir::{HasAttrs, Name}; -use ide_db::SymbolKind; +use hir::Name; +use ide_db::{documentation::HasDocs, SymbolKind}; use crate::{context::CompletionContext, CompletionItem, CompletionItemKind}; diff --git a/crates/ide-completion/src/completions/item_list/trait_impl.rs b/crates/ide-completion/src/completions/item_list/trait_impl.rs index 269e40e6ef..42dfbfc7d9 100644 --- a/crates/ide-completion/src/completions/item_list/trait_impl.rs +++ b/crates/ide-completion/src/completions/item_list/trait_impl.rs @@ -33,8 +33,8 @@ use hir::{self, HasAttrs}; use ide_db::{ - path_transform::PathTransform, syntax_helpers::insert_whitespace_into_node, - traits::get_missing_assoc_items, SymbolKind, + documentation::HasDocs, path_transform::PathTransform, + syntax_helpers::insert_whitespace_into_node, traits::get_missing_assoc_items, SymbolKind, }; use syntax::{ ast::{self, edit_in_place::AttrsOwnerEdit, HasTypeBounds}, diff --git a/crates/ide-completion/src/completions/postfix.rs b/crates/ide-completion/src/completions/postfix.rs index 2ffe123374..fc21bba456 100644 --- a/crates/ide-completion/src/completions/postfix.rs +++ b/crates/ide-completion/src/completions/postfix.rs @@ -2,8 +2,12 @@ mod format_like; -use hir::{Documentation, HasAttrs}; -use ide_db::{imports::insert_use::ImportScope, ty_filter::TryEnum, SnippetCap}; +use ide_db::{ + documentation::{Documentation, HasDocs}, + imports::insert_use::ImportScope, + ty_filter::TryEnum, + SnippetCap, +}; use syntax::{ ast::{self, make, AstNode, AstToken}, SyntaxKind::{BLOCK_EXPR, EXPR_STMT, FOR_EXPR, IF_EXPR, LOOP_EXPR, STMT_LIST, WHILE_EXPR}, diff --git a/crates/ide-completion/src/completions/snippet.rs b/crates/ide-completion/src/completions/snippet.rs index e9831a5b2a..3ff68b9788 100644 --- a/crates/ide-completion/src/completions/snippet.rs +++ b/crates/ide-completion/src/completions/snippet.rs @@ -1,7 +1,6 @@ //! This file provides snippet completions, like `pd` => `eprintln!(...)`. -use hir::Documentation; -use ide_db::{imports::insert_use::ImportScope, SnippetCap}; +use ide_db::{documentation::Documentation, imports::insert_use::ImportScope, SnippetCap}; use crate::{ context::{ExprCtx, ItemListKind, PathCompletionCtx, Qualified}, diff --git a/crates/ide-completion/src/item.rs b/crates/ide-completion/src/item.rs index 0309952c29..c45cc8d7b3 100644 --- a/crates/ide-completion/src/item.rs +++ b/crates/ide-completion/src/item.rs @@ -2,8 +2,11 @@ use std::fmt; -use hir::{Documentation, Mutability}; -use ide_db::{imports::import_assets::LocatedImport, RootDatabase, SnippetCap, SymbolKind}; +use hir::Mutability; +use ide_db::{ + documentation::Documentation, imports::import_assets::LocatedImport, RootDatabase, SnippetCap, + SymbolKind, +}; use itertools::Itertools; use smallvec::SmallVec; use stdx::{impl_from, never}; diff --git a/crates/ide-completion/src/render.rs b/crates/ide-completion/src/render.rs index 1953eb4795..dfe8fe7e2f 100644 --- a/crates/ide-completion/src/render.rs +++ b/crates/ide-completion/src/render.rs @@ -12,7 +12,10 @@ pub(crate) mod literal; use hir::{AsAssocItem, HasAttrs, HirDisplay, ScopeDef}; use ide_db::{ - helpers::item_name, imports::import_assets::LocatedImport, RootDatabase, SnippetCap, SymbolKind, + documentation::{Documentation, HasDocs}, + helpers::item_name, + imports::import_assets::LocatedImport, + RootDatabase, SnippetCap, SymbolKind, }; use syntax::{AstNode, SmolStr, SyntaxKind, TextRange}; @@ -114,7 +117,7 @@ impl<'a> RenderContext<'a> { } // FIXME: remove this - fn docs(&self, def: impl HasAttrs) -> Option { + fn docs(&self, def: impl HasDocs) -> Option { def.docs(self.db()) } } @@ -409,7 +412,7 @@ fn res_to_kind(resolution: ScopeDef) -> CompletionItemKind { } } -fn scope_def_docs(db: &RootDatabase, resolution: ScopeDef) -> Option { +fn scope_def_docs(db: &RootDatabase, resolution: ScopeDef) -> Option { use hir::ModuleDef::*; match resolution { ScopeDef::ModuleDef(Module(it)) => it.docs(db), diff --git a/crates/ide-completion/src/render/literal.rs b/crates/ide-completion/src/render/literal.rs index 728d236dff..b218502f7f 100644 --- a/crates/ide-completion/src/render/literal.rs +++ b/crates/ide-completion/src/render/literal.rs @@ -1,7 +1,10 @@ //! Renderer for `enum` variants. -use hir::{db::HirDatabase, Documentation, HasAttrs, StructKind}; -use ide_db::SymbolKind; +use hir::{db::HirDatabase, StructKind}; +use ide_db::{ + documentation::{Documentation, HasDocs}, + SymbolKind, +}; use crate::{ context::{CompletionContext, PathCompletionCtx, PathKind}, diff --git a/crates/ide-completion/src/render/macro_.rs b/crates/ide-completion/src/render/macro_.rs index ce7af1d340..68d175c2bd 100644 --- a/crates/ide-completion/src/render/macro_.rs +++ b/crates/ide-completion/src/render/macro_.rs @@ -1,7 +1,7 @@ //! Renderer for macro invocations. -use hir::{Documentation, HirDisplay}; -use ide_db::SymbolKind; +use hir::HirDisplay; +use ide_db::{documentation::Documentation, SymbolKind}; use syntax::SmolStr; use crate::{ diff --git a/crates/ide-completion/src/render/pattern.rs b/crates/ide-completion/src/render/pattern.rs index d06abc5e91..6f998119b7 100644 --- a/crates/ide-completion/src/render/pattern.rs +++ b/crates/ide-completion/src/render/pattern.rs @@ -1,7 +1,7 @@ //! Renderer for patterns. -use hir::{db::HirDatabase, HasAttrs, Name, StructKind}; -use ide_db::SnippetCap; +use hir::{db::HirDatabase, Name, StructKind}; +use ide_db::{documentation::HasDocs, SnippetCap}; use itertools::Itertools; use syntax::SmolStr; @@ -103,7 +103,7 @@ fn build_completion( label: SmolStr, lookup: SmolStr, pat: String, - def: impl HasAttrs + Copy, + def: impl HasDocs + Copy, adt_ty: hir::Type, // Missing in context of match statement completions is_variant_missing: bool, diff --git a/crates/ide-db/src/documentation.rs b/crates/ide-db/src/documentation.rs new file mode 100644 index 0000000000..26f3cd28a2 --- /dev/null +++ b/crates/ide-db/src/documentation.rs @@ -0,0 +1,281 @@ +//! Documentation attribute related utilties. +use either::Either; +use hir::{ + db::{DefDatabase, HirDatabase}, + resolve_doc_path_on, AttrId, AttrSourceMap, AttrsWithOwner, HasAttrs, InFile, +}; +use itertools::Itertools; +use syntax::{ + ast::{self, IsString}, + AstToken, +}; +use text_edit::{TextRange, TextSize}; + +/// Holds documentation +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Documentation(String); + +impl Documentation { + pub fn new(s: String) -> Self { + Documentation(s) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl From for String { + fn from(Documentation(string): Documentation) -> Self { + string + } +} + +pub trait HasDocs: HasAttrs { + fn docs(self, db: &dyn HirDatabase) -> Option; + fn resolve_doc_path( + self, + db: &dyn HirDatabase, + link: &str, + ns: Option, + ) -> Option; +} +/// A struct to map text ranges from [`Documentation`] back to TextRanges in the syntax tree. +#[derive(Debug)] +pub struct DocsRangeMap { + source_map: AttrSourceMap, + // (docstring-line-range, attr_index, attr-string-range) + // a mapping from the text range of a line of the [`Documentation`] to the attribute index and + // the original (untrimmed) syntax doc line + mapping: Vec<(TextRange, AttrId, TextRange)>, +} + +impl DocsRangeMap { + /// Maps a [`TextRange`] relative to the documentation string back to its AST range + pub fn map(&self, range: TextRange) -> Option> { + let found = self.mapping.binary_search_by(|(probe, ..)| probe.ordering(range)).ok()?; + let (line_docs_range, idx, original_line_src_range) = self.mapping[found]; + if !line_docs_range.contains_range(range) { + return None; + } + + let relative_range = range - line_docs_range.start(); + + let InFile { file_id, value: source } = self.source_map.source_of_id(idx); + match source { + Either::Left(attr) => { + let string = get_doc_string_in_attr(attr)?; + let text_range = string.open_quote_text_range()?; + let range = TextRange::at( + text_range.end() + original_line_src_range.start() + relative_range.start(), + string.syntax().text_range().len().min(range.len()), + ); + Some(InFile { file_id, value: range }) + } + Either::Right(comment) => { + let text_range = comment.syntax().text_range(); + let range = TextRange::at( + text_range.start() + + TextSize::try_from(comment.prefix().len()).ok()? + + original_line_src_range.start() + + relative_range.start(), + text_range.len().min(range.len()), + ); + Some(InFile { file_id, value: range }) + } + } + } +} + +pub fn docs_with_rangemap( + db: &dyn DefDatabase, + attrs: &AttrsWithOwner, +) -> Option<(Documentation, DocsRangeMap)> { + let docs = + attrs.by_key("doc").attrs().filter_map(|attr| attr.string_value().map(|s| (s, attr.id))); + let indent = doc_indent(attrs); + let mut buf = String::new(); + let mut mapping = Vec::new(); + for (doc, idx) in docs { + if !doc.is_empty() { + let mut base_offset = 0; + for raw_line in doc.split('\n') { + let line = raw_line.trim_end(); + let line_len = line.len(); + let (offset, line) = match line.char_indices().nth(indent) { + Some((offset, _)) => (offset, &line[offset..]), + None => (0, line), + }; + let buf_offset = buf.len(); + buf.push_str(line); + mapping.push(( + TextRange::new(buf_offset.try_into().ok()?, buf.len().try_into().ok()?), + idx, + TextRange::at( + (base_offset + offset).try_into().ok()?, + line_len.try_into().ok()?, + ), + )); + buf.push('\n'); + base_offset += raw_line.len() + 1; + } + } else { + buf.push('\n'); + } + } + buf.pop(); + if buf.is_empty() { + None + } else { + Some((Documentation(buf), DocsRangeMap { mapping, source_map: attrs.source_map(db) })) + } +} + +pub fn docs_from_attrs(attrs: &hir::Attrs) -> Option { + let docs = attrs.by_key("doc").attrs().filter_map(|attr| attr.string_value()); + let indent = doc_indent(attrs); + let mut buf = String::new(); + for doc in docs { + // str::lines doesn't yield anything for the empty string + if !doc.is_empty() { + buf.extend(Itertools::intersperse( + doc.lines().map(|line| { + line.char_indices() + .nth(indent) + .map_or(line, |(offset, _)| &line[offset..]) + .trim_end() + }), + "\n", + )); + } + buf.push('\n'); + } + buf.pop(); + if buf.is_empty() { + None + } else { + Some(buf) + } +} + +macro_rules! impl_has_docs { + ($($def:ident,)*) => {$( + impl HasDocs for hir::$def { + fn docs(self, db: &dyn HirDatabase) -> Option { + docs_from_attrs(&self.attrs(db)).map(Documentation) + } + fn resolve_doc_path( + self, + db: &dyn HirDatabase, + link: &str, + ns: Option + ) -> Option { + resolve_doc_path_on(db, self, link, ns) + } + } + )*}; +} + +impl_has_docs![ + Variant, Field, Static, Const, Trait, TraitAlias, TypeAlias, Macro, Function, Adt, Module, + Impl, +]; + +macro_rules! impl_has_docs_enum { + ($($variant:ident),* for $enum:ident) => {$( + impl HasDocs for hir::$variant { + fn docs(self, db: &dyn HirDatabase) -> Option { + hir::$enum::$variant(self).docs(db) + } + fn resolve_doc_path( + self, + db: &dyn HirDatabase, + link: &str, + ns: Option + ) -> Option { + hir::$enum::$variant(self).resolve_doc_path(db, link, ns) + } + } + )*}; +} + +impl_has_docs_enum![Struct, Union, Enum for Adt]; + +impl HasDocs for hir::AssocItem { + fn docs(self, db: &dyn HirDatabase) -> Option { + match self { + hir::AssocItem::Function(it) => it.docs(db), + hir::AssocItem::Const(it) => it.docs(db), + hir::AssocItem::TypeAlias(it) => it.docs(db), + } + } + + fn resolve_doc_path( + self, + db: &dyn HirDatabase, + link: &str, + ns: Option, + ) -> Option { + match self { + hir::AssocItem::Function(it) => it.resolve_doc_path(db, link, ns), + hir::AssocItem::Const(it) => it.resolve_doc_path(db, link, ns), + hir::AssocItem::TypeAlias(it) => it.resolve_doc_path(db, link, ns), + } + } +} + +impl HasDocs for hir::ExternCrateDecl { + fn docs(self, db: &dyn HirDatabase) -> Option { + let crate_docs = + docs_from_attrs(&self.resolved_crate(db)?.root_module().attrs(db)).map(String::from); + let decl_docs = docs_from_attrs(&self.attrs(db)).map(String::from); + match (decl_docs, crate_docs) { + (None, None) => None, + (Some(decl_docs), None) => Some(decl_docs), + (None, Some(crate_docs)) => Some(crate_docs), + (Some(mut decl_docs), Some(crate_docs)) => { + decl_docs.push('\n'); + decl_docs.push('\n'); + decl_docs += &crate_docs; + Some(decl_docs) + } + } + .map(Documentation::new) + } + fn resolve_doc_path( + self, + db: &dyn HirDatabase, + link: &str, + ns: Option, + ) -> Option { + resolve_doc_path_on(db, self, link, ns) + } +} + +fn get_doc_string_in_attr(it: &ast::Attr) -> Option { + match it.expr() { + // #[doc = lit] + Some(ast::Expr::Literal(lit)) => match lit.kind() { + ast::LiteralKind::String(it) => Some(it), + _ => None, + }, + // #[cfg_attr(..., doc = "", ...)] + None => { + // FIXME: See highlight injection for what to do here + None + } + _ => None, + } +} + +fn doc_indent(attrs: &hir::Attrs) -> usize { + attrs + .by_key("doc") + .attrs() + .filter_map(|attr| attr.string_value()) + .flat_map(|s| s.lines()) + .filter(|line| !line.chars().all(|c| c.is_whitespace())) + .map(|line| line.chars().take_while(|c| c.is_whitespace()).count()) + .min() + .unwrap_or(0) +} diff --git a/crates/ide-db/src/lib.rs b/crates/ide-db/src/lib.rs index ac3511ba47..226def4d52 100644 --- a/crates/ide-db/src/lib.rs +++ b/crates/ide-db/src/lib.rs @@ -22,6 +22,7 @@ pub mod symbol_index; pub mod traits; pub mod ty_filter; pub mod use_trivial_constructor; +pub mod documentation; pub mod imports { pub mod import_assets; diff --git a/crates/ide-db/src/rust_doc.rs b/crates/ide-db/src/rust_doc.rs index e27e23867a..ab2a250289 100644 --- a/crates/ide-db/src/rust_doc.rs +++ b/crates/ide-db/src/rust_doc.rs @@ -1,5 +1,7 @@ //! Rustdoc specific doc comment handling +use crate::documentation::Documentation; + // stripped down version of https://github.com/rust-lang/rust/blob/392ba2ba1a7d6c542d2459fb8133bebf62a4a423/src/librustdoc/html/markdown.rs#L810-L933 pub fn is_rust_fence(s: &str) -> bool { let mut seen_rust_tags = false; @@ -32,3 +34,170 @@ pub fn is_rust_fence(s: &str) -> bool { !seen_other_tags || seen_rust_tags } + +const RUSTDOC_FENCES: [&str; 2] = ["```", "~~~"]; + +pub fn format_docs(src: &Documentation) -> String { + format_docs_(src.as_str()) +} + +fn format_docs_(src: &str) -> String { + let mut processed_lines = Vec::new(); + let mut in_code_block = false; + let mut is_rust = false; + + for mut line in src.lines() { + if in_code_block && is_rust && code_line_ignored_by_rustdoc(line) { + continue; + } + + if let Some(header) = RUSTDOC_FENCES.into_iter().find_map(|fence| line.strip_prefix(fence)) + { + in_code_block ^= true; + + if in_code_block { + is_rust = is_rust_fence(header); + + if is_rust { + line = "```rust"; + } + } + } + + if in_code_block { + let trimmed = line.trim_start(); + if is_rust && trimmed.starts_with("##") { + line = &trimmed[1..]; + } + } + + processed_lines.push(line); + } + processed_lines.join("\n") +} + +fn code_line_ignored_by_rustdoc(line: &str) -> bool { + let trimmed = line.trim(); + trimmed == "#" || trimmed.starts_with("# ") || trimmed.starts_with("#\t") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_docs_adds_rust() { + let comment = "```\nfn some_rust() {}\n```"; + assert_eq!(format_docs_(comment), "```rust\nfn some_rust() {}\n```"); + } + + #[test] + fn test_format_docs_handles_plain_text() { + let comment = "```text\nthis is plain text\n```"; + assert_eq!(format_docs_(comment), "```text\nthis is plain text\n```"); + } + + #[test] + fn test_format_docs_handles_non_rust() { + let comment = "```sh\nsupposedly shell code\n```"; + assert_eq!(format_docs_(comment), "```sh\nsupposedly shell code\n```"); + } + + #[test] + fn test_format_docs_handles_rust_alias() { + let comment = "```ignore\nlet z = 55;\n```"; + assert_eq!(format_docs_(comment), "```rust\nlet z = 55;\n```"); + } + + #[test] + fn test_format_docs_handles_complex_code_block_attrs() { + let comment = "```rust,no_run\nlet z = 55;\n```"; + assert_eq!(format_docs_(comment), "```rust\nlet z = 55;\n```"); + } + + #[test] + fn test_format_docs_handles_error_codes() { + let comment = "```compile_fail,E0641\nlet b = 0 as *const _;\n```"; + assert_eq!(format_docs_(comment), "```rust\nlet b = 0 as *const _;\n```"); + } + + #[test] + fn test_format_docs_skips_comments_in_rust_block() { + let comment = + "```rust\n # skip1\n# skip2\n#stay1\nstay2\n#\n #\n # \n #\tskip3\n\t#\t\n```"; + assert_eq!(format_docs_(comment), "```rust\n#stay1\nstay2\n```"); + } + + #[test] + fn test_format_docs_does_not_skip_lines_if_plain_text() { + let comment = + "```text\n # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t\n```"; + assert_eq!( + format_docs_(comment), + "```text\n # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t\n```", + ); + } + + #[test] + fn test_format_docs_keeps_comments_outside_of_rust_block() { + let comment = " # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t"; + assert_eq!(format_docs_(comment), comment); + } + + #[test] + fn test_format_docs_preserves_newlines() { + let comment = "this\nis\nmultiline"; + assert_eq!(format_docs_(comment), comment); + } + + #[test] + fn test_code_blocks_in_comments_marked_as_rust() { + let comment = r#"```rust +fn main(){} +``` +Some comment. +``` +let a = 1; +```"#; + + assert_eq!( + format_docs_(comment), + "```rust\nfn main(){}\n```\nSome comment.\n```rust\nlet a = 1;\n```" + ); + } + + #[test] + fn test_code_blocks_in_comments_marked_as_text() { + let comment = r#"```text +filler +text +``` +Some comment. +``` +let a = 1; +```"#; + + assert_eq!( + format_docs_(comment), + "```text\nfiller\ntext\n```\nSome comment.\n```rust\nlet a = 1;\n```" + ); + } + + #[test] + fn test_format_docs_handles_escape_double_hashes() { + let comment = r#"```rust +let s = "foo +## bar # baz"; +```"#; + + assert_eq!(format_docs_(comment), "```rust\nlet s = \"foo\n# bar # baz\";\n```"); + } + + #[test] + fn test_format_docs_handles_double_hashes_non_rust() { + let comment = r#"```markdown +## A second-level heading +```"#; + assert_eq!(format_docs_(comment), "```markdown\n## A second-level heading\n```"); + } +} diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs index 901f7a2a79..37a1776221 100644 --- a/crates/ide/src/doc_links.rs +++ b/crates/ide/src/doc_links.rs @@ -16,6 +16,7 @@ use hir::{db::HirDatabase, Adt, AsAssocItem, AssocItem, AssocItemContainer, HasA use ide_db::{ base_db::{CrateOrigin, LangCrateOrigin, ReleaseChannel, SourceDatabase}, defs::{Definition, NameClass, NameRefClass}, + documentation::{docs_with_rangemap, Documentation, HasDocs}, helpers::pick_best_token, RootDatabase, }; @@ -171,7 +172,7 @@ pub(crate) fn external_docs( /// Extracts all links from a given markdown text returning the definition text range, link-text /// and the namespace if known. pub(crate) fn extract_definitions_from_docs( - docs: &hir::Documentation, + docs: &Documentation, ) -> Vec<(TextRange, String, Option)> { Parser::new_with_broken_link_callback( docs.as_str(), @@ -297,7 +298,7 @@ impl DocCommentToken { let abs_in_expansion_offset = token_start + relative_comment_offset + descended_prefix_len; let (attributes, def) = doc_attributes(sema, &node)?; - let (docs, doc_mapping) = attributes.docs_with_rangemap(sema.db)?; + let (docs, doc_mapping) = docs_with_rangemap(sema.db, &attributes)?; let (in_expansion_range, link, ns) = extract_definitions_from_docs(&docs).into_iter().find_map(|(range, link, ns)| { let mapped = doc_mapping.map(range)?; diff --git a/crates/ide/src/doc_links/tests.rs b/crates/ide/src/doc_links/tests.rs index 8036c77072..9ae70ae66f 100644 --- a/crates/ide/src/doc_links/tests.rs +++ b/crates/ide/src/doc_links/tests.rs @@ -1,10 +1,11 @@ use std::ffi::OsStr; use expect_test::{expect, Expect}; -use hir::{HasAttrs, Semantics}; +use hir::Semantics; use ide_db::{ base_db::{FilePosition, FileRange}, defs::Definition, + documentation::{Documentation, HasDocs}, RootDatabase, }; use itertools::Itertools; @@ -78,7 +79,7 @@ fn check_doc_links(ra_fixture: &str) { fn def_under_cursor( sema: &Semantics<'_, RootDatabase>, position: &FilePosition, -) -> (Definition, hir::Documentation) { +) -> (Definition, Documentation) { let (docs, def) = sema .parse(position.file_id) .syntax() @@ -96,7 +97,7 @@ fn def_under_cursor( fn node_to_def( sema: &Semantics<'_, RootDatabase>, node: &SyntaxNode, -) -> Option, Definition)>> { +) -> Option, Definition)>> { Some(match_ast! { match node { ast::SourceFile(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Module(def))), diff --git a/crates/ide/src/hover/render.rs b/crates/ide/src/hover/render.rs index a33a6ee181..f72ce37d1d 100644 --- a/crates/ide/src/hover/render.rs +++ b/crates/ide/src/hover/render.rs @@ -3,12 +3,13 @@ use std::fmt::Display; use either::Either; use hir::{ - Adt, AsAssocItem, AttributeTemplate, CaptureKind, HasAttrs, HasSource, HirDisplay, Layout, - LayoutError, Semantics, TypeInfo, + Adt, AsAssocItem, AttributeTemplate, CaptureKind, HasSource, HirDisplay, Layout, LayoutError, + Semantics, TypeInfo, }; use ide_db::{ base_db::SourceDatabase, defs::Definition, + documentation::{Documentation, HasDocs}, famous_defs::FamousDefs, generated::lints::{CLIPPY_LINTS, DEFAULT_LINTS, FEATURES}, syntax_helpers::insert_whitespace_into_node, @@ -470,7 +471,7 @@ pub(super) fn definition( Definition::SelfType(impl_def) => { impl_def.self_ty(db).as_adt().map(|adt| label_and_docs(db, adt))? } - Definition::GenericParam(it) => label_and_docs(db, it), + Definition::GenericParam(it) => (it.display(db).to_string(), None), Definition::Label(it) => return Some(Markup::fenced_block(&it.name(db).display(db))), Definition::ExternCrateDecl(it) => label_and_docs(db, it), // FIXME: We should be able to show more info about these @@ -616,9 +617,9 @@ fn render_builtin_attr(db: &RootDatabase, attr: hir::BuiltinAttr) -> Option(db: &RootDatabase, def: D) -> (String, Option) +fn label_and_docs(db: &RootDatabase, def: D) -> (String, Option) where - D: HasAttrs + HirDisplay, + D: HasDocs + HirDisplay, { let label = def.display(db).to_string(); let docs = def.docs(db); @@ -631,9 +632,9 @@ fn label_and_layout_info_and_docs( config: &HoverConfig, layout_extractor: E, layout_offset_extractor: E2, -) -> (String, Option) +) -> (String, Option) where - D: HasAttrs + HirDisplay, + D: HasDocs + HirDisplay, E: Fn(&D) -> Result, E2: Fn(&Layout) -> Option, { @@ -657,9 +658,9 @@ fn label_value_and_layout_info_and_docs( value_extractor: E, layout_extractor: E2, layout_tag_extractor: E3, -) -> (String, Option) +) -> (String, Option) where - D: HasAttrs + HirDisplay, + D: HasDocs + HirDisplay, E: Fn(&D) -> Option, E2: Fn(&D) -> Result, E3: Fn(&Layout) -> Option, @@ -686,9 +687,9 @@ fn label_value_and_docs( db: &RootDatabase, def: D, value_extractor: E, -) -> (String, Option) +) -> (String, Option) where - D: HasAttrs + HirDisplay, + D: HasDocs + HirDisplay, E: Fn(&D) -> Option, V: Display, { diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index c9cdbff7d7..ff0048903d 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs @@ -111,7 +111,7 @@ pub use crate::{ HighlightConfig, HlRange, }, }; -pub use hir::{Documentation, Semantics}; +pub use hir::Semantics; pub use ide_assists::{ Assist, AssistConfig, AssistId, AssistKind, AssistResolveStrategy, SingleResolve, }; @@ -124,6 +124,7 @@ pub use ide_db::{ Cancelled, Change, CrateGraph, CrateId, Edition, FileId, FilePosition, FileRange, SourceRoot, SourceRootId, }, + documentation::Documentation, label::Label, line_index::{LineCol, LineIndex}, search::{ReferenceCategory, SearchScope}, diff --git a/crates/ide/src/navigation_target.rs b/crates/ide/src/navigation_target.rs index 0740bfbc7b..32f211c6b2 100644 --- a/crates/ide/src/navigation_target.rs +++ b/crates/ide/src/navigation_target.rs @@ -4,14 +4,15 @@ use std::fmt; use either::Either; use hir::{ - symbols::FileSymbol, AssocItem, Documentation, FieldSource, HasAttrs, HasContainer, HasSource, - HirDisplay, HirFileId, InFile, LocalSource, ModuleSource, + symbols::FileSymbol, AssocItem, FieldSource, HasContainer, HasSource, HirDisplay, HirFileId, + InFile, LocalSource, ModuleSource, }; use ide_db::{ base_db::{FileId, FileRange}, - SymbolKind, + defs::Definition, + documentation::{Documentation, HasDocs}, + RootDatabase, SymbolKind, }; -use ide_db::{defs::Definition, RootDatabase}; use stdx::never; use syntax::{ ast::{self, HasName}, @@ -327,7 +328,7 @@ impl ToNavFromAst for hir::TraitAlias { impl TryToNav for D where - D: HasSource + ToNavFromAst + Copy + HasAttrs + HirDisplay, + D: HasSource + ToNavFromAst + Copy + HasDocs + HirDisplay, D::Ast: ast::HasName, { fn try_to_nav(&self, db: &RootDatabase) -> Option { diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs index 5f87a78551..2d528c6425 100644 --- a/crates/ide/src/runnables.rs +++ b/crates/ide/src/runnables.rs @@ -7,6 +7,7 @@ use ide_assists::utils::test_related_attribute; use ide_db::{ base_db::{FilePosition, FileRange}, defs::Definition, + documentation::docs_from_attrs, helpers::visit_file_defs, search::SearchScope, FxHashMap, FxHashSet, RootDatabase, SymbolKind, @@ -496,7 +497,7 @@ const RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE: &[&str] = &["", "rust", "should_panic", "edition2015", "edition2018", "edition2021"]; fn has_runnable_doc_test(attrs: &hir::Attrs) -> bool { - attrs.docs().map_or(false, |doc| { + docs_from_attrs(attrs).map_or(false, |doc| { let mut in_code_block = false; for line in String::from(doc).lines() { diff --git a/crates/ide/src/signature_help.rs b/crates/ide/src/signature_help.rs index 847ab3d21a..e020b52e17 100644 --- a/crates/ide/src/signature_help.rs +++ b/crates/ide/src/signature_help.rs @@ -4,12 +4,11 @@ use std::collections::BTreeSet; use either::Either; -use hir::{ - AssocItem, GenericParam, HasAttrs, HirDisplay, ModuleDef, PathResolution, Semantics, Trait, -}; +use hir::{AssocItem, GenericParam, HirDisplay, ModuleDef, PathResolution, Semantics, Trait}; use ide_db::{ active_parameter::{callable_for_node, generic_def_for_node}, base_db::FilePosition, + documentation::{Documentation, HasDocs}, FxIndexMap, }; use stdx::format_to; @@ -28,7 +27,7 @@ use crate::RootDatabase; /// edited. #[derive(Debug)] pub struct SignatureHelp { - pub doc: Option, + pub doc: Option, pub signature: String, pub active_parameter: Option, parameters: Vec, @@ -179,7 +178,7 @@ fn signature_help_for_call( let mut fn_params = None; match callable.kind() { hir::CallableKind::Function(func) => { - res.doc = func.docs(db).map(|it| it.into()); + res.doc = func.docs(db); format_to!(res.signature, "fn {}", func.name(db).display(db)); fn_params = Some(match callable.receiver_param(db) { Some(_self) => func.params_without_self(db), @@ -187,11 +186,11 @@ fn signature_help_for_call( }); } hir::CallableKind::TupleStruct(strukt) => { - res.doc = strukt.docs(db).map(|it| it.into()); + res.doc = strukt.docs(db); format_to!(res.signature, "struct {}", strukt.name(db).display(db)); } hir::CallableKind::TupleEnumVariant(variant) => { - res.doc = variant.docs(db).map(|it| it.into()); + res.doc = variant.docs(db); format_to!( res.signature, "enum {}::{}", @@ -265,38 +264,38 @@ fn signature_help_for_generics( let db = sema.db; match generics_def { hir::GenericDef::Function(it) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "fn {}", it.name(db).display(db)); } hir::GenericDef::Adt(hir::Adt::Enum(it)) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "enum {}", it.name(db).display(db)); } hir::GenericDef::Adt(hir::Adt::Struct(it)) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "struct {}", it.name(db).display(db)); } hir::GenericDef::Adt(hir::Adt::Union(it)) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "union {}", it.name(db).display(db)); } hir::GenericDef::Trait(it) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "trait {}", it.name(db).display(db)); } hir::GenericDef::TraitAlias(it) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "trait {}", it.name(db).display(db)); } hir::GenericDef::TypeAlias(it) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "type {}", it.name(db).display(db)); } hir::GenericDef::Variant(it) => { // In paths, generics of an enum can be specified *after* one of its variants. // eg. `None::` // We'll use the signature of the enum, but include the docs of the variant. - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); let enum_ = it.parent_enum(db); format_to!(res.signature, "enum {}", enum_.name(db).display(db)); generics_def = enum_.into(); diff --git a/crates/ide/src/syntax_highlighting/inject.rs b/crates/ide/src/syntax_highlighting/inject.rs index 2657a64148..71f4d07245 100644 --- a/crates/ide/src/syntax_highlighting/inject.rs +++ b/crates/ide/src/syntax_highlighting/inject.rs @@ -5,8 +5,8 @@ use std::mem; use either::Either; use hir::{InFile, Semantics}; use ide_db::{ - active_parameter::ActiveParameter, base_db::FileId, defs::Definition, rust_doc::is_rust_fence, - SymbolKind, + active_parameter::ActiveParameter, base_db::FileId, defs::Definition, + documentation::docs_with_rangemap, rust_doc::is_rust_fence, SymbolKind, }; use syntax::{ ast::{self, AstNode, IsString, QuoteOffsets}, @@ -118,7 +118,7 @@ pub(super) fn doc_comment( let src_file_id = src_file_id.into(); // Extract intra-doc links and emit highlights for them. - if let Some((docs, doc_mapping)) = attributes.docs_with_rangemap(sema.db) { + if let Some((docs, doc_mapping)) = docs_with_rangemap(sema.db, &attributes) { extract_definitions_from_docs(&docs) .into_iter() .filter_map(|(range, link, ns)| { diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index 22a7dd1548..8b6b461856 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -12,7 +12,10 @@ use ide_db::base_db::{CrateId, FileLoader, ProcMacroPaths, SourceDatabase}; use load_cargo::SourceRootConfig; use lsp_types::{SemanticTokens, Url}; use nohash_hasher::IntMap; -use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard}; +use parking_lot::{ + MappedRwLockReadGuard, Mutex, RwLock, RwLockReadGuard, RwLockUpgradableReadGuard, + RwLockWriteGuard, +}; use proc_macro_api::ProcMacroServer; use project_model::{CargoWorkspace, ProjectWorkspace, Target, WorkspaceBuildScripts}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -438,12 +441,16 @@ impl Drop for GlobalState { } impl GlobalStateSnapshot { + fn vfs_read(&self) -> MappedRwLockReadGuard<'_, vfs::Vfs> { + RwLockReadGuard::map(self.vfs.read(), |(it, _)| it) + } + pub(crate) fn url_to_file_id(&self, url: &Url) -> anyhow::Result { - url_to_file_id(&self.vfs.read().0, url) + url_to_file_id(&self.vfs_read(), url) } pub(crate) fn file_id_to_url(&self, id: FileId) -> Url { - file_id_to_url(&self.vfs.read().0, id) + file_id_to_url(&self.vfs_read(), id) } pub(crate) fn file_line_index(&self, file_id: FileId) -> Cancellable { @@ -459,7 +466,7 @@ impl GlobalStateSnapshot { } pub(crate) fn anchored_path(&self, path: &AnchoredPathBuf) -> Url { - let mut base = self.vfs.read().0.file_path(path.anchor); + let mut base = self.vfs_read().file_path(path.anchor); base.pop(); let path = base.join(&path.path).unwrap(); let path = path.as_path().unwrap(); @@ -467,7 +474,7 @@ impl GlobalStateSnapshot { } pub(crate) fn file_id_to_file_path(&self, file_id: FileId) -> vfs::VfsPath { - self.vfs.read().0.file_path(file_id) + self.vfs_read().file_path(file_id) } pub(crate) fn cargo_target_for_crate_root( @@ -475,7 +482,7 @@ impl GlobalStateSnapshot { crate_id: CrateId, ) -> Option<(&CargoWorkspace, Target)> { let file_id = self.analysis.crate_root(crate_id).ok()?; - let path = self.vfs.read().0.file_path(file_id); + let path = self.vfs_read().file_path(file_id); let path = path.as_path()?; self.workspaces.iter().find_map(|ws| match ws { ProjectWorkspace::Cargo { cargo, .. } => { @@ -487,7 +494,7 @@ impl GlobalStateSnapshot { } pub(crate) fn vfs_memory_usage(&self) -> usize { - self.vfs.read().0.memory_usage() + self.vfs_read().memory_usage() } } diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs index 04ac77b1f6..6c62577f69 100644 --- a/crates/rust-analyzer/src/lib.rs +++ b/crates/rust-analyzer/src/lib.rs @@ -26,7 +26,6 @@ mod dispatch; mod global_state; mod line_index; mod main_loop; -mod markdown; mod mem_docs; mod op_queue; mod reload; diff --git a/crates/rust-analyzer/src/lsp/to_proto.rs b/crates/rust-analyzer/src/lsp/to_proto.rs index daa7f5fe19..57f4602254 100644 --- a/crates/rust-analyzer/src/lsp/to_proto.rs +++ b/crates/rust-analyzer/src/lsp/to_proto.rs @@ -13,6 +13,7 @@ use ide::{ RenameError, Runnable, Severity, SignatureHelp, SnippetEdit, SourceChange, StructureNodeKind, SymbolKind, TextEdit, TextRange, TextSize, }; +use ide_db::rust_doc::format_docs; use itertools::Itertools; use serde_json::to_value; use vfs::AbsPath; @@ -105,7 +106,7 @@ pub(crate) fn diagnostic_severity(severity: Severity) -> lsp_types::DiagnosticSe } pub(crate) fn documentation(documentation: Documentation) -> lsp_types::Documentation { - let value = crate::markdown::format_docs(documentation.as_str()); + let value = format_docs(&documentation); let markup_content = lsp_types::MarkupContent { kind: lsp_types::MarkupKind::Markdown, value }; lsp_types::Documentation::MarkupContent(markup_content) } @@ -416,7 +417,7 @@ pub(crate) fn signature_help( let documentation = call_info.doc.filter(|_| config.docs).map(|doc| { lsp_types::Documentation::MarkupContent(lsp_types::MarkupContent { kind: lsp_types::MarkupKind::Markdown, - value: crate::markdown::format_docs(&doc), + value: format_docs(&doc), }) }); @@ -1531,7 +1532,7 @@ pub(crate) fn markup_content( ide::HoverDocFormat::Markdown => lsp_types::MarkupKind::Markdown, ide::HoverDocFormat::PlainText => lsp_types::MarkupKind::PlainText, }; - let value = crate::markdown::format_docs(markup.as_str()); + let value = format_docs(&Documentation::new(markup.into())); lsp_types::MarkupContent { kind, value } } diff --git a/crates/rust-analyzer/src/markdown.rs b/crates/rust-analyzer/src/markdown.rs deleted file mode 100644 index 3201b0ed52..0000000000 --- a/crates/rust-analyzer/src/markdown.rs +++ /dev/null @@ -1,166 +0,0 @@ -//! Transforms markdown -use ide_db::rust_doc::is_rust_fence; - -const RUSTDOC_FENCES: [&str; 2] = ["```", "~~~"]; - -// FIXME: why is this in this crate? -pub(crate) fn format_docs(src: &str) -> String { - let mut processed_lines = Vec::new(); - let mut in_code_block = false; - let mut is_rust = false; - - for mut line in src.lines() { - if in_code_block && is_rust && code_line_ignored_by_rustdoc(line) { - continue; - } - - if let Some(header) = RUSTDOC_FENCES.into_iter().find_map(|fence| line.strip_prefix(fence)) - { - in_code_block ^= true; - - if in_code_block { - is_rust = is_rust_fence(header); - - if is_rust { - line = "```rust"; - } - } - } - - if in_code_block { - let trimmed = line.trim_start(); - if is_rust && trimmed.starts_with("##") { - line = &trimmed[1..]; - } - } - - processed_lines.push(line); - } - processed_lines.join("\n") -} - -fn code_line_ignored_by_rustdoc(line: &str) -> bool { - let trimmed = line.trim(); - trimmed == "#" || trimmed.starts_with("# ") || trimmed.starts_with("#\t") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_docs_adds_rust() { - let comment = "```\nfn some_rust() {}\n```"; - assert_eq!(format_docs(comment), "```rust\nfn some_rust() {}\n```"); - } - - #[test] - fn test_format_docs_handles_plain_text() { - let comment = "```text\nthis is plain text\n```"; - assert_eq!(format_docs(comment), "```text\nthis is plain text\n```"); - } - - #[test] - fn test_format_docs_handles_non_rust() { - let comment = "```sh\nsupposedly shell code\n```"; - assert_eq!(format_docs(comment), "```sh\nsupposedly shell code\n```"); - } - - #[test] - fn test_format_docs_handles_rust_alias() { - let comment = "```ignore\nlet z = 55;\n```"; - assert_eq!(format_docs(comment), "```rust\nlet z = 55;\n```"); - } - - #[test] - fn test_format_docs_handles_complex_code_block_attrs() { - let comment = "```rust,no_run\nlet z = 55;\n```"; - assert_eq!(format_docs(comment), "```rust\nlet z = 55;\n```"); - } - - #[test] - fn test_format_docs_handles_error_codes() { - let comment = "```compile_fail,E0641\nlet b = 0 as *const _;\n```"; - assert_eq!(format_docs(comment), "```rust\nlet b = 0 as *const _;\n```"); - } - - #[test] - fn test_format_docs_skips_comments_in_rust_block() { - let comment = - "```rust\n # skip1\n# skip2\n#stay1\nstay2\n#\n #\n # \n #\tskip3\n\t#\t\n```"; - assert_eq!(format_docs(comment), "```rust\n#stay1\nstay2\n```"); - } - - #[test] - fn test_format_docs_does_not_skip_lines_if_plain_text() { - let comment = - "```text\n # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t\n```"; - assert_eq!( - format_docs(comment), - "```text\n # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t\n```", - ); - } - - #[test] - fn test_format_docs_keeps_comments_outside_of_rust_block() { - let comment = " # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t"; - assert_eq!(format_docs(comment), comment); - } - - #[test] - fn test_format_docs_preserves_newlines() { - let comment = "this\nis\nmultiline"; - assert_eq!(format_docs(comment), comment); - } - - #[test] - fn test_code_blocks_in_comments_marked_as_rust() { - let comment = r#"```rust -fn main(){} -``` -Some comment. -``` -let a = 1; -```"#; - - assert_eq!( - format_docs(comment), - "```rust\nfn main(){}\n```\nSome comment.\n```rust\nlet a = 1;\n```" - ); - } - - #[test] - fn test_code_blocks_in_comments_marked_as_text() { - let comment = r#"```text -filler -text -``` -Some comment. -``` -let a = 1; -```"#; - - assert_eq!( - format_docs(comment), - "```text\nfiller\ntext\n```\nSome comment.\n```rust\nlet a = 1;\n```" - ); - } - - #[test] - fn test_format_docs_handles_escape_double_hashes() { - let comment = r#"```rust -let s = "foo -## bar # baz"; -```"#; - - assert_eq!(format_docs(comment), "```rust\nlet s = \"foo\n# bar # baz\";\n```"); - } - - #[test] - fn test_format_docs_handles_double_hashes_non_rust() { - let comment = r#"```markdown -## A second-level heading -```"#; - assert_eq!(format_docs(comment), "```markdown\n## A second-level heading\n```"); - } -}