@@ -201,14 +201,8 @@ impl Docs {
201201
202202 fn extend_with_doc_comment ( & mut self , comment : ast:: Comment , indent : & mut usize ) {
203203 let Some ( ( doc, offset) ) = comment. doc_comment ( ) else { return } ;
204- // Multiline block doc comments are usually decorated with a leading `*` on every line,
205- // and may be framed by lines of all `*`s. Mirror rustdoc's beautification.
206- let trim = match comment. kind ( ) . shape {
207- ast:: CommentShape :: Block => compute_block_doc_trim ( doc) ,
208- ast:: CommentShape :: Line => None ,
209- } ;
210204 let offset = comment. syntax ( ) . text_range ( ) . start ( ) + offset;
211- self . push_doc_lines ( doc, Some ( offset) , indent, trim . as_ref ( ) ) ;
205+ self . push_doc_lines ( doc, Some ( offset) , indent, Some ( comment . kind ( ) . shape ) ) ;
212206 }
213207
214208 fn extend_with_doc_attr ( & mut self , value : ast:: String , indent : & mut usize ) {
@@ -232,48 +226,83 @@ impl Docs {
232226 self . push_doc_lines ( doc, None , indent, None ) ;
233227 }
234228
235- /// `trim` is the vertical + horizontal trim for a block doc comment, as computed by
236- /// [`compute_block_doc_trim()`]. `None` means the doc is emitted verbatim (line docs, doc
237- /// attributes, macro-expanded doc strings).
229+ /// Beautifies `doc` and appends the result to `self.docs`, one line at a time via
230+ /// [`Docs::push_doc_line`]. Mirrors rustc's [`beautify_doc_string`] when `shape` is
231+ /// `Some`, delegating to [`get_vertical_trim`] and [`get_horizontal_trim`]. When `shape`
232+ /// is `None` (doc attributes and macro-expanded doc strings) the beautifier is skipped
233+ /// and every line is emitted verbatim.
234+ ///
235+ /// Individual `///` line comments always reach us as a single-line `doc`, so the
236+ /// `!doc.contains('\n')` fast path fires and the block-shape logic never runs on them.
237+ ///
238+ /// Unlike rustc's version, which joins the beautified lines into a new interned `Symbol`,
239+ /// this port carries each line's byte offset relative to `doc`'s start so
240+ /// [`Docs::push_doc_line`] can record accurate per-line source-map offsets.
241+ ///
242+ /// [`beautify_doc_string`]: https://github.com/rust-lang/rust/blob/16a623ad672a92409b5c04beb303583c6cf72a7e/compiler/rustc_ast/src/util/comments.rs#L37
238243 fn push_doc_lines (
239244 & mut self ,
240245 doc : & str ,
241- mut ast_offset : Option < TextSize > ,
246+ ast_offset : Option < TextSize > ,
242247 indent : & mut usize ,
243- trim : Option < & BlockDocTrim < ' _ > > ,
248+ shape : Option < ast :: CommentShape > ,
244249 ) {
245- for ( idx, raw) in doc. split ( '\n' ) . enumerate ( ) {
246- let source_len = TextSize :: of ( raw) ;
247-
248- // Vertical trim: drop lines outside `keep`, but keep advancing `ast_offset` by
249- // `source_len + "\n"` so later kept lines still map back to the right source byte.
250- let dropped = trim. is_some_and ( |t| !t. keep . contains ( & idx) ) ;
251- if !dropped {
252- // Horizontal trim: `*`, `* ` and `**` are decoration, but `*foo` is content.
253- let line = trim
254- . and_then ( |t| t. horizontal )
255- . and_then ( |prefix| raw. strip_prefix ( prefix) )
256- . filter ( |& rest| rest == "*" || rest. starts_with ( "* " ) || rest. starts_with ( "**" ) )
257- . map_or ( raw, |rest| & rest[ 1 ..] ) ;
258-
259- self . docs_source_map . push ( DocsSourceMapLine {
260- string_offset : TextSize :: of ( & self . docs ) ,
261- ast_offset : ast_offset. map ( |it| it + ( source_len - TextSize :: of ( line) ) ) ,
262- } ) ;
263-
264- let line = line. trim_end ( ) ;
265- if let Some ( line_indent) = line. chars ( ) . position ( |ch| !ch. is_whitespace ( ) ) {
266- // Empty lines are handled because `position()` returns `None` for them.
267- * indent = std:: cmp:: min ( * indent, line_indent) ;
250+ if !doc. contains ( '\n' ) {
251+ self . push_doc_line ( doc, ast_offset, indent) ;
252+ return ;
253+ }
254+
255+ // Each entry is `(line, offset_from_doc_start)`. The offset stays in sync with the
256+ // string as we strip its prefix, so the caller can add it to `ast_offset` for the
257+ // source map. Computed manually rather than via `str::substr_range` to stay compatible
258+ // with the workspace's MSRV.
259+ let mut lines: Vec < ( & str , TextSize ) > = Vec :: new ( ) ;
260+ let mut cursor = TextSize :: new ( 0 ) ;
261+ for line in doc. split ( '\n' ) {
262+ lines. push ( ( line, cursor) ) ;
263+ cursor += TextSize :: of ( line) + TextSize :: of ( "\n " ) ;
264+ }
265+
266+ let lines = match get_vertical_trim ( & lines) {
267+ Some ( ( i, j) ) => & mut lines[ i..j] ,
268+ None => & mut lines[ ..] ,
269+ } ;
270+ if let Some ( shape) = shape
271+ && let Some ( horizontal) = get_horizontal_trim ( lines, shape)
272+ {
273+ let horizontal_len = TextSize :: of ( horizontal. as_str ( ) ) ;
274+ // Strip `"[ \t]*\*"` from each line where present, exactly like rustc.
275+ for ( line, line_offset) in lines. iter_mut ( ) {
276+ if let Some ( rest) = line. strip_prefix ( horizontal. as_str ( ) ) {
277+ * line = rest;
278+ * line_offset += horizontal_len;
279+ if shape == ast:: CommentShape :: Block
280+ && ( * line == "*" || line. starts_with ( "* " ) || line. starts_with ( "**" ) )
281+ {
282+ * line = & line[ 1 ..] ;
283+ * line_offset += TextSize :: of ( "*" ) ;
284+ }
268285 }
269- self . docs . push_str ( line) ;
270- self . docs . push ( '\n' ) ;
271286 }
287+ }
272288
273- if let Some ( offset) = ast_offset. as_mut ( ) {
274- * offset += source_len + TextSize :: of ( "\n " ) ;
275- }
289+ for ( line, line_offset) in lines. iter ( ) . copied ( ) {
290+ self . push_doc_line ( line, ast_offset. map ( |it| it + line_offset) , indent) ;
291+ }
292+ }
293+
294+ /// Appends a single beautified line to `self.docs` and records its source-map row.
295+ fn push_doc_line ( & mut self , line : & str , ast_offset : Option < TextSize > , indent : & mut usize ) {
296+ self . docs_source_map
297+ . push ( DocsSourceMapLine { string_offset : TextSize :: of ( & self . docs ) , ast_offset } ) ;
298+
299+ let line = line. trim_end ( ) ;
300+ if let Some ( line_indent) = line. chars ( ) . position ( |ch| !ch. is_whitespace ( ) ) {
301+ // Empty lines are handled because `position()` returns `None` for them.
302+ * indent = std:: cmp:: min ( * indent, line_indent) ;
276303 }
304+ self . docs . push_str ( line) ;
305+ self . docs . push ( '\n' ) ;
277306 }
278307
279308 fn remove_indent ( & mut self , indent : usize , start_source_map_index : usize ) {
@@ -380,97 +409,76 @@ impl Docs {
380409 }
381410}
382411
383- /// The vertical and horizontal trim to apply to a block doc comment, mirroring rustdoc's
384- /// beautification. `keep` is the half-open range of surviving *input* line indices (relative to
385- /// `doc.split('\n')`); `horizontal` is the common `[ \t]*` prefix stripped from each kept line
386- /// before its `*` decoration, if the block is uniformly decorated.
387- #[ derive( Debug ) ]
388- struct BlockDocTrim < ' a > {
389- keep : std:: ops:: Range < usize > ,
390- horizontal : Option < & ' a str > ,
391- }
392-
393- /// Computes the trim to apply to a block doc comment. Returns `None` when no trimming would take
394- /// place, so the caller stays on the fast path (single-line blocks, blocks without a consistent
395- /// star column, etc.).
396- ///
397- /// Adapted from rustc's [`beautify_doc_string`], keeping the `CommentKind::Block` behavior only.
398- /// The port operates on borrowed line slices instead of interning a new `Symbol`, so
399- /// [`Docs::push_doc_lines`] can compute per-line source-map offsets.
400- ///
401- /// [`beautify_doc_string`]: https://github.com/rust-lang/rust/blob/16a623ad672a92409b5c04beb303583c6cf72a7e/compiler/rustc_ast/src/util/comments.rs#L37
402- fn compute_block_doc_trim ( doc : & str ) -> Option < BlockDocTrim < ' _ > > {
403- if !doc. contains ( '\n' ) {
404- // Single-line block doc comments are left alone, matching rustdoc.
405- return None ;
412+ /// Copied verbatim from rustc's [`beautify_doc_string`] (`compiler/rustc_ast/src/util/comments.rs`
413+ /// @ [`16a623ad`](https://github.com/rust-lang/rust/blob/16a623ad672a92409b5c04beb303583c6cf72a7e/compiler/rustc_ast/src/util/comments.rs#L38)),
414+ /// except that the input is `&[(&str, TextSize)]` — the paired offset is ignored here.
415+ fn get_vertical_trim ( lines : & [ ( & str , TextSize ) ] ) -> Option < ( usize , usize ) > {
416+ let mut i = 0 ;
417+ let mut j = lines. len ( ) ;
418+ // first line of all-stars should be omitted
419+ if lines. first ( ) . is_some_and ( |( line, _) | !line. is_empty ( ) && line. chars ( ) . all ( |c| c == '*' ) ) {
420+ i += 1 ;
406421 }
407- let lines: Vec < & str > = doc. split ( '\n' ) . collect ( ) ;
408422
409- // Vertical trim: drop leading/trailing all-`*` fence lines (rustc `get_vertical_trim`).
410- let mut v_start = 0usize ;
411- let mut v_end = lines. len ( ) ;
412- if lines[ v_start] . chars ( ) . all ( |c| c == '*' ) && !lines[ v_start] . is_empty ( ) {
413- v_start += 1 ;
414- }
415- if v_end > v_start && !lines[ v_end - 1 ] . is_empty ( ) && lines[ v_end - 1 ] . chars ( ) . all ( |c| c == '*' )
416- {
417- v_end -= 1 ;
423+ // like the first, a last line of all stars should be omitted
424+ if j > i && !lines[ j - 1 ] . 0 . is_empty ( ) && lines[ j - 1 ] . 0 . chars ( ) . all ( |c| c == '*' ) {
425+ j -= 1 ;
418426 }
419427
420- // Horizontal trim: only scan lines that carry decoration. Skip the first kept line (it
421- // follows `/**` and typically contains prose) unless it already begins with `*`, and skip
422- // whitespace-only lines at the boundaries.
423- let horizontal = {
424- let kept = & lines[ v_start..v_end] ;
425- let mut i =
426- kept. first ( ) . map ( |l| if l. trim_start ( ) . starts_with ( '*' ) { 0 } else { 1 } ) . unwrap_or ( 0 ) ;
427- let mut j = kept. len ( ) ;
428- while i < j && kept[ i] . trim ( ) . is_empty ( ) {
429- i += 1 ;
430- }
431- while j > i && kept[ j - 1 ] . trim ( ) . is_empty ( ) {
432- j -= 1 ;
433- }
434- let scan = & kept[ i..j] ;
435-
436- // Only whitespace + `*` is allowed in the prefix, and the `*` must sit at the same
437- // column on every scanned line. Empty scan means no decoration to strip.
438- let mut prefix_col: Option < usize > = None ;
439- let mut ok = !scan. is_empty ( ) ;
440- ' outer: for line in scan {
441- let mut hit_star = false ;
442- for ( col, c) in line. chars ( ) . enumerate ( ) {
443- if prefix_col. is_some_and ( |p| col > p) || !matches ! ( c, '*' | ' ' | '\t' ) {
444- ok = false ;
445- break ' outer;
446- }
447- if c == '*' {
448- match prefix_col {
449- None => prefix_col = Some ( col) ,
450- Some ( p) if p != col => {
451- ok = false ;
452- break ' outer;
453- }
454- _ => { }
455- }
456- hit_star = true ;
457- break ;
458- }
428+ if i != 0 || j != lines. len ( ) { Some ( ( i, j) ) } else { None }
429+ }
430+
431+ /// Copied verbatim from rustc's [`beautify_doc_string`] (`compiler/rustc_ast/src/util/comments.rs`
432+ /// @ [`16a623ad`](https://github.com/rust-lang/rust/blob/16a623ad672a92409b5c04beb303583c6cf72a7e/compiler/rustc_ast/src/util/comments.rs#L54)),
433+ /// except that the input is `&[(&str, TextSize)]` (the offset is ignored) and the return type is
434+ /// `Option<String>` for parity with the upstream signature.
435+ fn get_horizontal_trim ( lines : & [ ( & str , TextSize ) ] , kind : ast:: CommentShape ) -> Option < String > {
436+ let mut i = usize:: MAX ;
437+ let mut first = true ;
438+
439+ // In case we have doc comments like `/**` or `/*!`, we want to remove stars if they are
440+ // present. However, we first need to strip the empty lines so they don't get in the middle
441+ // when we try to compute the "horizontal trim".
442+ let lines = match kind {
443+ ast:: CommentShape :: Block => {
444+ // Whatever happens, we skip the first line.
445+ let mut i = lines
446+ . first ( )
447+ . map ( |( l, _) | if l. trim_start ( ) . starts_with ( '*' ) { 0 } else { 1 } )
448+ . unwrap_or ( 0 ) ;
449+ let mut j = lines. len ( ) ;
450+
451+ while i < j && lines[ i] . 0 . trim ( ) . is_empty ( ) {
452+ i += 1 ;
459453 }
460- if !hit_star {
461- // A scanned line without a `*` at the expected column means the block is not
462- // uniformly decorated.
463- ok = false ;
464- break ;
454+ while j > i && lines[ j - 1 ] . 0 . trim ( ) . is_empty ( ) {
455+ j -= 1 ;
465456 }
457+ & lines[ i..j]
466458 }
467- prefix_col . filter ( |_| ok ) . map ( |p| & scan [ 0 ] [ ..p ] )
459+ ast :: CommentShape :: Line => lines ,
468460 } ;
469461
470- if v_start == 0 && v_end == lines. len ( ) && horizontal. is_none ( ) {
471- return None ;
462+ for ( line, _) in lines {
463+ for ( j, c) in line. chars ( ) . enumerate ( ) {
464+ if j > i || !"* \t " . contains ( c) {
465+ return None ;
466+ }
467+ if c == '*' {
468+ if first {
469+ i = j;
470+ first = false ;
471+ } else if i != j {
472+ return None ;
473+ }
474+ break ;
475+ }
476+ }
477+ if i >= line. len ( ) {
478+ return None ;
479+ }
472480 }
473- Some ( BlockDocTrim { keep : v_start..v_end , horizontal } )
481+ Some ( lines . first ( ) ? . 0 [ ..i ] . to_string ( ) )
474482}
475483
476484struct DocMacroExpander < ' db > {
@@ -925,13 +933,17 @@ mod tests {
925933 }
926934
927935 // The decoration is stripped, but markdown bullets and `*foo` are content.
936+ // `*bar` doesn't start with `* ` / `**`, so rustc's beautifier only strips the
937+ // horizontal `[ \t]*` prefix (here a single space) and leaves the leading `*` in
938+ // place. That in turn pins the block's minimum indent at 0, so surrounding lines
939+ // aren't re-indented.
928940 check (
929941 "/**\n * foo\n *\n * * bullet\n *bar\n */" ,
930942 expect ! [ [ r#"
931943
932- foo
944+ foo
933945
934- * bullet
946+ * bullet
935947 *bar
936948 "# ] ] ,
937949 ) ;
0 commit comments