1use std::{
10 borrow::Cow,
11 convert::Infallible,
12 ops::{ControlFlow, Range},
13};
14
15use base_db::{Crate, SourceDatabase};
16use cfg::CfgOptions;
17use either::Either;
18use hir_expand::{
19 AstId, ExpandTo, HirFileId, InFile, MacroCallId,
20 attrs::{AstPathExt, expand_cfg_attr_with_doc_comments},
21 mod_path::ModPath,
22 span_map::SpanMap,
23};
24use span::AstIdMap;
25use syntax::{
26 AstNode, SyntaxNode,
27 ast::{self, IsString},
28};
29use thin_vec::ThinVec;
30use tt::{TextRange, TextSize};
31
32use crate::{macro_call_as_call_id, nameres::MacroSubNs, resolver::Resolver};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35struct DocsSourceMapLine {
36 string_offset: TextSize,
38 ast_offset: Option<TextSize>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct Docs {
45 docs: String,
47 docs_source_map: Vec<DocsSourceMapLine>,
49 outline_mod: Option<(HirFileId, usize)>,
54 inline_file: HirFileId,
55 prefix_len: TextSize,
57 inline_inner_docs_start: Option<TextSize>,
59 outline_inner_docs_start: Option<TextSize>,
62 macro_calls: ThinVec<(AstId<ast::MacroCall>, MacroCallId)>,
64}
65
66#[derive(Clone, Copy)]
67enum DocCommentKind {
68 Sugared(ast::CommentShape),
70 Desugared,
72}
73
74#[derive(Default)]
75struct Indent {
76 lines: Vec<Option<(usize, DocCommentKind)>>,
77 seen_sugared: bool,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum IsInnerDoc {
82 No,
83 Yes,
84}
85
86impl IsInnerDoc {
87 #[inline]
88 pub fn yes(self) -> bool {
89 self == IsInnerDoc::Yes
90 }
91}
92
93impl Docs {
94 #[inline]
95 pub fn docs(&self) -> &str {
96 &self.docs
97 }
98
99 #[inline]
100 pub fn into_docs(self) -> String {
101 self.docs
102 }
103
104 #[inline]
105 pub fn macro_calls(&self) -> impl Iterator<Item = (AstId<ast::MacroCall>, MacroCallId)> {
106 self.macro_calls.iter().copied()
107 }
108
109 fn is_empty(&self) -> bool {
110 let Self {
111 docs,
112 docs_source_map: _,
113 outline_mod: _,
114 inline_file: _,
115 prefix_len: _,
116 inline_inner_docs_start: _,
117 outline_inner_docs_start: _,
118 macro_calls,
119 } = self;
120 docs.is_empty() && macro_calls.is_empty()
121 }
122
123 pub fn find_ast_range(
124 &self,
125 mut string_range: TextRange,
126 ) -> Option<(InFile<TextRange>, IsInnerDoc)> {
127 if string_range.start() < self.prefix_len {
128 return None;
129 }
130 string_range -= self.prefix_len;
131
132 let mut file = self.inline_file;
133 let mut inner_docs_start = self.inline_inner_docs_start;
134 let source_map = if let Some((outline_mod_file, outline_mod_end)) = self.outline_mod {
136 if let Some(first_inline) = self.docs_source_map.get(outline_mod_end) {
137 if string_range.end() <= first_inline.string_offset {
138 file = outline_mod_file;
140 inner_docs_start = self.outline_inner_docs_start;
141 &self.docs_source_map[..outline_mod_end]
142 } else if string_range.start() >= first_inline.string_offset {
143 &self.docs_source_map[outline_mod_end..]
145 } else {
146 return None;
148 }
149 } else {
150 file = outline_mod_file;
152 inner_docs_start = self.outline_inner_docs_start;
153 &self.docs_source_map
154 }
155 } else {
156 &self.docs_source_map
158 };
159
160 let after_range =
161 source_map.partition_point(|line| line.string_offset <= string_range.start()) - 1;
162 let after_range = &source_map[after_range..];
163 let line = after_range.first()?;
164 let ast_offset = line.ast_offset?;
166 if after_range.get(1).is_some_and(|next_line| next_line.string_offset < string_range.end())
167 {
168 return None;
170 }
171 let ast_range = string_range - line.string_offset + ast_offset;
172 let is_inner = if inner_docs_start
173 .is_some_and(|inner_docs_start| string_range.start() >= inner_docs_start)
174 {
175 IsInnerDoc::Yes
176 } else {
177 IsInnerDoc::No
178 };
179 Some((InFile::new(file, ast_range), is_inner))
180 }
181
182 #[inline]
183 pub fn shift_by(&mut self, offset: TextSize) {
184 self.prefix_len += offset;
185 }
186
187 pub fn prepend_str(&mut self, s: &str) {
188 self.prefix_len += TextSize::of(s);
189 self.docs.insert_str(0, s);
190 }
191
192 pub fn append_str(&mut self, s: &str) {
193 self.docs.push_str(s);
194 }
195
196 pub fn append(&mut self, other: &Docs) {
197 let other_offset = TextSize::of(&self.docs);
198
199 assert!(
200 self.outline_mod.is_none() && other.outline_mod.is_none(),
201 "cannot merge `Docs` that have `outline_mod` set"
202 );
203 self.outline_mod = Some((self.inline_file, self.docs_source_map.len()));
204 self.inline_file = other.inline_file;
205 self.outline_inner_docs_start = self.inline_inner_docs_start;
206 self.inline_inner_docs_start = other.inline_inner_docs_start.map(|it| it + other_offset);
207
208 self.docs.push_str(&other.docs);
209 self.docs_source_map.extend(other.docs_source_map.iter().map(
210 |&DocsSourceMapLine { string_offset, ast_offset }| DocsSourceMapLine {
211 ast_offset,
212 string_offset: string_offset + other_offset,
213 },
214 ));
215 }
216
217 fn extend_with_doc_comment(&mut self, comment: ast::DocComment, indent: &mut Indent) {
218 let doc = comment.text();
219 let offset = comment.syntax().text_range().start() + ast::DocComment::PREFIX_LEN;
220 self.extend_with_doc_str(doc, offset, DocCommentKind::Sugared(comment.shape()), indent);
221 }
222
223 fn extend_with_doc_attr(&mut self, value: ast::String, indent: &mut Indent) {
224 let Some(value_offset) = value.text_range_between_quotes() else { return };
225 let value_offset = value_offset.start();
226 let Ok(unescaped) = value.value() else { return };
227 let ast_offset = matches!(unescaped, Cow::Borrowed(_)).then_some(value_offset);
231 self.push_doc_lines(&unescaped, ast_offset, DocCommentKind::Desugared, indent);
232 }
233
234 fn extend_with_doc_str(
235 &mut self,
236 doc: &str,
237 offset_in_ast: TextSize,
238 comment_kind: DocCommentKind,
239 indent: &mut Indent,
240 ) {
241 self.push_doc_lines(doc, Some(offset_in_ast), comment_kind, indent);
242 }
243
244 fn extend_with_unmapped_doc_str(&mut self, doc: &str, indent: &mut Indent) {
245 self.push_doc_lines(doc, None, DocCommentKind::Desugared, indent);
246 }
247
248 fn push_doc_lines(
264 &mut self,
265 doc: &str,
266 ast_offset: Option<TextSize>,
267 comment_kind: DocCommentKind,
268 indent: &mut Indent,
269 ) {
270 let shape = match comment_kind {
272 DocCommentKind::Sugared(shape) => {
273 indent.seen_sugared = true;
274 shape
275 }
276 DocCommentKind::Desugared => ast::CommentShape::Line,
278 };
279
280 if !doc.contains('\n') {
281 self.push_doc_line(doc, ast_offset, comment_kind, indent);
282 return;
283 }
284
285 let mut lines: Vec<(&str, TextSize)> = doc
286 .lines()
287 .map(|line| {
288 let offset = TextSize::new(doc.substr_range(line).unwrap().start as u32);
289 (line, offset)
290 })
291 .collect();
292
293 let raw_lines: Vec<&str> = lines.iter().map(|(l, _)| *l).collect();
294 let lines = match get_vertical_trim(&raw_lines) {
295 Some((i, j)) => &mut lines[i..j],
296 None => &mut lines[..],
297 };
298
299 let raw_lines: Vec<&str> = lines.iter().map(|(l, _)| *l).collect();
300 if let Some(horizontal) = get_horizontal_trim(&raw_lines, shape) {
301 let horizontal_len = TextSize::of(horizontal.as_str());
302 for (line, line_offset) in lines.iter_mut() {
304 if let Some(rest) = line.strip_prefix(horizontal.as_str()) {
305 *line = rest;
306 *line_offset += horizontal_len;
307 if shape == ast::CommentShape::Block
308 && (*line == "*" || line.starts_with("* ") || line.starts_with("**"))
309 {
310 *line = &line[1..];
311 *line_offset += TextSize::of("*");
312 }
313 }
314 }
315 }
316
317 for (line, line_offset) in lines.iter().copied() {
318 self.push_doc_line(line, ast_offset.map(|it| it + line_offset), comment_kind, indent);
319 }
320 }
321
322 fn push_doc_line(
324 &mut self,
325 line: &str,
326 ast_offset: Option<TextSize>,
327 comment_kind: DocCommentKind,
328 indent: &mut Indent,
329 ) {
330 self.docs_source_map
331 .push(DocsSourceMapLine { string_offset: TextSize::of(&self.docs), ast_offset });
332
333 let line = line.trim_end();
334 let line_indent = if line.chars().any(|ch| !ch.is_whitespace()) {
335 let line_indent = line.bytes().take_while(|c| *c == b' ' || *c == b'\t').count();
337 Some((line_indent, comment_kind))
338 } else {
339 None
340 };
341 indent.lines.push(line_indent);
342 self.docs.push_str(line);
343 self.docs.push('\n');
344 }
345
346 fn remove_indent(&mut self, indent: &Indent) {
347 struct Guard<'a>(&'a mut Docs);
349 impl Drop for Guard<'_> {
350 fn drop(&mut self) {
351 let Docs {
352 docs,
353 docs_source_map,
354 outline_mod,
355 inline_file: _,
356 prefix_len: _,
357 inline_inner_docs_start: _,
358 outline_inner_docs_start: _,
359 macro_calls: _,
360 } = self.0;
361 unsafe { docs.as_mut_vec() }.clear();
364 docs_source_map.clear();
366 *outline_mod = None;
367 }
368 }
369
370 if self.docs.is_empty() {
371 return;
372 }
373
374 let add_indent = if indent.seen_sugared { 1 } else { 0 };
387
388 let Some(min_indent) = indent
389 .lines
390 .iter()
391 .filter_map(|it| *it)
392 .map(|(line_indent, line_kind)| {
393 line_indent
394 + match line_kind {
395 DocCommentKind::Sugared(_) => 0,
396 DocCommentKind::Desugared => add_indent,
397 }
398 })
399 .min()
400 else {
401 return;
402 };
403
404 let guard = Guard(self);
405 let source_map = guard.0.docs_source_map.as_mut_slice();
406 let Some(&DocsSourceMapLine { string_offset: mut copy_into, .. }) = source_map.first()
407 else {
408 return;
409 };
410 let mut accumulated_offset = TextSize::new(0);
414 for idx in 0..source_map.len() {
415 let string_end_offset = source_map
416 .get(idx + 1)
417 .map_or_else(|| TextSize::of(&guard.0.docs), |next_attr| next_attr.string_offset);
418 let line_source = &mut source_map[idx];
419 let line_docs =
420 &guard.0.docs[TextRange::new(line_source.string_offset, string_end_offset)];
421 let line_docs_len = TextSize::of(line_docs);
422 let indent_size = if let Some((_, DocCommentKind::Desugared)) = indent.lines[idx]
423 && min_indent > 0
424 {
425 min_indent - add_indent
426 } else {
427 min_indent
428 };
429 let indent_size = line_docs.char_indices().nth(indent_size).map_or_else(
430 || TextSize::of(line_docs) - TextSize::of("\n"),
431 |(offset, _)| TextSize::new(offset as u32),
432 );
433 unsafe { guard.0.docs.as_bytes_mut() }.copy_within(
434 Range::<usize>::from(TextRange::new(
435 line_source.string_offset + indent_size,
436 string_end_offset,
437 )),
438 copy_into.into(),
439 );
440 copy_into += line_docs_len - indent_size;
441
442 if let Some(inner_attrs_start) = &mut guard.0.inline_inner_docs_start
443 && *inner_attrs_start == line_source.string_offset
444 {
445 *inner_attrs_start -= accumulated_offset;
446 }
447 line_source.string_offset -= accumulated_offset;
453 if let Some(ref mut ast_offset) = line_source.ast_offset {
454 *ast_offset += indent_size;
455 }
456
457 accumulated_offset += indent_size;
458 }
459 unsafe { guard.0.docs.as_mut_vec() }.truncate(copy_into.into());
462
463 std::mem::forget(guard);
464 }
465
466 fn remove_last_newline(&mut self) {
467 self.docs.truncate(self.docs.len().saturating_sub(1));
468 }
469
470 fn shrink_to_fit(&mut self) {
471 let Docs {
472 docs,
473 docs_source_map,
474 outline_mod: _,
475 inline_file: _,
476 prefix_len: _,
477 inline_inner_docs_start: _,
478 outline_inner_docs_start: _,
479 macro_calls,
480 } = self;
481 docs.shrink_to_fit();
482 docs_source_map.shrink_to_fit();
483 macro_calls.shrink_to_fit();
484 }
485}
486
487fn get_vertical_trim(lines: &[&str]) -> Option<(usize, usize)> {
492 let mut i = 0;
493 let mut j = lines.len();
494 if lines.first().is_some_and(|line| line.chars().all(|c| c == '*')) {
496 i += 1;
497 }
498
499 if j > i && !lines[j - 1].is_empty() && lines[j - 1].chars().all(|c| c == '*') {
501 j -= 1;
502 }
503
504 if i != 0 || j != lines.len() { Some((i, j)) } else { None }
505}
506
507fn get_horizontal_trim(lines: &[&str], kind: ast::CommentShape) -> Option<String> {
512 let mut i = usize::MAX;
513 let mut first = true;
514
515 let lines = match kind {
519 ast::CommentShape::Block => {
520 let mut i = lines
522 .first()
523 .map(|l| if l.trim_start().starts_with('*') { 0 } else { 1 })
524 .unwrap_or(0);
525 let mut j = lines.len();
526
527 while i < j && lines[i].trim().is_empty() {
528 i += 1;
529 }
530 while j > i && lines[j - 1].trim().is_empty() {
531 j -= 1;
532 }
533 &lines[i..j]
534 }
535 ast::CommentShape::Line => lines,
536 };
537
538 for line in lines {
539 for (j, c) in line.chars().enumerate() {
540 if j > i || !"* \t".contains(c) {
541 return None;
542 }
543 if c == '*' {
544 if first {
545 i = j;
546 first = false;
547 } else if i != j {
548 return None;
549 }
550 break;
551 }
552 }
553 if i >= line.len() {
554 return None;
555 }
556 }
557 Some(lines.first()?[..i].to_string())
558}
559
560struct DocMacroExpander<'db> {
561 db: &'db dyn SourceDatabase,
562 krate: Crate,
563 macro_depth: u32,
564 recursion_limit: u32,
565 resolver: Resolver<'db>,
566 file_id: HirFileId,
567 ast_id_map: &'db AstIdMap,
568 span_map: SpanMap<'db>,
569}
570
571fn expand_doc_expr_via_macro_pipeline<'db>(
572 expander: &mut DocMacroExpander<'db>,
573 macro_calls: &mut ThinVec<(AstId<ast::MacroCall>, MacroCallId)>,
574 expr: ast::Expr,
575) -> Option<String> {
576 match expr {
577 ast::Expr::ParenExpr(paren_expr) => {
578 expand_doc_expr_via_macro_pipeline(expander, macro_calls, paren_expr.expr()?)
579 }
580 ast::Expr::Literal(literal) => match literal.kind() {
581 ast::LiteralKind::String(string) => string.value().ok().map(Into::into),
582 _ => None,
583 },
584 ast::Expr::MacroExpr(macro_expr) => {
585 let macro_call = macro_expr.macro_call()?;
586 expand_doc_macro_call(expander, macro_calls, macro_call)
587 }
588 _ => None,
589 }
590}
591
592fn expand_doc_macro_call<'db>(
593 expander: &mut DocMacroExpander<'db>,
594 macro_calls: &mut ThinVec<(AstId<ast::MacroCall>, MacroCallId)>,
595 macro_call: ast::MacroCall,
596) -> Option<String> {
597 if expander.macro_depth >= expander.recursion_limit {
598 return None;
599 }
600
601 let path = macro_call.path()?;
602 let mod_path = ModPath::from_src(expander.db, path, &mut |range| {
603 expander.span_map.span_for_range(range).ctx
604 })?;
605 let call_site = expander.span_map.span_for_range(macro_call.syntax().text_range());
606 let ast_id = AstId::new(expander.file_id, expander.ast_id_map.ast_id(¯o_call));
607 let call_id = macro_call_as_call_id(
608 expander.db,
609 ast_id,
610 &mod_path,
611 call_site.ctx,
612 ExpandTo::Expr,
613 expander.krate,
614 expander.macro_depth + 1,
615 expander.recursion_limit,
616 |path| {
617 expander.resolver.resolve_path_as_macro_def(expander.db, path, Some(MacroSubNs::Bang))
618 },
619 &mut |_, _| (),
620 )
621 .ok()?
622 .value?;
623 macro_calls.push((ast_id, call_id));
624
625 let (parse, span_map) = &call_id.parse_macro_expansion(expander.db).value;
626 let expr = parse.clone().cast::<ast::Expr>().map(|parse| parse.tree())?;
627
628 let expansion_file_id: HirFileId = call_id.into();
632 let old_file_id = std::mem::replace(&mut expander.file_id, expansion_file_id);
633 let old_span_map =
634 std::mem::replace(&mut expander.span_map, SpanMap::ExpansionSpanMap(span_map));
635 let old_ast_id_map =
636 std::mem::replace(&mut expander.ast_id_map, expansion_file_id.ast_id_map(expander.db));
637 expander.macro_depth += 1;
638
639 let expansion = expand_doc_expr_via_macro_pipeline(expander, macro_calls, expr);
640
641 expander.file_id = old_file_id;
642 expander.span_map = old_span_map;
643 expander.ast_id_map = old_ast_id_map;
644 expander.macro_depth -= 1;
645
646 expansion
647}
648
649fn extend_with_attrs<'a, 'db>(
650 result: &mut Docs,
651 db: &'db dyn SourceDatabase,
652 krate: Crate,
653 node: &SyntaxNode,
654 file_id: HirFileId,
655 expect_inner_attrs: bool,
656 indent: &mut Indent,
657 get_cfg_options: &dyn Fn() -> &'a CfgOptions,
658 cfg_options: &mut Option<&'a CfgOptions>,
659 make_resolver: &dyn Fn() -> Resolver<'db>,
660) {
661 let mut expander = None;
663
664 expand_cfg_attr_with_doc_comments::<_, Infallible>(
665 node.children()
666 .filter_map(ast::AnyAttr::cast)
667 .filter(|attr| attr.kind().is_inner() == expect_inner_attrs)
668 .map(|attr| match attr {
669 ast::AnyAttr::Attr(it) => Either::Left(it),
670 ast::AnyAttr::DocComment(it) => Either::Right(it),
671 }),
672 || *cfg_options.get_or_insert_with(get_cfg_options),
673 |attr| {
674 match attr {
675 Either::Right(doc_comment) => result.extend_with_doc_comment(doc_comment, indent),
676 Either::Left((attr, _)) => match attr {
677 ast::Meta::KeyValueMeta(attr) if attr.path().is1("doc") => {
678 if let Some(value) = attr.expr() {
679 if let ast::Expr::Literal(value) = &value
680 && let ast::LiteralKind::String(value) = value.kind()
681 {
682 result.extend_with_doc_attr(value, indent);
683 } else {
684 let exp = expander.get_or_insert_with(|| {
685 let resolver = make_resolver();
686 let def_map = resolver.top_level_def_map();
687 let recursion_limit = def_map.recursion_limit();
688 DocMacroExpander {
689 db,
690 krate,
691 macro_depth: file_id.macro_expansion_depth(db),
692 recursion_limit,
693 resolver,
694 file_id,
695 ast_id_map: file_id.ast_id_map(db),
696 span_map: file_id.span_map(db),
697 }
698 });
699 if let Some(expanded) = expand_doc_expr_via_macro_pipeline(
700 exp,
701 &mut result.macro_calls,
702 value,
703 ) {
704 result.extend_with_unmapped_doc_str(&expanded, indent);
705 }
706 }
707 }
708 }
709 _ => {}
710 },
711 }
712 ControlFlow::Continue(())
713 },
714 );
715}
716
717pub(crate) fn extract_docs<'a, 'db>(
718 db: &'db dyn SourceDatabase,
719 krate: Crate,
720 resolver: &dyn Fn() -> Resolver<'db>,
721 get_cfg_options: &dyn Fn() -> &'a CfgOptions,
722 source: InFile<ast::AnyHasAttrs>,
723 outer_mod_decl: Option<InFile<ast::Module>>,
724 inner_attrs_node: Option<SyntaxNode>,
725) -> Option<Box<Docs>> {
726 let mut result = Docs {
727 docs: String::new(),
728 docs_source_map: Vec::new(),
729 outline_mod: None,
730 inline_file: source.file_id,
731 prefix_len: TextSize::new(0),
732 inline_inner_docs_start: None,
733 outline_inner_docs_start: None,
734 macro_calls: ThinVec::new(),
735 };
736
737 let mut cfg_options = None;
738
739 let mut indent = Indent::default();
740 if let Some(outer_mod_decl) = outer_mod_decl {
741 extend_with_attrs(
743 &mut result,
744 db,
745 krate,
746 outer_mod_decl.value.syntax(),
747 outer_mod_decl.file_id,
748 false,
749 &mut indent,
750 get_cfg_options,
751 &mut cfg_options,
752 resolver,
753 );
754 result.outline_mod = Some((outer_mod_decl.file_id, result.docs_source_map.len()));
755 }
756
757 extend_with_attrs(
759 &mut result,
760 db,
761 krate,
762 source.value.syntax(),
763 source.file_id,
764 false,
765 &mut indent,
766 get_cfg_options,
767 &mut cfg_options,
768 resolver,
769 );
770 if let Some(inner_attrs_node) = &inner_attrs_node {
771 result.inline_inner_docs_start = Some(TextSize::of(&result.docs));
772 extend_with_attrs(
773 &mut result,
774 db,
775 krate,
776 inner_attrs_node,
777 source.file_id,
778 true,
779 &mut indent,
780 get_cfg_options,
781 &mut cfg_options,
782 resolver,
783 );
784 }
785 result.remove_indent(&indent);
786
787 result.remove_last_newline();
788
789 result.shrink_to_fit();
790
791 if result.is_empty() { None } else { Some(Box::new(result)) }
792}
793
794#[cfg(test)]
795mod tests {
796 use expect_test::expect;
797 use hir_expand::InFile;
798 use syntax::{AstNode, AstToken, ast};
799 use test_fixture::WithFixture;
800 use thin_vec::ThinVec;
801 use tt::{TextRange, TextSize};
802
803 use crate::test_db::TestDB;
804
805 use super::{DocCommentKind, Docs, Indent, IsInnerDoc};
806
807 #[test]
808 fn docs() {
809 let (_db, file_id) = TestDB::with_single_file("");
810 let mut docs = Docs {
811 docs: String::new(),
812 docs_source_map: Vec::new(),
813 outline_mod: None,
814 inline_file: file_id.into(),
815 prefix_len: TextSize::new(0),
816 inline_inner_docs_start: None,
817 outline_inner_docs_start: None,
818 macro_calls: ThinVec::new(),
819 };
820 let mut indent = Indent::default();
821
822 let outer = " foo\n\tbar baz";
823 let mut ast_offset = TextSize::new(123);
824 for line in outer.split('\n') {
825 docs.extend_with_doc_str(
826 line,
827 ast_offset,
828 DocCommentKind::Sugared(ast::CommentShape::Line),
829 &mut indent,
830 );
831 ast_offset += TextSize::of(line) + TextSize::of("\n");
832 }
833
834 docs.inline_inner_docs_start = Some(TextSize::of(&docs.docs));
835 ast_offset += TextSize::new(123);
836 let inner = " bar \n baz";
837 for line in inner.split('\n') {
838 docs.extend_with_doc_str(
839 line,
840 ast_offset,
841 DocCommentKind::Sugared(ast::CommentShape::Line),
842 &mut indent,
843 );
844 ast_offset += TextSize::of(line) + TextSize::of("\n");
845 }
846
847 expect![[r#"
848 [
849 DocsSourceMapLine {
850 string_offset: 0,
851 ast_offset: Some(
852 123,
853 ),
854 },
855 DocsSourceMapLine {
856 string_offset: 5,
857 ast_offset: Some(
858 128,
859 ),
860 },
861 DocsSourceMapLine {
862 string_offset: 15,
863 ast_offset: Some(
864 261,
865 ),
866 },
867 DocsSourceMapLine {
868 string_offset: 20,
869 ast_offset: Some(
870 267,
871 ),
872 },
873 ]
874 "#]]
875 .assert_debug_eq(&docs.docs_source_map);
876
877 docs.remove_indent(&indent);
878
879 assert_eq!(docs.inline_inner_docs_start, Some(TextSize::new(13)));
880
881 assert_eq!(docs.docs, "foo\nbar baz\nbar\nbaz\n");
882 expect![[r#"
883 [
884 DocsSourceMapLine {
885 string_offset: 0,
886 ast_offset: Some(
887 124,
888 ),
889 },
890 DocsSourceMapLine {
891 string_offset: 4,
892 ast_offset: Some(
893 129,
894 ),
895 },
896 DocsSourceMapLine {
897 string_offset: 13,
898 ast_offset: Some(
899 262,
900 ),
901 },
902 DocsSourceMapLine {
903 string_offset: 17,
904 ast_offset: Some(
905 268,
906 ),
907 },
908 ]
909 "#]]
910 .assert_debug_eq(&docs.docs_source_map);
911
912 docs.append(&docs.clone());
913 docs.prepend_str("prefix---");
914 assert_eq!(docs.docs, "prefix---foo\nbar baz\nbar\nbaz\nfoo\nbar baz\nbar\nbaz\n");
915 expect![[r#"
916 [
917 DocsSourceMapLine {
918 string_offset: 0,
919 ast_offset: Some(
920 124,
921 ),
922 },
923 DocsSourceMapLine {
924 string_offset: 4,
925 ast_offset: Some(
926 129,
927 ),
928 },
929 DocsSourceMapLine {
930 string_offset: 13,
931 ast_offset: Some(
932 262,
933 ),
934 },
935 DocsSourceMapLine {
936 string_offset: 17,
937 ast_offset: Some(
938 268,
939 ),
940 },
941 DocsSourceMapLine {
942 string_offset: 21,
943 ast_offset: Some(
944 124,
945 ),
946 },
947 DocsSourceMapLine {
948 string_offset: 25,
949 ast_offset: Some(
950 129,
951 ),
952 },
953 DocsSourceMapLine {
954 string_offset: 34,
955 ast_offset: Some(
956 262,
957 ),
958 },
959 DocsSourceMapLine {
960 string_offset: 38,
961 ast_offset: Some(
962 268,
963 ),
964 },
965 ]
966 "#]]
967 .assert_debug_eq(&docs.docs_source_map);
968
969 let range = |start, end| TextRange::new(TextSize::new(start), TextSize::new(end));
970 let in_file = |range| InFile::new(file_id.into(), range);
971 assert_eq!(docs.find_ast_range(range(0, 2)), None);
972 assert_eq!(docs.find_ast_range(range(8, 10)), None);
973 assert_eq!(
974 docs.find_ast_range(range(9, 10)),
975 Some((in_file(range(124, 125)), IsInnerDoc::No))
976 );
977 assert_eq!(docs.find_ast_range(range(20, 23)), None);
978 assert_eq!(
979 docs.find_ast_range(range(23, 25)),
980 Some((in_file(range(263, 265)), IsInnerDoc::Yes))
981 );
982 }
983
984 #[test]
985 fn sugared_desugared_mix() {
986 let (_db, file_id) = TestDB::with_single_file("");
987 let mut docs = Docs {
988 docs: String::new(),
989 docs_source_map: Vec::new(),
990 outline_mod: None,
991 inline_file: file_id.into(),
992 prefix_len: TextSize::new(0),
993 inline_inner_docs_start: None,
994 outline_inner_docs_start: None,
995 macro_calls: ThinVec::new(),
996 };
997 let mut indent = Indent::default();
998
999 docs.push_doc_lines(
1000 " hello!",
1001 None,
1002 DocCommentKind::Sugared(ast::CommentShape::Line),
1003 &mut indent,
1004 );
1005 docs.push_doc_lines("another", None, DocCommentKind::Desugared, &mut indent);
1006 docs.remove_indent(&indent);
1007 docs.remove_last_newline();
1008
1009 assert_eq!(docs.docs(), "hello!\nanother");
1010 }
1011
1012 fn comment_docs(source: &str) -> Docs {
1015 let (_db, file_id) = TestDB::with_single_file("");
1016 let comment = syntax::SourceFile::parse(source, span::Edition::CURRENT)
1017 .syntax_node()
1018 .descendants_with_tokens()
1019 .filter_map(|it| it.into_node())
1020 .find_map(ast::DocComment::cast)
1021 .expect("no comment in the fixture");
1022 let mut docs = Docs {
1023 docs: String::new(),
1024 docs_source_map: Vec::new(),
1025 outline_mod: None,
1026 inline_file: file_id.into(),
1027 prefix_len: TextSize::new(0),
1028 inline_inner_docs_start: None,
1029 outline_inner_docs_start: None,
1030 macro_calls: ThinVec::new(),
1031 };
1032 let mut indent = Indent::default();
1033 docs.extend_with_doc_comment(comment, &mut indent);
1034 docs.remove_indent(&indent);
1035 docs.remove_last_newline();
1036 docs
1037 }
1038
1039 #[test]
1040 fn block_doc_comment_stars() {
1041 #[track_caller]
1042 fn check(source: &str, expect: expect_test::Expect) {
1043 expect.assert_eq(&comment_docs(source).docs);
1044 }
1045
1046 check(
1052 "/**\n * foo\n *\n * * bullet\n *bar\n */",
1053 expect![[r#"
1054 foo
1055
1056 * bullet
1057 *bar
1058 "#]],
1059 );
1060 check("/** * item */", expect!["* item"]);
1062 check(
1064 "/**\n * foo\n * bar\n */",
1065 expect![[r#"
1066 * foo
1067 * bar
1068 "#]],
1069 );
1070 }
1071
1072 #[test]
1073 fn block_doc_comment_source_map() {
1074 let docs = comment_docs("/**\n * foo\n * bar\n */");
1075 assert_eq!(docs.docs, "foo\nbar\n");
1078
1079 let range = |start, end| TextRange::new(TextSize::new(start), TextSize::new(end));
1080 let in_file = |range| InFile::new(docs.inline_file, range);
1081 let mapped = |start, end| docs.find_ast_range(range(start, end));
1082 assert_eq!(mapped(0, 3), Some((in_file(range(7, 10)), IsInnerDoc::No)));
1084 assert_eq!(mapped(4, 7), Some((in_file(range(14, 17)), IsInnerDoc::No)));
1085 }
1086
1087 fn doc_attr_docs(source: &str) -> Docs {
1089 let (_db, file_id) = TestDB::with_single_file("");
1090 let value = syntax::SourceFile::parse(source, span::Edition::CURRENT)
1091 .syntax_node()
1092 .descendants_with_tokens()
1093 .filter_map(|it| it.into_token())
1094 .find_map(ast::String::cast)
1095 .expect("no string in the fixture");
1096 let mut docs = Docs {
1097 docs: String::new(),
1098 docs_source_map: Vec::new(),
1099 outline_mod: None,
1100 inline_file: file_id.into(),
1101 prefix_len: TextSize::new(0),
1102 inline_inner_docs_start: None,
1103 outline_inner_docs_start: None,
1104 macro_calls: ThinVec::new(),
1105 };
1106 let mut indent = Indent::default();
1107 docs.extend_with_doc_attr(value, &mut indent);
1108 docs.remove_indent(&indent);
1109 docs.remove_last_newline();
1110 docs
1111 }
1112
1113 #[test]
1114 fn doc_attr_source_map() {
1115 let range = |start, end| TextRange::new(TextSize::new(start), TextSize::new(end));
1116 let mapped = |docs: &Docs, start, end| {
1117 docs.find_ast_range(range(start, end)).map(|(range, _)| range.value)
1118 };
1119
1120 let docs = doc_attr_docs(r#"#[doc = "foo bar"] fn f() {}"#);
1122 assert_eq!(docs.docs, "foo bar");
1123 assert_eq!(mapped(&docs, 4, 7), Some(range(13, 16)));
1124
1125 let docs = doc_attr_docs(r##"#[doc = r"a\b"] fn f() {}"##);
1127 assert_eq!(docs.docs, r"a\b");
1128 assert_eq!(mapped(&docs, 2, 3), Some(range(12, 13)));
1129
1130 let docs = doc_attr_docs(r#"#[doc = "a\"b"] fn f() {}"#);
1133 assert_eq!(docs.docs, "a\"b");
1134 assert_eq!(mapped(&docs, 2, 3), None);
1135 }
1136}