ide_db/
documentation.rs

1//! Documentation attribute related utilities.
2use either::Either;
3use hir::{
4    AttrId, AttrSourceMap, AttrsWithOwner, HasAttrs, InFile,
5    db::{DefDatabase, HirDatabase},
6    resolve_doc_path_on, sym,
7};
8use itertools::Itertools;
9use span::{TextRange, TextSize};
10use syntax::{
11    AstToken,
12    ast::{self, IsString},
13};
14
15/// Holds documentation
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct Documentation(String);
18
19impl Documentation {
20    pub fn new(s: String) -> Self {
21        Documentation(s)
22    }
23
24    pub fn as_str(&self) -> &str {
25        &self.0
26    }
27}
28
29impl From<Documentation> for String {
30    fn from(Documentation(string): Documentation) -> Self {
31        string
32    }
33}
34
35pub trait HasDocs: HasAttrs {
36    fn docs(self, db: &dyn HirDatabase) -> Option<Documentation>;
37    fn docs_with_rangemap(self, db: &dyn HirDatabase) -> Option<(Documentation, DocsRangeMap)>;
38    fn resolve_doc_path(
39        self,
40        db: &dyn HirDatabase,
41        link: &str,
42        ns: Option<hir::Namespace>,
43        is_inner_doc: bool,
44    ) -> Option<hir::DocLinkDef>;
45}
46/// A struct to map text ranges from [`Documentation`] back to TextRanges in the syntax tree.
47#[derive(Debug)]
48pub struct DocsRangeMap {
49    source_map: AttrSourceMap,
50    // (docstring-line-range, attr_index, attr-string-range)
51    // a mapping from the text range of a line of the [`Documentation`] to the attribute index and
52    // the original (untrimmed) syntax doc line
53    mapping: Vec<(TextRange, AttrId, TextRange)>,
54}
55
56impl DocsRangeMap {
57    /// Maps a [`TextRange`] relative to the documentation string back to its AST range
58    pub fn map(&self, range: TextRange) -> Option<(InFile<TextRange>, AttrId)> {
59        let found = self.mapping.binary_search_by(|(probe, ..)| probe.ordering(range)).ok()?;
60        let (line_docs_range, idx, original_line_src_range) = self.mapping[found];
61        if !line_docs_range.contains_range(range) {
62            return None;
63        }
64
65        let relative_range = range - line_docs_range.start();
66
67        let InFile { file_id, value: source } = self.source_map.source_of_id(idx);
68        match source {
69            Either::Left(attr) => {
70                let string = get_doc_string_in_attr(attr)?;
71                let text_range = string.open_quote_text_range()?;
72                let range = TextRange::at(
73                    text_range.end() + original_line_src_range.start() + relative_range.start(),
74                    string.syntax().text_range().len().min(range.len()),
75                );
76                Some((InFile { file_id, value: range }, idx))
77            }
78            Either::Right(comment) => {
79                let text_range = comment.syntax().text_range();
80                let range = TextRange::at(
81                    text_range.start()
82                        + TextSize::try_from(comment.prefix().len()).ok()?
83                        + original_line_src_range.start()
84                        + relative_range.start(),
85                    text_range.len().min(range.len()),
86                );
87                Some((InFile { file_id, value: range }, idx))
88            }
89        }
90    }
91
92    pub fn shift_docstring_line_range(self, offset: TextSize) -> DocsRangeMap {
93        let mapping = self
94            .mapping
95            .into_iter()
96            .map(|(buf_offset, id, base_offset)| {
97                let buf_offset = buf_offset.checked_add(offset).unwrap();
98                (buf_offset, id, base_offset)
99            })
100            .collect_vec();
101        DocsRangeMap { source_map: self.source_map, mapping }
102    }
103}
104
105pub fn docs_with_rangemap(
106    db: &dyn DefDatabase,
107    attrs: &AttrsWithOwner,
108) -> Option<(Documentation, DocsRangeMap)> {
109    let docs = attrs
110        .by_key(sym::doc)
111        .attrs()
112        .filter_map(|attr| attr.string_value_unescape().map(|s| (s, attr.id)));
113    let indent = doc_indent(attrs);
114    let mut buf = String::new();
115    let mut mapping = Vec::new();
116    for (doc, idx) in docs {
117        if !doc.is_empty() {
118            let mut base_offset = 0;
119            for raw_line in doc.split('\n') {
120                let line = raw_line.trim_end();
121                let line_len = line.len();
122                let (offset, line) = match line.char_indices().nth(indent) {
123                    Some((offset, _)) => (offset, &line[offset..]),
124                    None => (0, line),
125                };
126                let buf_offset = buf.len();
127                buf.push_str(line);
128                mapping.push((
129                    TextRange::new(buf_offset.try_into().ok()?, buf.len().try_into().ok()?),
130                    idx,
131                    TextRange::at(
132                        (base_offset + offset).try_into().ok()?,
133                        line_len.try_into().ok()?,
134                    ),
135                ));
136                buf.push('\n');
137                base_offset += raw_line.len() + 1;
138            }
139        } else {
140            buf.push('\n');
141        }
142    }
143    buf.pop();
144    if buf.is_empty() {
145        None
146    } else {
147        Some((Documentation(buf), DocsRangeMap { mapping, source_map: attrs.source_map(db) }))
148    }
149}
150
151pub fn docs_from_attrs(attrs: &hir::Attrs) -> Option<String> {
152    let docs = attrs.by_key(sym::doc).attrs().filter_map(|attr| attr.string_value_unescape());
153    let indent = doc_indent(attrs);
154    let mut buf = String::new();
155    for doc in docs {
156        // str::lines doesn't yield anything for the empty string
157        if !doc.is_empty() {
158            // We don't trim trailing whitespace from doc comments as multiple trailing spaces
159            // indicates a hard line break in Markdown.
160            let lines = doc.lines().map(|line| {
161                line.char_indices().nth(indent).map_or(line, |(offset, _)| &line[offset..])
162            });
163
164            buf.extend(Itertools::intersperse(lines, "\n"));
165        }
166        buf.push('\n');
167    }
168    buf.pop();
169    if buf.is_empty() { None } else { Some(buf) }
170}
171
172macro_rules! impl_has_docs {
173    ($($def:ident,)*) => {$(
174        impl HasDocs for hir::$def {
175            fn docs(self, db: &dyn HirDatabase) -> Option<Documentation> {
176                docs_from_attrs(&self.attrs(db)).map(Documentation)
177            }
178            fn docs_with_rangemap(
179                self,
180                db: &dyn HirDatabase,
181            ) -> Option<(Documentation, DocsRangeMap)> {
182                docs_with_rangemap(db, &self.attrs(db))
183            }
184            fn resolve_doc_path(
185                self,
186                db: &dyn HirDatabase,
187                link: &str,
188                ns: Option<hir::Namespace>,
189                is_inner_doc: bool,
190            ) -> Option<hir::DocLinkDef> {
191                resolve_doc_path_on(db, self, link, ns, is_inner_doc)
192            }
193        }
194    )*};
195}
196
197impl_has_docs![
198    Variant, Field, Static, Const, Trait, TypeAlias, Macro, Function, Adt, Module, Impl, Crate,
199];
200
201macro_rules! impl_has_docs_enum {
202    ($($variant:ident),* for $enum:ident) => {$(
203        impl HasDocs for hir::$variant {
204            fn docs(self, db: &dyn HirDatabase) -> Option<Documentation> {
205                hir::$enum::$variant(self).docs(db)
206            }
207
208            fn docs_with_rangemap(
209                self,
210                db: &dyn HirDatabase,
211            ) -> Option<(Documentation, DocsRangeMap)> {
212                hir::$enum::$variant(self).docs_with_rangemap(db)
213            }
214            fn resolve_doc_path(
215                self,
216                db: &dyn HirDatabase,
217                link: &str,
218                ns: Option<hir::Namespace>,
219                is_inner_doc: bool,
220            ) -> Option<hir::DocLinkDef> {
221                hir::$enum::$variant(self).resolve_doc_path(db, link, ns, is_inner_doc)
222            }
223        }
224    )*};
225}
226
227impl_has_docs_enum![Struct, Union, Enum for Adt];
228
229impl HasDocs for hir::AssocItem {
230    fn docs(self, db: &dyn HirDatabase) -> Option<Documentation> {
231        match self {
232            hir::AssocItem::Function(it) => it.docs(db),
233            hir::AssocItem::Const(it) => it.docs(db),
234            hir::AssocItem::TypeAlias(it) => it.docs(db),
235        }
236    }
237
238    fn docs_with_rangemap(self, db: &dyn HirDatabase) -> Option<(Documentation, DocsRangeMap)> {
239        match self {
240            hir::AssocItem::Function(it) => it.docs_with_rangemap(db),
241            hir::AssocItem::Const(it) => it.docs_with_rangemap(db),
242            hir::AssocItem::TypeAlias(it) => it.docs_with_rangemap(db),
243        }
244    }
245
246    fn resolve_doc_path(
247        self,
248        db: &dyn HirDatabase,
249        link: &str,
250        ns: Option<hir::Namespace>,
251        is_inner_doc: bool,
252    ) -> Option<hir::DocLinkDef> {
253        match self {
254            hir::AssocItem::Function(it) => it.resolve_doc_path(db, link, ns, is_inner_doc),
255            hir::AssocItem::Const(it) => it.resolve_doc_path(db, link, ns, is_inner_doc),
256            hir::AssocItem::TypeAlias(it) => it.resolve_doc_path(db, link, ns, is_inner_doc),
257        }
258    }
259}
260
261impl HasDocs for hir::ExternCrateDecl {
262    fn docs(self, db: &dyn HirDatabase) -> Option<Documentation> {
263        let crate_docs = docs_from_attrs(&self.resolved_crate(db)?.root_module().attrs(db));
264        let decl_docs = docs_from_attrs(&self.attrs(db));
265        match (decl_docs, crate_docs) {
266            (None, None) => None,
267            (Some(decl_docs), None) => Some(decl_docs),
268            (None, Some(crate_docs)) => Some(crate_docs),
269            (Some(mut decl_docs), Some(crate_docs)) => {
270                decl_docs.push('\n');
271                decl_docs.push('\n');
272                decl_docs += &crate_docs;
273                Some(decl_docs)
274            }
275        }
276        .map(Documentation::new)
277    }
278
279    fn docs_with_rangemap(self, db: &dyn HirDatabase) -> Option<(Documentation, DocsRangeMap)> {
280        let crate_docs = docs_with_rangemap(db, &self.resolved_crate(db)?.root_module().attrs(db));
281        let decl_docs = docs_with_rangemap(db, &self.attrs(db));
282        match (decl_docs, crate_docs) {
283            (None, None) => None,
284            (Some(decl_docs), None) => Some(decl_docs),
285            (None, Some(crate_docs)) => Some(crate_docs),
286            (
287                Some((Documentation(mut decl_docs), mut decl_range_map)),
288                Some((Documentation(crate_docs), crate_range_map)),
289            ) => {
290                decl_docs.push('\n');
291                decl_docs.push('\n');
292                let offset = TextSize::new(decl_docs.len() as u32);
293                decl_docs += &crate_docs;
294                let crate_range_map = crate_range_map.shift_docstring_line_range(offset);
295                decl_range_map.mapping.extend(crate_range_map.mapping);
296                Some((Documentation(decl_docs), decl_range_map))
297            }
298        }
299    }
300    fn resolve_doc_path(
301        self,
302        db: &dyn HirDatabase,
303        link: &str,
304        ns: Option<hir::Namespace>,
305        is_inner_doc: bool,
306    ) -> Option<hir::DocLinkDef> {
307        resolve_doc_path_on(db, self, link, ns, is_inner_doc)
308    }
309}
310
311fn get_doc_string_in_attr(it: &ast::Attr) -> Option<ast::String> {
312    match it.expr() {
313        // #[doc = lit]
314        Some(ast::Expr::Literal(lit)) => match lit.kind() {
315            ast::LiteralKind::String(it) => Some(it),
316            _ => None,
317        },
318        // #[cfg_attr(..., doc = "", ...)]
319        None => {
320            // FIXME: See highlight injection for what to do here
321            None
322        }
323        _ => None,
324    }
325}
326
327fn doc_indent(attrs: &hir::Attrs) -> usize {
328    let mut min = !0;
329    for val in attrs.by_key(sym::doc).attrs().filter_map(|attr| attr.string_value_unescape()) {
330        if let Some(m) =
331            val.lines().filter_map(|line| line.chars().position(|c| !c.is_whitespace())).min()
332        {
333            min = min.min(m);
334        }
335    }
336    min
337}