wdl-format 0.22.0

Formatting of WDL (Workflow Description Language) documents
Documentation
//! Tokens emitted during the formatting of particular elements.

use std::collections::HashSet;
use std::rc::Rc;

use wdl_ast::AstToken;
use wdl_ast::Directive;
use wdl_ast::SyntaxKind;
use wdl_ast::SyntaxTokenExt;

use crate::Comment;
use crate::Token;
use crate::TokenStream;
use crate::Trivia;
use crate::TriviaBlankLineSpacingPolicy;

/// A token that can be written by elements.
///
/// These are tokens that are intended to be written directly by elements to a
/// [`TokenStream`](super::TokenStream) consisting of [`PreToken`]s. Note that
/// this will transformed into a [`TokenStream`](super::TokenStream) of
/// [`PostToken`](super::PostToken)s by a
/// [`Postprocessor`](super::Postprocessor) (authors of elements are never
/// expected to write [`PostToken`](super::PostToken)s directly).
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PreToken {
    /// A non-trivial blank line.
    ///
    /// This will not be ignored by the postprocessor (unlike
    /// [`Trivia::BlankLine`] which is potentially ignored).
    BlankLine,

    /// The end of a line.
    LineEnd,

    /// The end of a word.
    WordEnd,

    /// The start of an indented block.
    IndentStart,

    /// The end of an indented block.
    IndentEnd,

    /// How to handle trivial blank lines from this point onwards.
    LineSpacingPolicy(TriviaBlankLineSpacingPolicy),

    /// Literal text.
    Literal(Rc<String>, SyntaxKind),

    /// Trivia.
    Trivia(Trivia),

    /// A temporary indent start. Used in command section formatting.
    ///
    /// Command sections must account for indentation from both the
    /// WDL context and the embedded Bash context, so this is used to
    /// add additional indentation from the Bash context.
    TempIndentStart(Rc<String>),

    /// A temporary indent end. Used in command section formatting.
    ///
    /// See [`PreToken::TempIndentStart`] for more information.
    TempIndentEnd,

    /// The start of a fit or split block.
    FitOrSplitStart {
        /// If the block will be "fit", insert this literal string at the
        /// beginning.
        fit_start: Rc<String>,
        /// If the block will be "fit", insert this literal string between each
        /// potential split.
        fit_delimiter: Rc<String>,
        /// If the block will be split, end the line immediately.
        split_end_line: bool,
    },

    /// A potential split in a fit or split block.
    PotentialSplit,

    /// The end of a fit or split block.
    FitOrSplitEnd {
        /// If the block will be "fit", insert this literal string at the end.
        fit_end: Rc<String>,
        /// If the block will be split, insert this literal string at the end.
        split_end: Rc<String>,
        /// If the block will be split, end the line after inserting
        /// `split_end`.
        split_end_line: bool,
    },
}

impl std::fmt::Display for PreToken {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PreToken::BlankLine => write!(f, "<BlankLine>"),
            PreToken::LineEnd => write!(f, "<EndOfLine>"),
            PreToken::WordEnd => write!(f, "<WordEnd>"),
            PreToken::IndentStart => write!(f, "<IndentStart>"),
            PreToken::IndentEnd => write!(f, "<IndentEnd>"),
            PreToken::LineSpacingPolicy(policy) => {
                write!(f, "<LineSpacingPolicy@{policy:?}>")
            }
            PreToken::Literal(value, kind) => {
                write!(f, "<Literal-{kind:?}@{value}>")
            }
            PreToken::Trivia(trivia) => match trivia {
                Trivia::BlankLine => {
                    write!(f, "<OptionalBlankLine>")
                }
                Trivia::Comment(comment) => match comment {
                    Comment::Directive(directive) => {
                        write!(f, "<Comment-Directive@{directive:?}>")
                    }
                    Comment::Documentation(documentation) => {
                        write!(f, "<Comment-Documentation@{documentation}>")
                    }
                    Comment::Preceding(value) => {
                        write!(f, "<Comment-Preceding@{value}>")
                    }
                    Comment::Inline(value) => {
                        write!(f, "<Comment-Inline@{value}>")
                    }
                },
            },
            PreToken::TempIndentStart(value) => write!(f, "<TempIndentStart@{value}>"),
            PreToken::TempIndentEnd => write!(f, "<TempIndentEnd>"),
            PreToken::FitOrSplitStart { .. } => write!(f, "<FitOrSplitStart>"),
            PreToken::PotentialSplit => write!(f, "<PotentialSplit>"),
            PreToken::FitOrSplitEnd { .. } => write!(f, "<FitOrSplitEnd>"),
        }
    }
}

impl Token for PreToken {
    /// Returns a displayable version of the token.
    fn display<'a>(&'a self, _config: &'a crate::Config) -> impl std::fmt::Display {
        self
    }
}

impl TokenStream<PreToken> {
    /// Inserts a blank line token to the stream if the stream does not already
    /// end with a blank line. This will replace any [`Trivia::BlankLine`]
    /// tokens with [`PreToken::BlankLine`].
    pub fn blank_line(&mut self) {
        self.trim_while(|t| matches!(t, PreToken::BlankLine | PreToken::Trivia(Trivia::BlankLine)));
        self.0.push(PreToken::BlankLine);
    }

    /// Inserts an end of line token to the stream if the stream does not
    /// already end with an end of line token.
    ///
    /// This will also trim any trailing [`PreToken::WordEnd`] tokens.
    pub fn end_line(&mut self) {
        self.trim_while(|t| matches!(t, PreToken::WordEnd | PreToken::LineEnd));
        self.0.push(PreToken::LineEnd);
    }

    /// Inserts a word end token to the stream if the stream does not already
    /// end with a word end token.
    pub fn end_word(&mut self) {
        self.trim_end(&PreToken::WordEnd);
        self.0.push(PreToken::WordEnd);
    }

    /// Inserts an indent start token to the stream. This will **not** end the
    /// current line.
    ///
    /// Callers that want the indent change to take effect on the next line must
    /// call `end_line()` after this.
    pub fn increment_indent(&mut self) {
        self.0.push(PreToken::IndentStart);
    }

    /// Inserts an indent end token to the stream. This will **not** end the
    /// current line.
    ///
    /// Callers that want the indent change to take effect on the next line must
    /// call `end_line()` after this.
    pub fn decrement_indent(&mut self) {
        self.0.push(PreToken::IndentEnd);
    }

    /// Start a fit or split block.
    pub fn fit_or_split_start(
        &mut self,
        fit_start: Rc<String>,
        fit_delimiter: Rc<String>,
        split_end_line: bool,
    ) {
        self.0.push(PreToken::FitOrSplitStart {
            fit_start,
            fit_delimiter,
            split_end_line,
        })
    }

    /// Insert a potential split in the middle of a fit or split block.
    pub fn potential_split(&mut self) {
        self.0.push(PreToken::PotentialSplit);
    }

    /// End a fit or split block.
    pub fn fit_or_split_end(
        &mut self,
        fit_end: Rc<String>,
        split_end: Rc<String>,
        split_end_line: bool,
    ) {
        self.0.push(PreToken::FitOrSplitEnd {
            fit_end,
            split_end,
            split_end_line,
        })
    }

    /// Inserts a trivial blank lines "always allowed" context change.
    pub fn allow_blank_lines(&mut self) {
        self.0.push(PreToken::LineSpacingPolicy(
            TriviaBlankLineSpacingPolicy::Always,
        ));
    }

    /// Inserts a trivial blank lines "not allowed after comments" context
    /// change.
    pub fn ignore_trailing_blank_lines(&mut self) {
        self.0.push(PreToken::LineSpacingPolicy(
            TriviaBlankLineSpacingPolicy::RemoveTrailingBlanks,
        ));
    }

    /// Inserts any preceding trivia into the stream.
    ///
    /// This will consolidate directive comments which
    /// precede this token.
    ///
    /// # Panics
    ///
    /// This will panic if the provided token is itself trivia, as trivia
    /// cannot have trivia.
    fn push_preceding_trivia(&mut self, token: &wdl_ast::Token) {
        assert!(!token.inner().kind().is_trivia());
        let preceding_trivia = token.inner().preceding_trivia();
        let mut trivia = Vec::new();
        let mut exceptions = HashSet::new();
        for token in preceding_trivia {
            match token.kind() {
                SyntaxKind::Whitespace => {
                    if !self.0.last().is_some_and(|t| {
                        matches!(t, PreToken::BlankLine | PreToken::Trivia(Trivia::BlankLine))
                    }) {
                        trivia.push(PreToken::Trivia(Trivia::BlankLine));
                    }
                }
                SyntaxKind::Comment => {
                    if let Some(comment) = wdl_ast::Comment::cast(token.clone())
                        && let Some(directive) = comment.directive()
                    {
                        match directive {
                            Directive::Except(e) => exceptions.extend(e),
                        }
                    } else {
                        let comment = PreToken::Trivia(Trivia::Comment(Comment::Preceding(
                            Rc::new(token.text().trim_end().to_string()),
                        )));
                        trivia.push(comment);
                    }
                }
                _ => unreachable!("unexpected trivia: {:?}", token),
            };
        }

        for token in trivia {
            self.0.push(token);
        }

        if !exceptions.is_empty() {
            let comment = PreToken::Trivia(Trivia::Comment(Comment::Directive(Rc::new(
                Directive::Except(exceptions),
            ))));
            self.0.push(comment);
        }
    }

    /// Inserts any inline trivia into the stream.
    ///
    /// # Panics
    ///
    /// This will panic if the provided token is itself trivia, as trivia
    /// cannot have trivia.
    fn push_inline_trivia(&mut self, token: &wdl_ast::Token) {
        assert!(!token.inner().kind().is_trivia());
        if let Some(token) = token.inner().inline_comment() {
            let inline_comment = PreToken::Trivia(Trivia::Comment(Comment::Inline(Rc::new(
                token.text().trim_end().to_owned(),
            ))));
            self.0.push(inline_comment);
        }
    }

    /// Pushes an AST token into the stream.
    ///
    /// This will also push any preceding or inline trivia into the stream.
    /// Any token may have preceding or inline trivia, unless that token is
    /// itself trivia (i.e. trivia cannot have trivia).
    ///
    /// # Panics
    ///
    /// This will panic if the provided token is trivia.
    pub fn push_ast_token(&mut self, token: &wdl_ast::Token) {
        self.push_preceding_trivia(token);
        self.0.push(PreToken::Literal(
            Rc::new(token.inner().text().to_owned()),
            token.inner().kind(),
        ));
        self.push_inline_trivia(token);
    }

    /// Pushes an AST token into the stream as another [`SyntaxKind`].
    ///
    /// This will insert any trivia that would have been inserted with the AST
    /// token.
    ///
    /// # Panics
    ///
    /// This will panic if the provided token is trivia.
    pub fn push_ast_token_as(&mut self, token: &wdl_ast::Token, kind: SyntaxKind) {
        self.push_preceding_trivia(token);
        self.0.push(PreToken::Literal(
            Rc::new(token.inner().text().to_owned()),
            kind,
        ));
        self.push_inline_trivia(token);
    }

    /// Pushes a literal string into the stream in place of an AST token.
    ///
    /// This will insert any trivia that would have been inserted with the AST
    /// token.
    ///
    /// # Panics
    ///
    /// This will panic if the provided token is trivia.
    pub fn push_literal_in_place_of_token(&mut self, token: &wdl_ast::Token, replacement: String) {
        self.push_preceding_trivia(token);
        self.0.push(PreToken::Literal(
            Rc::new(replacement),
            token.inner().kind(),
        ));
        self.push_inline_trivia(token);
    }

    /// Pushes a literal string into the stream.
    ///
    /// This will not insert any trivia.
    pub fn push_literal(&mut self, value: String, kind: SyntaxKind) {
        self.0.push(PreToken::Literal(Rc::new(value), kind));
    }
}