1use 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#[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#[derive(Debug)]
48pub struct DocsRangeMap {
49 source_map: AttrSourceMap,
50 mapping: Vec<(TextRange, AttrId, TextRange)>,
54}
55
56impl DocsRangeMap {
57 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 if !doc.is_empty() {
158 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 Some(ast::Expr::Literal(lit)) => match lit.kind() {
315 ast::LiteralKind::String(it) => Some(it),
316 _ => None,
317 },
318 None => {
320 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}