Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide-db/src/documentation.rs')
-rw-r--r--crates/ide-db/src/documentation.rs351
1 files changed, 57 insertions, 294 deletions
diff --git a/crates/ide-db/src/documentation.rs b/crates/ide-db/src/documentation.rs
index cab19aadfd..4c4691cca2 100644
--- a/crates/ide-db/src/documentation.rs
+++ b/crates/ide-db/src/documentation.rs
@@ -1,337 +1,100 @@
//! Documentation attribute related utilities.
-use either::Either;
-use hir::{
- AttrId, AttrSourceMap, AttrsWithOwner, HasAttrs, InFile,
- db::{DefDatabase, HirDatabase},
- resolve_doc_path_on, sym,
-};
-use itertools::Itertools;
-use span::{TextRange, TextSize};
-use syntax::{
- AstToken,
- ast::{self, IsString},
-};
+use std::borrow::Cow;
+
+use hir::{HasAttrs, db::HirDatabase, resolve_doc_path_on};
/// Holds documentation
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct Documentation(String);
+pub struct Documentation<'db>(Cow<'db, str>);
+
+impl<'db> Documentation<'db> {
+ #[inline]
+ pub fn new_owned(s: String) -> Self {
+ Documentation(Cow::Owned(s))
+ }
-impl Documentation {
- pub fn new(s: String) -> Self {
- Documentation(s)
+ #[inline]
+ pub fn new_borrowed(s: &'db str) -> Self {
+ Documentation(Cow::Borrowed(s))
}
+ #[inline]
+ pub fn into_owned(self) -> Documentation<'static> {
+ Documentation::new_owned(self.0.into_owned())
+ }
+
+ #[inline]
pub fn as_str(&self) -> &str {
&self.0
}
}
-impl From<Documentation> for String {
- fn from(Documentation(string): Documentation) -> Self {
- string
+pub trait HasDocs: HasAttrs + Copy {
+ fn docs(self, db: &dyn HirDatabase) -> Option<Documentation<'_>> {
+ let docs = match self.docs_with_rangemap(db)? {
+ Cow::Borrowed(docs) => Documentation::new_borrowed(docs.docs()),
+ Cow::Owned(docs) => Documentation::new_owned(docs.into_docs()),
+ };
+ Some(docs)
+ }
+ fn docs_with_rangemap(self, db: &dyn HirDatabase) -> Option<Cow<'_, hir::Docs>> {
+ self.hir_docs(db).map(Cow::Borrowed)
}
-}
-
-pub trait HasDocs: HasAttrs {
- fn docs(self, db: &dyn HirDatabase) -> Option<Documentation>;
- fn docs_with_rangemap(self, db: &dyn HirDatabase) -> Option<(Documentation, DocsRangeMap)>;
fn resolve_doc_path(
self,
db: &dyn HirDatabase,
link: &str,
ns: Option<hir::Namespace>,
- is_inner_doc: bool,
- ) -> Option<hir::DocLinkDef>;
-}
-/// 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<(InFile<TextRange>, AttrId)> {
- 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 }, idx))
- }
- 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 }, idx))
- }
- }
- }
-
- pub fn shift_docstring_line_range(self, offset: TextSize) -> DocsRangeMap {
- let mapping = self
- .mapping
- .into_iter()
- .map(|(buf_offset, id, base_offset)| {
- let buf_offset = buf_offset.checked_add(offset).unwrap();
- (buf_offset, id, base_offset)
- })
- .collect_vec();
- DocsRangeMap { source_map: self.source_map, mapping }
- }
-}
-
-pub fn docs_with_rangemap(
- db: &dyn DefDatabase,
- attrs: &AttrsWithOwner,
-) -> Option<(Documentation, DocsRangeMap)> {
- let docs = attrs
- .by_key(sym::doc)
- .attrs()
- .filter_map(|attr| attr.string_value_unescape().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<String> {
- let docs = attrs.by_key(sym::doc).attrs().filter_map(|attr| attr.string_value_unescape());
- 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() {
- // We don't trim trailing whitespace from doc comments as multiple trailing spaces
- // indicates a hard line break in Markdown.
- let lines = doc.lines().map(|line| {
- line.char_indices().nth(indent).map_or(line, |(offset, _)| &line[offset..])
- });
-
- buf.extend(Itertools::intersperse(lines, "\n"));
- }
- buf.push('\n');
+ is_inner_doc: hir::IsInnerDoc,
+ ) -> Option<hir::DocLinkDef> {
+ resolve_doc_path_on(db, self, link, ns, is_inner_doc)
}
- 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<Documentation> {
- docs_from_attrs(&self.attrs(db)).map(Documentation)
- }
- fn docs_with_rangemap(
- self,
- db: &dyn HirDatabase,
- ) -> Option<(Documentation, DocsRangeMap)> {
- docs_with_rangemap(db, &self.attrs(db))
- }
- fn resolve_doc_path(
- self,
- db: &dyn HirDatabase,
- link: &str,
- ns: Option<hir::Namespace>,
- is_inner_doc: bool,
- ) -> Option<hir::DocLinkDef> {
- resolve_doc_path_on(db, self, link, ns, is_inner_doc)
- }
- }
+ impl HasDocs for hir::$def {}
)*};
}
impl_has_docs![
Variant, Field, Static, Const, Trait, TypeAlias, Macro, Function, Adt, Module, Impl, Crate,
+ AssocItem, Struct, Union, Enum,
];
-macro_rules! impl_has_docs_enum {
- ($($variant:ident),* for $enum:ident) => {$(
- impl HasDocs for hir::$variant {
- fn docs(self, db: &dyn HirDatabase) -> Option<Documentation> {
- hir::$enum::$variant(self).docs(db)
- }
-
- fn docs_with_rangemap(
- self,
- db: &dyn HirDatabase,
- ) -> Option<(Documentation, DocsRangeMap)> {
- hir::$enum::$variant(self).docs_with_rangemap(db)
- }
- fn resolve_doc_path(
- self,
- db: &dyn HirDatabase,
- link: &str,
- ns: Option<hir::Namespace>,
- is_inner_doc: bool,
- ) -> Option<hir::DocLinkDef> {
- hir::$enum::$variant(self).resolve_doc_path(db, link, ns, is_inner_doc)
- }
- }
- )*};
-}
-
-impl_has_docs_enum![Struct, Union, Enum for Adt];
-
-impl HasDocs for hir::AssocItem {
- fn docs(self, db: &dyn HirDatabase) -> Option<Documentation> {
- match self {
- hir::AssocItem::Function(it) => it.docs(db),
- hir::AssocItem::Const(it) => it.docs(db),
- hir::AssocItem::TypeAlias(it) => it.docs(db),
- }
- }
-
- fn docs_with_rangemap(self, db: &dyn HirDatabase) -> Option<(Documentation, DocsRangeMap)> {
- match self {
- hir::AssocItem::Function(it) => it.docs_with_rangemap(db),
- hir::AssocItem::Const(it) => it.docs_with_rangemap(db),
- hir::AssocItem::TypeAlias(it) => it.docs_with_rangemap(db),
- }
- }
-
- fn resolve_doc_path(
- self,
- db: &dyn HirDatabase,
- link: &str,
- ns: Option<hir::Namespace>,
- is_inner_doc: bool,
- ) -> Option<hir::DocLinkDef> {
- match self {
- hir::AssocItem::Function(it) => it.resolve_doc_path(db, link, ns, is_inner_doc),
- hir::AssocItem::Const(it) => it.resolve_doc_path(db, link, ns, is_inner_doc),
- hir::AssocItem::TypeAlias(it) => it.resolve_doc_path(db, link, ns, is_inner_doc),
- }
- }
-}
-
impl HasDocs for hir::ExternCrateDecl {
- fn docs(self, db: &dyn HirDatabase) -> Option<Documentation> {
- let crate_docs = docs_from_attrs(&self.resolved_crate(db)?.root_module().attrs(db));
- let decl_docs = docs_from_attrs(&self.attrs(db));
+ fn docs(self, db: &dyn HirDatabase) -> Option<Documentation<'_>> {
+ let crate_docs = self.resolved_crate(db)?.hir_docs(db);
+ let decl_docs = self.hir_docs(db);
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)
+ (Some(docs), None) | (None, Some(docs)) => {
+ Some(Documentation::new_borrowed(docs.docs()))
+ }
+ (Some(decl_docs), Some(crate_docs)) => {
+ let mut docs = String::with_capacity(
+ decl_docs.docs().len() + "\n\n".len() + crate_docs.docs().len(),
+ );
+ docs.push_str(decl_docs.docs());
+ docs.push_str("\n\n");
+ docs.push_str(crate_docs.docs());
+ Some(Documentation::new_owned(docs))
}
}
- .map(Documentation::new)
}
- fn docs_with_rangemap(self, db: &dyn HirDatabase) -> Option<(Documentation, DocsRangeMap)> {
- let crate_docs = docs_with_rangemap(db, &self.resolved_crate(db)?.root_module().attrs(db));
- let decl_docs = docs_with_rangemap(db, &self.attrs(db));
+ fn docs_with_rangemap(self, db: &dyn HirDatabase) -> Option<Cow<'_, hir::Docs>> {
+ let crate_docs = self.resolved_crate(db)?.hir_docs(db);
+ let decl_docs = self.hir_docs(db);
match (decl_docs, crate_docs) {
(None, None) => None,
- (Some(decl_docs), None) => Some(decl_docs),
- (None, Some(crate_docs)) => Some(crate_docs),
- (
- Some((Documentation(mut decl_docs), mut decl_range_map)),
- Some((Documentation(crate_docs), crate_range_map)),
- ) => {
- decl_docs.push('\n');
- decl_docs.push('\n');
- let offset = TextSize::new(decl_docs.len() as u32);
- decl_docs += &crate_docs;
- let crate_range_map = crate_range_map.shift_docstring_line_range(offset);
- decl_range_map.mapping.extend(crate_range_map.mapping);
- Some((Documentation(decl_docs), decl_range_map))
+ (Some(docs), None) | (None, Some(docs)) => Some(Cow::Borrowed(docs)),
+ (Some(decl_docs), Some(crate_docs)) => {
+ let mut docs = decl_docs.clone();
+ docs.append_str("\n\n");
+ docs.append(crate_docs);
+ Some(Cow::Owned(docs))
}
}
}
- fn resolve_doc_path(
- self,
- db: &dyn HirDatabase,
- link: &str,
- ns: Option<hir::Namespace>,
- is_inner_doc: bool,
- ) -> Option<hir::DocLinkDef> {
- resolve_doc_path_on(db, self, link, ns, is_inner_doc)
- }
-}
-
-fn get_doc_string_in_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 => {
- // FIXME: See highlight injection for what to do here
- None
- }
- _ => None,
- }
-}
-
-fn doc_indent(attrs: &hir::Attrs) -> usize {
- let mut min = !0;
- for val in attrs.by_key(sym::doc).attrs().filter_map(|attr| attr.string_value_unescape()) {
- if let Some(m) =
- val.lines().filter_map(|line| line.chars().position(|c| !c.is_whitespace())).min()
- {
- min = min.min(m);
- }
- }
- min
}