Skip to main content

forge_fmt/state/
mod.rs

1#![allow(clippy::too_many_arguments)]
2use crate::{
3    FormatterConfig, InlineConfig,
4    pp::{self, BreakToken, SIZE_INFINITY, Token},
5    state::sol::BinOpGroup,
6};
7use foundry_common::{
8    comments::{Comment, CommentStyle, Comments, estimate_line_width, line_with_tabs},
9    iter::IterDelimited,
10};
11use foundry_config::fmt::{DocCommentStyle, IndentStyle};
12use solar::parse::{
13    ast::{self, Span},
14    interface::{BytePos, SourceMap},
15    token,
16};
17use std::{borrow::Cow, ops::Deref, sync::Arc};
18
19mod common;
20mod sol;
21mod yul;
22
23/// Specifies the nature of a complex call.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub(super) enum CallContextKind {
26    /// A chained method call, `a().b()`.
27    Chained,
28
29    /// A nested function call, `a(b())`.
30    Nested,
31}
32
33/// Formatting context for a call expression.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub(super) struct CallContext {
36    /// The kind call.
37    pub(super) kind: CallContextKind,
38
39    /// The size of the callee's head, excluding its arguments.
40    pub(super) size: usize,
41
42    /// Whether this chain context added its own indentation box.
43    pub(super) has_indent: bool,
44}
45
46impl CallContext {
47    pub(super) fn nested(size: usize) -> Self {
48        Self { kind: CallContextKind::Nested, size, has_indent: false }
49    }
50
51    pub(super) fn chained(size: usize, has_indent: bool) -> Self {
52        Self { kind: CallContextKind::Chained, size, has_indent }
53    }
54
55    pub(super) fn is_nested(&self) -> bool {
56        matches!(self.kind, CallContextKind::Nested)
57    }
58
59    pub(super) fn is_chained(&self) -> bool {
60        matches!(self.kind, CallContextKind::Chained)
61    }
62}
63
64#[derive(Debug, Default)]
65pub(super) struct CallStack {
66    stack: Vec<CallContext>,
67}
68
69impl Deref for CallStack {
70    type Target = [CallContext];
71    fn deref(&self) -> &Self::Target {
72        &self.stack
73    }
74}
75
76impl CallStack {
77    pub(crate) fn push(&mut self, call: CallContext) {
78        self.stack.push(call);
79    }
80
81    pub(crate) fn pop(&mut self) -> Option<CallContext> {
82        self.stack.pop()
83    }
84
85    pub(crate) fn is_nested(&self) -> bool {
86        self.last().is_some_and(|call| call.is_nested())
87    }
88
89    /// Returns true if any chained call in the stack has its own indentation.
90    /// Used to determine if commasep should skip its own indentation (to avoid double indent).
91    pub(crate) fn has_chain_with_indent(&self) -> bool {
92        self.stack.iter().any(|call| call.is_chained() && call.has_indent)
93    }
94}
95
96pub(super) struct State<'sess, 'ast> {
97    // CORE COMPONENTS
98    pub(super) s: pp::Printer,
99    ind: isize,
100
101    sm: &'sess SourceMap,
102    pub(super) comments: Comments,
103    config: Arc<FormatterConfig>,
104    inline_config: InlineConfig<()>,
105    cursor: SourcePos,
106
107    // FORMATTING CONTEXT:
108    // Whether the source file uses CRLF (`\r\n`) line endings.
109    has_crlf: bool,
110    // The current contract being formatted, if inside a contract definition.
111    contract: Option<&'ast ast::ItemContract<'ast>>,
112    // Current block nesting depth (incremented for each `{...}` block entered).
113    block_depth: usize,
114    // Stack tracking nested and chained function calls.
115    call_stack: CallStack,
116
117    // Whether the current statement should be formatted as a single line, or not.
118    single_line_stmt: Option<bool>,
119    // The current binary expression chain context, if inside one.
120    binary_expr: Option<BinOpGroup>,
121    // Whether inside a `return` statement that contains a binary expression, or not.
122    return_bin_expr: bool,
123    // Whether inside a call with call options and at least one argument.
124    call_with_opts_and_args: bool,
125    // Whether to skip the index soft breaks because the callee fits inline.
126    skip_index_break: bool,
127    // Whether inside an `emit` or `revert` call with a qualified path, or not.
128    emit_or_revert: bool,
129    // Whether inside a variable initialization expression, or not.
130    var_init: bool,
131}
132
133impl std::ops::Deref for State<'_, '_> {
134    type Target = pp::Printer;
135
136    #[inline(always)]
137    fn deref(&self) -> &Self::Target {
138        &self.s
139    }
140}
141
142impl std::ops::DerefMut for State<'_, '_> {
143    #[inline(always)]
144    fn deref_mut(&mut self) -> &mut Self::Target {
145        &mut self.s
146    }
147}
148
149struct SourcePos {
150    pos: BytePos,
151    enabled: bool,
152}
153
154impl SourcePos {
155    pub(super) fn advance(&mut self, bytes: u32) {
156        self.pos += BytePos(bytes);
157    }
158
159    pub(super) fn advance_to(&mut self, pos: BytePos, enabled: bool) {
160        self.pos = std::cmp::max(pos, self.pos);
161        self.enabled = enabled;
162    }
163
164    pub(super) fn next_line(&mut self, is_at_crlf: bool) {
165        self.pos += if is_at_crlf { 2 } else { 1 };
166    }
167
168    pub(super) fn span(&self, to: BytePos) -> Span {
169        Span::new(self.pos, to)
170    }
171}
172
173pub(super) enum Separator {
174    Nbsp,
175    Space,
176    Hardbreak,
177    SpaceOrNbsp(bool),
178}
179
180impl Separator {
181    fn print(&self, p: &mut pp::Printer, cursor: &mut SourcePos, is_at_crlf: bool) {
182        match self {
183            Self::Nbsp => p.nbsp(),
184            Self::Space => p.space(),
185            Self::Hardbreak => p.hardbreak(),
186            Self::SpaceOrNbsp(breaks) => p.space_or_nbsp(*breaks),
187        }
188
189        cursor.next_line(is_at_crlf);
190    }
191}
192
193/// Generic methods
194impl<'sess> State<'sess, '_> {
195    pub(super) fn new(
196        sm: &'sess SourceMap,
197        config: Arc<FormatterConfig>,
198        inline_config: InlineConfig<()>,
199        comments: Comments,
200    ) -> Self {
201        Self {
202            s: pp::Printer::new(
203                config.line_length,
204                if matches!(config.style, IndentStyle::Tab) {
205                    Some(config.tab_width)
206                } else {
207                    None
208                },
209            ),
210            ind: config.tab_width as isize,
211            sm,
212            comments,
213            config,
214            inline_config,
215            cursor: SourcePos { pos: BytePos::from_u32(0), enabled: true },
216            has_crlf: false,
217            contract: None,
218            single_line_stmt: None,
219            call_with_opts_and_args: false,
220            skip_index_break: false,
221            binary_expr: None,
222            return_bin_expr: false,
223            emit_or_revert: false,
224            var_init: false,
225            block_depth: 0,
226            call_stack: CallStack::default(),
227        }
228    }
229
230    /// Checks a span of the source for a carriage return (`\r`) to determine if the file
231    /// uses CRLF line endings.
232    ///
233    /// If a `\r` is found, `self.has_crlf` is set to `true`. This is intended to be
234    /// called once at the beginning of the formatting process for efficiency.
235    fn check_crlf(&mut self, span: Span) {
236        if let Ok(snip) = self.sm.span_to_snippet(span)
237            && snip.contains('\r')
238        {
239            self.has_crlf = true;
240        }
241    }
242
243    /// Checks if the cursor is currently positioned at the start of a CRLF sequence (`\r\n`).
244    /// The check is only meaningful if `self.has_crlf` is true.
245    fn is_at_crlf(&self) -> bool {
246        self.has_crlf && self.char_at(self.cursor.pos) == Some('\r')
247    }
248
249    /// Computes the space left, bounded by the max space left.
250    fn space_left(&self) -> usize {
251        std::cmp::min(self.s.space_left(), self.max_space_left(0))
252    }
253
254    /// Computes the maximum space left given the context information available:
255    /// `block_depth`, `tab_width`, and a user-defined unavailable size `prefix_len`.
256    fn max_space_left(&self, prefix_len: usize) -> usize {
257        self.config
258            .line_length
259            .saturating_sub(self.block_depth * self.config.tab_width + prefix_len)
260    }
261
262    fn break_offset_if_not_bol(&mut self, n: usize, off: isize, search: bool) {
263        // When searching, the break token is expected to be inside a closed box. Thus, we will
264        // traverse the buffer and evaluate the first non-end token.
265        if search {
266            // We do something pretty sketchy here: tuck the nonzero offset-adjustment we
267            // were going to deposit along with the break into the previous hardbreak.
268            self.find_and_replace_last_token_still_buffered(
269                pp::Printer::hardbreak_tok_offset(off),
270                |token| token.is_hardbreak(),
271            );
272            return;
273        }
274
275        // When not explicitly searching, the break token is expected to be the last token.
276        if !self.is_beginning_of_line() {
277            self.break_offset(n, off)
278        } else if off != 0
279            && let Some(last_token) = self.last_token_still_buffered()
280            && last_token.is_hardbreak()
281        {
282            // We do something pretty sketchy here: tuck the nonzero offset-adjustment we
283            // were going to deposit along with the break into the previous hardbreak.
284            self.replace_last_token_still_buffered(pp::Printer::hardbreak_tok_offset(off));
285        }
286    }
287
288    fn braces_break(&mut self) {
289        if self.config.bracket_spacing {
290            self.space();
291        } else {
292            self.zerobreak();
293        }
294    }
295}
296
297/// Span to source.
298impl State<'_, '_> {
299    fn char_at(&self, pos: BytePos) -> Option<char> {
300        let res = self.sm.lookup_byte_offset(pos);
301        res.sf.src.get(res.pos.to_usize()..)?.chars().next()
302    }
303
304    fn print_span(&mut self, span: Span) {
305        match self.sm.span_to_snippet(span) {
306            Ok(s) => self.s.word(if matches!(self.config.style, IndentStyle::Tab) {
307                snippet_with_tabs(s, self.config.tab_width)
308            } else {
309                s
310            }),
311            Err(e) => panic!("failed to print {span:?}: {e:#?}"),
312        }
313        // Drop comments that are included in the span.
314        while let Some(cmnt) = self.peek_comment() {
315            if cmnt.pos() >= span.hi() {
316                break;
317            }
318            let _ = self.next_comment().unwrap();
319        }
320        // Update cursor
321        self.cursor.advance_to(span.hi(), false);
322    }
323
324    /// Returns `true` if the span is disabled and has been printed as-is.
325    #[must_use]
326    fn handle_span(&mut self, span: Span, skip_prev_cmnts: bool) -> bool {
327        if !skip_prev_cmnts {
328            self.print_comments(span.lo(), CommentConfig::default());
329        }
330        self.print_span_if_disabled(span)
331    }
332
333    /// Returns `true` if the span is disabled and has been printed as-is.
334    #[inline]
335    #[must_use]
336    fn print_span_if_disabled(&mut self, span: Span) -> bool {
337        let cursor_span = self.cursor.span(span.hi());
338        if self.inline_config.is_disabled(cursor_span) {
339            self.print_span_cold(cursor_span);
340            return true;
341        }
342        if self.inline_config.is_disabled(span) {
343            self.print_span_cold(span);
344            return true;
345        }
346        false
347    }
348
349    #[cold]
350    fn print_span_cold(&mut self, span: Span) {
351        self.print_span(span);
352    }
353
354    fn print_tokens(&mut self, tokens: &[token::Token]) {
355        // Leave unchanged.
356        let span = Span::join_first_last(tokens.iter().map(|t| t.span));
357        self.print_span(span);
358    }
359
360    fn print_word(&mut self, w: impl Into<Cow<'static, str>>) {
361        let cow = w.into();
362        self.cursor.advance(cow.len() as u32);
363        self.word(cow);
364    }
365
366    fn print_sep(&mut self, sep: Separator) {
367        if self.handle_span(
368            self.cursor.span(self.cursor.pos + if self.is_at_crlf() { 2 } else { 1 }),
369            true,
370        ) {
371            return;
372        }
373
374        self.print_sep_unhandled(sep);
375    }
376
377    fn print_sep_unhandled(&mut self, sep: Separator) {
378        let is_at_crlf = self.is_at_crlf();
379        sep.print(&mut self.s, &mut self.cursor, is_at_crlf);
380    }
381
382    fn print_ident(&mut self, ident: &ast::Ident) {
383        if self.handle_span(ident.span, true) {
384            return;
385        }
386
387        self.print_comments(ident.span.lo(), CommentConfig::skip_ws());
388        self.word(ident.to_string());
389    }
390
391    fn print_inside_parens<F>(&mut self, f: F)
392    where
393        F: FnOnce(&mut Self),
394    {
395        self.print_word("(");
396        f(self);
397        self.print_word(")");
398    }
399
400    fn estimate_size(&self, span: Span) -> usize {
401        if let Ok(snip) = self.sm.span_to_snippet(span) {
402            let (mut size, mut first, mut prev_needs_space) = (0, true, false);
403
404            for line in snip.lines() {
405                let line = line.trim();
406
407                if prev_needs_space {
408                    size += 1;
409                } else if !first && let Some(char) = line.chars().next() {
410                    // A line break or a space are required if this line:
411                    // - starts with an operator.
412                    // - starts with one of the ternary operators
413                    // - starts with a bracket and fmt config forces bracket spacing.
414                    match char {
415                        '&' | '|' | '=' | '>' | '<' | '+' | '-' | '*' | '/' | '%' | '^' | '?'
416                        | ':' => size += 1,
417                        '}' | ')' | ']' if self.config.bracket_spacing => size += 1,
418                        _ => (),
419                    }
420                }
421                first = false;
422
423                // trim spaces before and after mixed comments
424                let mut search = line;
425                loop {
426                    if let Some((lhs, comment)) = search.split_once(r#"/*"#) {
427                        size += lhs.trim_end().len() + 2;
428                        search = comment;
429                    } else if let Some((comment, rhs)) = search.split_once(r#"*/"#) {
430                        size += comment.len() + 2;
431                        search = rhs;
432                    } else {
433                        size += search.trim().len();
434                        break;
435                    }
436                }
437
438                // Next line requires a line break if this one:
439                // - ends with a bracket and fmt config forces bracket spacing.
440                // - ends with ',' a line break or a space are required.
441                // - ends with ';' a line break is required.
442                prev_needs_space = match line.chars().next_back() {
443                    Some('[') | Some('(') | Some('{') => self.config.bracket_spacing,
444                    Some(',') | Some(';') => true,
445                    _ => false,
446                };
447            }
448            return size;
449        }
450
451        span.to_range().len()
452    }
453
454    fn same_source_line(&self, a: BytePos, b: BytePos) -> bool {
455        self.sm.lookup_char_pos(a).line == self.sm.lookup_char_pos(b).line
456    }
457}
458
459/// Comment-related methods.
460impl<'sess> State<'sess, '_> {
461    /// Returns `None` if the span is disabled and has been printed as-is.
462    #[must_use]
463    fn handle_comment(&mut self, cmnt: Comment, skip_break: bool) -> Option<Comment> {
464        if self.cursor.enabled {
465            if self.inline_config.is_disabled(cmnt.span) {
466                if cmnt.style.is_trailing() && !self.last_token_is_space() {
467                    self.nbsp();
468                }
469                self.print_span_cold(cmnt.span);
470                if !skip_break && (cmnt.style.is_isolated() || cmnt.style.is_trailing()) {
471                    self.print_sep(Separator::Hardbreak);
472                }
473                return None;
474            }
475        } else if self.print_span_if_disabled(cmnt.span) {
476            if !skip_break && (cmnt.style.is_isolated() || cmnt.style.is_trailing()) {
477                self.print_sep(Separator::Hardbreak);
478            }
479            return None;
480        }
481        Some(cmnt)
482    }
483
484    fn cmnt_config(&self) -> CommentConfig {
485        CommentConfig { ..Default::default() }
486    }
487
488    fn print_docs(&mut self, docs: &'_ ast::DocComments<'_>) {
489        // Intetionally no-op. Handled with `self.comments`.
490        let _ = docs;
491    }
492
493    /// Prints comments that are before the given position.
494    ///
495    /// Returns `Some` with the style of the last comment printed, or `None` if no comment was
496    /// printed.
497    fn print_comments(&mut self, pos: BytePos, mut config: CommentConfig) -> Option<CommentStyle> {
498        let mut last_style: Option<CommentStyle> = None;
499        let mut is_leading = true;
500        let config_cache = config;
501        let mut buffered_blank = None;
502        while self.peek_comment().is_some_and(|c| c.pos() < pos) {
503            let mut cmnt = self.next_comment().unwrap();
504            let style_cache = cmnt.style;
505
506            // Merge consecutive line doc comments when converting to block style
507            if self.config.docs_style == foundry_config::fmt::DocCommentStyle::Block
508                && cmnt.is_doc
509                && cmnt.kind == ast::CommentKind::Line
510            {
511                let mut ref_line = self.sm.lookup_char_pos(cmnt.span.hi()).line;
512                while let Some(next_cmnt) = self.peek_comment() {
513                    if !next_cmnt.is_doc
514                        || next_cmnt.kind != ast::CommentKind::Line
515                        || ref_line + 1 != self.sm.lookup_char_pos(next_cmnt.span.lo()).line
516                    {
517                        break;
518                    }
519
520                    let next_to_merge = self.next_comment().unwrap();
521                    cmnt.lines.extend(next_to_merge.lines);
522                    cmnt.span = cmnt.span.to(next_to_merge.span);
523                    ref_line += 1;
524                }
525            }
526
527            // Ensure breaks are never skipped when there are multiple comments
528            if self.peek_comment_before(pos).is_some() {
529                config.iso_no_break = false;
530                config.trailing_no_break = false;
531            }
532
533            // Handle disabled comments
534            let Some(cmnt) = self.handle_comment(
535                cmnt,
536                if style_cache.is_isolated() {
537                    config.iso_no_break
538                } else {
539                    config.trailing_no_break
540                },
541            ) else {
542                last_style = Some(style_cache);
543                continue;
544            };
545
546            if cmnt.style.is_blank() {
547                match config.skip_blanks {
548                    Some(Skip::All) => continue,
549                    Some(Skip::Leading { resettable: true }) if is_leading => continue,
550                    Some(Skip::Leading { resettable: false }) if last_style.is_none() => continue,
551                    Some(Skip::Trailing) => {
552                        buffered_blank = Some(cmnt);
553                        continue;
554                    }
555                    _ => (),
556                }
557            // Never print blank lines after docs comments
558            } else if !cmnt.is_doc {
559                is_leading = false;
560            }
561
562            if let Some(blank) = buffered_blank.take() {
563                self.print_comment(blank, config);
564            }
565
566            // Handle mixed with follow-up comment
567            if cmnt.style.is_mixed() {
568                if let Some(cmnt) = self.peek_comment_before(pos) {
569                    config.mixed_no_break_prev = true;
570                    config.mixed_no_break_post = true;
571                    config.mixed_post_nbsp = cmnt.style.is_mixed();
572                }
573
574                // Ensure consecutive mixed comments don't have a double-space
575                if last_style.is_some_and(|s| s.is_mixed()) {
576                    config.mixed_no_break_prev = true;
577                    config.mixed_no_break_post = true;
578                    config.mixed_prev_space = false;
579                }
580            } else if config.offset != 0
581                && cmnt.style.is_isolated()
582                && last_style.is_some_and(|s| s.is_isolated())
583            {
584                self.offset(config.offset);
585            }
586
587            last_style = Some(cmnt.style);
588            self.print_comment(cmnt, config);
589            config = config_cache;
590        }
591        last_style
592    }
593
594    /// Prints a line, wrapping it if it starts with the given prefix.
595    fn print_wrapped_line(
596        &mut self,
597        line: &str,
598        prefix: &'static str,
599        break_offset: isize,
600        is_doc: bool,
601    ) {
602        if !line.starts_with(prefix) {
603            self.word(line.to_owned());
604            return;
605        }
606
607        fn post_break_prefix(prefix: &'static str, has_content: bool) -> &'static str {
608            if !has_content {
609                return prefix;
610            }
611            match prefix {
612                "///" => "/// ",
613                "//" => "// ",
614                "/*" => "/* ",
615                " *" => " * ",
616                _ => prefix,
617            }
618        }
619
620        self.ibox(0);
621        self.word(prefix);
622
623        let content = &line[prefix.len()..];
624        let content = if is_doc {
625            // Doc comments preserve leading whitespaces (right after the prefix) as nbps.
626            let ws_len = content
627                .char_indices()
628                .take_while(|(_, c)| c.is_whitespace())
629                .last()
630                .map_or(0, |(idx, c)| idx + c.len_utf8());
631            let (leading_ws, rest) = content.split_at(ws_len);
632            if !leading_ws.is_empty() {
633                self.word(leading_ws.to_owned());
634            }
635            rest
636        } else {
637            // Non-doc comments: replace first whitespace with nbsp, rest of content continues
638            if let Some(first_char) = content.chars().next() {
639                if first_char.is_whitespace() {
640                    self.nbsp();
641                    &content[first_char.len_utf8()..]
642                } else {
643                    content
644                }
645            } else {
646                ""
647            }
648        };
649
650        let post_break = post_break_prefix(prefix, !content.is_empty());
651
652        // Process content character by character to preserve consecutive whitespaces
653        let (mut chars, mut current_word) = (content.chars().peekable(), String::new());
654        while let Some(ch) = chars.next() {
655            if ch.is_whitespace() {
656                // Print current word
657                if !current_word.is_empty() {
658                    self.word(std::mem::take(&mut current_word));
659                }
660
661                // Preserve multiple spaces while adding a single break
662                let mut ws_count = 1;
663                while chars.peek().is_some_and(|c| c.is_whitespace()) {
664                    ws_count += 1;
665                    chars.next();
666                }
667                self.s.scan_break(BreakToken {
668                    offset: break_offset,
669                    blank_space: ws_count,
670                    post_break: if post_break.starts_with("/*") { None } else { Some(post_break) },
671                    ..Default::default()
672                });
673                continue;
674            }
675
676            current_word.push(ch);
677        }
678
679        // Print final word
680        if !current_word.is_empty() {
681            self.word(current_word);
682        }
683
684        self.end();
685    }
686
687    /// Merges consecutive line comments to avoid orphan words.
688    fn merge_comment_lines(&self, lines: &[String], prefix: &str) -> Vec<String> {
689        // Do not apply smart merging to block comments
690        if lines.is_empty() || lines.len() < 2 || !prefix.starts_with("//") {
691            return lines.to_vec();
692        }
693
694        let mut result = Vec::new();
695        let mut i = 0;
696
697        while i < lines.len() {
698            let current_line = &lines[i];
699
700            // Keep empty lines, and non-prefixed lines, untouched
701            if current_line.trim().is_empty() || !current_line.starts_with(prefix) {
702                result.push(current_line.clone());
703                i += 1;
704                continue;
705            }
706
707            if i + 1 < lines.len() {
708                let next_line = &lines[i + 1];
709
710                // Check if next line is has the same prefix and is not empty
711                if next_line.starts_with(prefix) && !next_line.trim().is_empty() {
712                    // Only merge if the current line doesn't fit within available width
713                    if estimate_line_width(current_line, self.config.tab_width) > self.space_left()
714                    {
715                        // Merge the lines and let the wrapper handle breaking if needed
716                        let merged_line = format!(
717                            "{current_line} {next_content}",
718                            next_content = &next_line[prefix.len()..].trim_start()
719                        );
720                        result.push(merged_line);
721
722                        // Skip both lines since they are merged
723                        i += 2;
724                        continue;
725                    }
726                }
727            }
728
729            // No merge possible, keep the line as-is
730            result.push(current_line.clone());
731            i += 1;
732        }
733
734        result
735    }
736
737    fn print_comment(&mut self, mut cmnt: Comment, mut config: CommentConfig) {
738        self.cursor.advance_to(cmnt.span.hi(), true);
739
740        if cmnt.is_doc {
741            cmnt = style_doc_comment(self.config.docs_style, cmnt);
742        }
743
744        match cmnt.style {
745            CommentStyle::Mixed => {
746                let Some(prefix) = cmnt.prefix() else { return };
747                let never_break = self.last_token_is_neverbreak();
748                if !self.is_bol_or_only_ind() {
749                    match (never_break || config.mixed_no_break_prev, config.mixed_prev_space) {
750                        (false, true) => config.space(&mut self.s),
751                        (false, false) => config.zerobreak(&mut self.s),
752                        (true, true) => self.nbsp(),
753                        (true, false) => (),
754                    };
755                }
756                if self.config.wrap_comments {
757                    // Merge and wrap comments
758                    let merged_lines = self.merge_comment_lines(&cmnt.lines, prefix);
759                    for (pos, line) in merged_lines.into_iter().delimited() {
760                        self.print_wrapped_line(&line, prefix, 0, cmnt.is_doc);
761                        if !pos.is_last {
762                            self.hardbreak();
763                        }
764                    }
765                } else {
766                    // No wrapping, print as-is
767                    for (pos, line) in cmnt.lines.into_iter().delimited() {
768                        self.word(line);
769                        if !pos.is_last {
770                            self.hardbreak();
771                        }
772                    }
773                }
774                if config.mixed_post_nbsp {
775                    config.nbsp_or_space(self.config.wrap_comments, &mut self.s);
776                    self.cursor.advance(1);
777                } else if !config.mixed_no_break_post {
778                    config.space(&mut self.s);
779                    self.cursor.advance(1);
780                }
781            }
782            CommentStyle::Isolated => {
783                let Some(mut prefix) = cmnt.prefix() else { return };
784                if !config.iso_no_break {
785                    config.hardbreak_if_not_bol(self.is_bol_or_only_ind(), &mut self.s);
786                }
787
788                if self.config.wrap_comments {
789                    // Merge and wrap comments
790                    let merged_lines = self.merge_comment_lines(&cmnt.lines, prefix);
791                    for (pos, line) in merged_lines.into_iter().delimited() {
792                        let hb = |this: &mut Self| {
793                            this.hardbreak();
794                            if pos.is_last {
795                                this.cursor.next_line(this.is_at_crlf());
796                            }
797                        };
798                        if line.is_empty() {
799                            hb(self);
800                            continue;
801                        }
802                        if pos.is_first {
803                            self.ibox(config.offset);
804                            if cmnt.is_doc && matches!(prefix, "/**") {
805                                self.word(prefix);
806                                hb(self);
807                                prefix = " * ";
808                                continue;
809                            }
810                        }
811
812                        self.print_wrapped_line(&line, prefix, 0, cmnt.is_doc);
813
814                        if pos.is_last {
815                            self.end();
816                            if !config.iso_no_break {
817                                hb(self);
818                            }
819                        } else {
820                            hb(self);
821                        }
822                    }
823                } else {
824                    // No wrapping, print as-is
825                    for (pos, line) in cmnt.lines.into_iter().delimited() {
826                        let hb = |this: &mut Self| {
827                            this.hardbreak();
828                            if pos.is_last {
829                                this.cursor.next_line(this.is_at_crlf());
830                            }
831                        };
832                        if line.is_empty() {
833                            hb(self);
834                            continue;
835                        }
836                        if pos.is_first {
837                            self.ibox(config.offset);
838                            if cmnt.is_doc && matches!(prefix, "/**") {
839                                self.word(prefix);
840                                hb(self);
841                                prefix = " * ";
842                                continue;
843                            }
844                        }
845
846                        self.word(line);
847
848                        if pos.is_last {
849                            self.end();
850                            if !config.iso_no_break {
851                                hb(self);
852                            }
853                        } else {
854                            hb(self);
855                        }
856                    }
857                }
858            }
859            CommentStyle::Trailing => {
860                let Some(prefix) = cmnt.prefix() else { return };
861                self.neverbreak();
862                if !self.is_bol_or_only_ind() {
863                    self.nbsp();
864                }
865
866                if !self.config.wrap_comments && cmnt.lines.len() == 1 {
867                    self.word(cmnt.lines.pop().unwrap());
868                } else if self.config.wrap_comments {
869                    if cmnt.is_doc || matches!(cmnt.kind, ast::CommentKind::Line) {
870                        config.offset = 0;
871                    } else {
872                        config.offset = self.ind;
873                    }
874                    for (lpos, line) in cmnt.lines.into_iter().delimited() {
875                        if !line.is_empty() {
876                            self.print_wrapped_line(&line, prefix, config.offset, cmnt.is_doc);
877                        }
878                        if !lpos.is_last {
879                            config.hardbreak(&mut self.s);
880                        }
881                    }
882                } else {
883                    self.visual_align();
884                    for (pos, line) in cmnt.lines.into_iter().delimited() {
885                        if !line.is_empty() {
886                            self.word(line);
887                            if !pos.is_last {
888                                self.hardbreak();
889                            }
890                        }
891                    }
892                    self.end();
893                }
894
895                if !config.trailing_no_break {
896                    self.print_sep(Separator::Hardbreak);
897                }
898            }
899
900            CommentStyle::BlankLine => {
901                // Pre-requisite: ensure that blank links are printed at the beginning of new line.
902                if !self.last_token_is_break() && !self.is_bol_or_only_ind() {
903                    config.hardbreak(&mut self.s);
904                    self.cursor.next_line(self.is_at_crlf());
905                }
906
907                // We need to do at least one, possibly two hardbreaks.
908                let twice = match self.last_token() {
909                    Some(Token::String(s)) => ";" == s,
910                    Some(Token::Begin(_)) => true,
911                    Some(Token::End) => true,
912                    _ => false,
913                };
914                if twice {
915                    config.hardbreak(&mut self.s);
916                    self.cursor.next_line(self.is_at_crlf());
917                }
918                config.hardbreak(&mut self.s);
919                self.cursor.next_line(self.is_at_crlf());
920            }
921        }
922    }
923
924    fn peek_comment<'b>(&'b self) -> Option<&'b Comment>
925    where
926        'sess: 'b,
927    {
928        self.comments.peek()
929    }
930
931    fn peek_comment_before<'b>(&'b self, pos: BytePos) -> Option<&'b Comment>
932    where
933        'sess: 'b,
934    {
935        self.comments.iter().take_while(|c| c.pos() < pos).find(|c| !c.style.is_blank())
936    }
937
938    fn has_comment_before_with<F>(&self, pos: BytePos, f: F) -> bool
939    where
940        F: FnMut(&Comment) -> bool,
941    {
942        self.comments.iter().take_while(|c| c.pos() < pos).any(f)
943    }
944
945    fn peek_comment_between<'b>(&'b self, pos_lo: BytePos, pos_hi: BytePos) -> Option<&'b Comment>
946    where
947        'sess: 'b,
948    {
949        self.comments
950            .iter()
951            .take_while(|c| pos_lo < c.pos() && c.pos() < pos_hi)
952            .find(|c| !c.style.is_blank())
953    }
954
955    fn has_comment_between(&self, start_pos: BytePos, end_pos: BytePos) -> bool {
956        self.comments.iter().filter(|c| c.pos() > start_pos && c.pos() < end_pos).any(|_| true)
957    }
958
959    pub(crate) fn next_comment(&mut self) -> Option<Comment> {
960        self.comments.next()
961    }
962
963    fn peek_trailing_comment<'b>(
964        &'b self,
965        span_pos: BytePos,
966        next_pos: Option<BytePos>,
967    ) -> Option<&'b Comment>
968    where
969        'sess: 'b,
970    {
971        self.comments.peek_trailing(self.sm, span_pos, next_pos).map(|(cmnt, _)| cmnt)
972    }
973
974    fn print_trailing_comment_inner(
975        &mut self,
976        span_pos: BytePos,
977        next_pos: Option<BytePos>,
978        config: Option<CommentConfig>,
979    ) -> bool {
980        let mut printed = 0;
981        if let Some((_, n)) = self.comments.peek_trailing(self.sm, span_pos, next_pos) {
982            let config =
983                config.unwrap_or(CommentConfig::skip_ws().mixed_no_break().mixed_prev_space());
984            while printed <= n {
985                let cmnt = self.comments.next().unwrap();
986                if let Some(cmnt) = self.handle_comment(cmnt, config.trailing_no_break) {
987                    self.print_comment(cmnt, config);
988                };
989                printed += 1;
990            }
991        }
992        printed != 0
993    }
994
995    fn print_trailing_comment(&mut self, span_pos: BytePos, next_pos: Option<BytePos>) -> bool {
996        self.print_trailing_comment_inner(span_pos, next_pos, None)
997    }
998
999    fn print_trailing_comment_no_break(&mut self, span_pos: BytePos, next_pos: Option<BytePos>) {
1000        self.print_trailing_comment_inner(
1001            span_pos,
1002            next_pos,
1003            Some(CommentConfig::skip_ws().trailing_no_break().mixed_no_break().mixed_prev_space()),
1004        );
1005    }
1006
1007    fn print_remaining_comments(&mut self, skip_leading_ws: bool) {
1008        // If there aren't any remaining comments, then we need to manually
1009        // make sure there is a line break at the end.
1010        if self.peek_comment().is_none() && !self.is_bol_or_only_ind() {
1011            self.hardbreak();
1012            return;
1013        }
1014
1015        let mut is_leading = true;
1016        while let Some(cmnt) = self.next_comment() {
1017            if cmnt.style.is_blank() && skip_leading_ws && is_leading {
1018                continue;
1019            }
1020
1021            is_leading = false;
1022            if let Some(cmnt) = self.handle_comment(cmnt, false) {
1023                self.print_comment(cmnt, CommentConfig::default());
1024            } else if self.peek_comment().is_none() && !self.is_bol_or_only_ind() {
1025                self.hardbreak();
1026            }
1027        }
1028    }
1029}
1030
1031#[derive(Clone, Copy)]
1032enum Skip {
1033    All,
1034    Leading { resettable: bool },
1035    Trailing,
1036}
1037
1038#[derive(Default, Clone, Copy)]
1039pub(crate) struct CommentConfig {
1040    // Config: all
1041    skip_blanks: Option<Skip>,
1042    offset: isize,
1043
1044    // Config: isolated comments
1045    iso_no_break: bool,
1046    // Config: trailing comments
1047    trailing_no_break: bool,
1048    // Config: mixed comments
1049    mixed_prev_space: bool,
1050    mixed_post_nbsp: bool,
1051    mixed_no_break_prev: bool,
1052    mixed_no_break_post: bool,
1053}
1054
1055impl CommentConfig {
1056    pub(crate) fn skip_ws() -> Self {
1057        Self { skip_blanks: Some(Skip::All), ..Default::default() }
1058    }
1059
1060    pub(crate) fn skip_leading_ws(resettable: bool) -> Self {
1061        Self { skip_blanks: Some(Skip::Leading { resettable }), ..Default::default() }
1062    }
1063
1064    pub(crate) fn skip_trailing_ws() -> Self {
1065        Self { skip_blanks: Some(Skip::Trailing), ..Default::default() }
1066    }
1067
1068    pub(crate) fn offset(mut self, off: isize) -> Self {
1069        self.offset = off;
1070        self
1071    }
1072
1073    pub(crate) fn no_breaks(mut self) -> Self {
1074        self.iso_no_break = true;
1075        self.trailing_no_break = true;
1076        self.mixed_no_break_prev = true;
1077        self.mixed_no_break_post = true;
1078        self
1079    }
1080
1081    pub(crate) fn trailing_no_break(mut self) -> Self {
1082        self.trailing_no_break = true;
1083        self
1084    }
1085
1086    pub(crate) fn mixed_no_break(mut self) -> Self {
1087        self.mixed_no_break_prev = true;
1088        self.mixed_no_break_post = true;
1089        self
1090    }
1091
1092    pub(crate) fn mixed_no_break_post(mut self) -> Self {
1093        self.mixed_no_break_post = true;
1094        self
1095    }
1096
1097    pub(crate) fn mixed_prev_space(mut self) -> Self {
1098        self.mixed_prev_space = true;
1099        self
1100    }
1101
1102    pub(crate) fn mixed_post_nbsp(mut self) -> Self {
1103        self.mixed_post_nbsp = true;
1104        self
1105    }
1106
1107    pub(crate) fn hardbreak_if_not_bol(&self, is_bol: bool, p: &mut pp::Printer) {
1108        if self.offset != 0 && !is_bol {
1109            self.hardbreak(p);
1110        } else {
1111            p.hardbreak_if_not_bol();
1112        }
1113    }
1114
1115    pub(crate) fn hardbreak(&self, p: &mut pp::Printer) {
1116        p.break_offset(SIZE_INFINITY as usize, self.offset);
1117    }
1118
1119    pub(crate) fn space(&self, p: &mut pp::Printer) {
1120        p.break_offset(1, self.offset);
1121    }
1122
1123    pub(crate) fn nbsp_or_space(&self, breaks: bool, p: &mut pp::Printer) {
1124        if breaks {
1125            self.space(p);
1126        } else {
1127            p.nbsp();
1128        }
1129    }
1130
1131    pub(crate) fn zerobreak(&self, p: &mut pp::Printer) {
1132        p.break_offset(0, self.offset);
1133    }
1134}
1135
1136fn snippet_with_tabs(s: String, tab_width: usize) -> String {
1137    // process leading breaks
1138    let trimmed = s.trim_start_matches('\n');
1139    let num_breaks = s.len() - trimmed.len();
1140    let mut formatted = std::iter::repeat_n('\n', num_breaks).collect::<String>();
1141
1142    // process lines
1143    for (pos, line) in trimmed.lines().delimited() {
1144        line_with_tabs(&mut formatted, line, tab_width, None);
1145        if !pos.is_last {
1146            formatted.push('\n');
1147        }
1148    }
1149
1150    formatted
1151}
1152
1153/// Formats a doc comment with the requested style.
1154///
1155/// NOTE: assumes comments have already been normalized.
1156fn style_doc_comment(style: DocCommentStyle, mut cmnt: Comment) -> Comment {
1157    match style {
1158        DocCommentStyle::Line if cmnt.kind == ast::CommentKind::Block => {
1159            let mut new_lines = Vec::new();
1160            for (pos, line) in cmnt.lines.iter().delimited() {
1161                if pos.is_first || pos.is_last {
1162                    // Skip the opening '/**' and closing '*/' lines
1163                    continue;
1164                }
1165
1166                // Convert ' * {content}' to '/// {content}'
1167                let trimmed = line.trim_start();
1168                if let Some(content) = trimmed.strip_prefix('*') {
1169                    new_lines.push(format!("///{content}"));
1170                } else if !trimmed.is_empty() {
1171                    new_lines.push(format!("/// {trimmed}"));
1172                }
1173            }
1174
1175            cmnt.lines = new_lines;
1176            cmnt.kind = ast::CommentKind::Line;
1177            cmnt
1178        }
1179        DocCommentStyle::Block if cmnt.kind == ast::CommentKind::Line => {
1180            let mut new_lines = vec!["/**".to_string()];
1181
1182            for line in &cmnt.lines {
1183                // Convert '/// {content}' to ' * {content}'
1184                new_lines.push(format!(" *{content}", content = &line[3..]))
1185            }
1186
1187            new_lines.push(" */".to_string());
1188            cmnt.lines = new_lines;
1189            cmnt.kind = ast::CommentKind::Block;
1190            cmnt
1191        }
1192        // Otherwise, no conversion needed.
1193        _ => cmnt,
1194    }
1195}