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, 294 insertions, 57 deletions
diff --git a/crates/ide-db/src/documentation.rs b/crates/ide-db/src/documentation.rs
index 4c4691cca2..cab19aadfd 100644
--- a/crates/ide-db/src/documentation.rs
+++ b/crates/ide-db/src/documentation.rs
@@ -1,100 +1,337 @@
//! Documentation attribute related utilities.
-use std::borrow::Cow;
-
-use hir::{HasAttrs, db::HirDatabase, resolve_doc_path_on};
+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},
+};
/// Holds documentation
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct Documentation<'db>(Cow<'db, str>);
-
-impl<'db> Documentation<'db> {
- #[inline]
- pub fn new_owned(s: String) -> Self {
- Documentation(Cow::Owned(s))
- }
+pub struct Documentation(String);
- #[inline]
- pub fn new_borrowed(s: &'db str) -> Self {
- Documentation(Cow::Borrowed(s))
+impl Documentation {
+ pub fn new(s: String) -> Self {
+ Documentation(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
}
}
-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)
+impl From<Documentation> for String {
+ fn from(Documentation(string): Documentation) -> Self {
+ string
}
+}
+
+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: hir::IsInnerDoc,
- ) -> Option<hir::DocLinkDef> {
- resolve_doc_path_on(db, self, link, ns, is_inner_doc)
+ 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');
+ }
+ buf.pop();
+ if buf.is_empty() { None } else { Some(buf) }
}
macro_rules! impl_has_docs {
($($def:ident,)*) => {$(
- impl HasDocs for hir::$def {}
+ 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_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 = self.resolved_crate(db)?.hir_docs(db);
- let decl_docs = self.hir_docs(db);
+ 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));
match (decl_docs, crate_docs) {
(None, None) => None,
- (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))
+ (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 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);
+ 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));
match (decl_docs, crate_docs) {
(None, None) => None,
- (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))
+ (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))
}
}
}
+ 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
}