packages feed

fourmolu-0.4.0.0: src/Ormolu/Printer/Meat/Declaration/Value.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}

module Ormolu.Printer.Meat.Declaration.Value
  ( p_valDecl,
    p_pat,
    p_hsExpr,
    p_hsSplice,
    p_stringLit,
  )
where

import Control.Monad
import Data.Bool (bool)
import Data.Char (isPunctuation, isSymbol)
import Data.Data hiding (Infix, Prefix)
import Data.Function (on)
import Data.Functor ((<&>))
import Data.Generics.Schemes (everything)
import Data.List (intersperse, sortBy)
import Data.List.NonEmpty (NonEmpty (..), (<|))
import qualified Data.List.NonEmpty as NE
import Data.Maybe (isJust)
import Data.Text (Text)
import qualified Data.Text as Text
import GHC.Data.Bag (bagToList)
import GHC.Hs.Binds
import GHC.Hs.Expr
import GHC.Hs.Extension
import GHC.Hs.Lit
import GHC.Hs.Pat
import GHC.Hs.Type
import GHC.LanguageExtensions.Type (Extension (NegativeLiterals))
import GHC.Parser.Annotation
import GHC.Parser.CharClass (is_space)
import GHC.Types.Basic
import GHC.Types.Name.Occurrence (occNameString)
import GHC.Types.Name.Reader
import GHC.Types.SrcLoc
import Ormolu.Config
import Ormolu.Printer.Combinators
import Ormolu.Printer.Internal
import Ormolu.Printer.Meat.Common
import {-# SOURCE #-} Ormolu.Printer.Meat.Declaration
import Ormolu.Printer.Meat.Declaration.Signature
import Ormolu.Printer.Meat.Type
import Ormolu.Printer.Operators
import Ormolu.Utils

-- | Style of a group of equations.
data MatchGroupStyle
  = Function (Located RdrName)
  | PatternBind
  | Case
  | Lambda
  | LambdaCase

-- | Style of equations in a group.
data GroupStyle
  = EqualSign
  | RightArrow

-- | Expression placement. This marks the places where expressions that
-- implement handing forms may use them.
data Placement
  = -- | Multi-line layout should cause
    -- insertion of a newline and indentation
    -- bump
    Normal
  | -- | Expressions that have hanging form
    -- should use it and avoid bumping one level
    -- of indentation
    Hanging
  deriving (Eq, Show)

p_valDecl :: HsBindLR GhcPs GhcPs -> R ()
p_valDecl = \case
  FunBind NoExtField funId funMatches _ -> p_funBind funId funMatches
  PatBind NoExtField pat grhss _ -> p_match PatternBind False NoSrcStrict [pat] grhss
  VarBind {} -> notImplemented "VarBinds" -- introduced by the type checker
  AbsBinds {} -> notImplemented "AbsBinds" -- introduced by the type checker
  PatSynBind NoExtField psb -> p_patSynBind psb

p_funBind ::
  Located RdrName ->
  MatchGroup GhcPs (LHsExpr GhcPs) ->
  R ()
p_funBind name = p_matchGroup (Function name)

p_matchGroup ::
  MatchGroupStyle ->
  MatchGroup GhcPs (LHsExpr GhcPs) ->
  R ()
p_matchGroup = p_matchGroup' exprPlacement p_hsExpr

p_matchGroup' ::
  Data body =>
  -- | How to get body placement
  (body -> Placement) ->
  -- | How to print body
  (body -> R ()) ->
  -- | Style of this group of equations
  MatchGroupStyle ->
  -- | Match group
  MatchGroup GhcPs (Located body) ->
  R ()
p_matchGroup' placer render style mg@MG {..} = do
  let ob = case style of
        Case -> bracesIfEmpty
        LambdaCase -> bracesIfEmpty
        _ -> dontUseBraces
        where
          bracesIfEmpty = if isEmptyMatchGroup mg then useBraces else id
  -- Since we are forcing braces on 'sepSemi' based on 'ob', we have to
  -- restore the brace state inside the sepsemi.
  ub <- bool dontUseBraces useBraces <$> canUseBraces
  ob $ sepSemi (located' (ub . p_Match)) (unLoc mg_alts)
  where
    p_Match m@Match {..} =
      p_match'
        placer
        render
        (adjustMatchGroupStyle m style)
        (isInfixMatch m)
        (matchStrictness m)
        m_pats
        m_grhss

-- | Function id obtained through pattern matching on 'FunBind' should not
-- be used to print the actual equations because the different ‘RdrNames’
-- used in the equations may have different “decorations” (such as backticks
-- and paretheses) associated with them. It is necessary to use per-equation
-- names obtained from 'm_ctxt' of 'Match'. This function replaces function
-- name inside of 'Function' accordingly.
adjustMatchGroupStyle ::
  Match GhcPs body ->
  MatchGroupStyle ->
  MatchGroupStyle
adjustMatchGroupStyle m = \case
  Function _ -> (Function . mc_fun . m_ctxt) m
  style -> style

matchStrictness :: Match id body -> SrcStrictness
matchStrictness match =
  case m_ctxt match of
    FunRhs {mc_strictness = s} -> s
    _ -> NoSrcStrict

p_match ::
  -- | Style of the group
  MatchGroupStyle ->
  -- | Is this an infix match?
  Bool ->
  -- | Strictness prefix (FunBind)
  SrcStrictness ->
  -- | Argument patterns
  [LPat GhcPs] ->
  -- | Equations
  GRHSs GhcPs (LHsExpr GhcPs) ->
  R ()
p_match = p_match' exprPlacement p_hsExpr

p_match' ::
  Data body =>
  -- | How to get body placement
  (body -> Placement) ->
  -- | How to print body
  (body -> R ()) ->
  -- | Style of this group of equations
  MatchGroupStyle ->
  -- | Is this an infix match?
  Bool ->
  -- | Strictness prefix (FunBind)
  SrcStrictness ->
  -- | Argument patterns
  [LPat GhcPs] ->
  -- | Equations
  GRHSs GhcPs (Located body) ->
  R ()
p_match' placer render style isInfix strictness m_pats GRHSs {..} = do
  -- Normally, since patterns may be placed in a multi-line layout, it is
  -- necessary to bump indentation for the pattern group so it's more
  -- indented than function name. This in turn means that indentation for
  -- the body should also be bumped. Normally this would mean that bodies
  -- would start with two indentation steps applied, which is ugly, so we
  -- need to be a bit more clever here and bump indentation level only when
  -- pattern group is multiline.
  case strictness of
    NoSrcStrict -> return ()
    SrcStrict -> txt "!"
    SrcLazy -> txt "~"
  indentBody <- case NE.nonEmpty m_pats of
    Nothing ->
      False <$ case style of
        Function name -> p_rdrName name
        _ -> return ()
    Just ne_pats -> do
      let combinedSpans = case style of
            Function name -> combineSrcSpans (getLoc name) patSpans
            _ -> patSpans
          patSpans = combineSrcSpans' (getLoc <$> ne_pats)
          indentBody = not (isOneLineSpan combinedSpans)
      switchLayout [combinedSpans] $ do
        let stdCase = sep breakpoint (located' p_pat) m_pats
        case style of
          Function name ->
            p_infixDefHelper
              isInfix
              indentBody
              (p_rdrName name)
              (located' p_pat <$> m_pats)
          PatternBind -> stdCase
          Case -> stdCase
          Lambda -> do
            let needsSpace = case unLoc (NE.head ne_pats) of
                  LazyPat _ _ -> True
                  BangPat _ _ -> True
                  SplicePat _ _ -> True
                  _ -> False
            txt "\\"
            when needsSpace space
            sitcc stdCase
          LambdaCase -> stdCase
      return indentBody
  let -- Calculate position of end of patterns. This is useful when we decide
      -- about putting certain constructions in hanging positions.
      endOfPats = case NE.nonEmpty m_pats of
        Nothing -> case style of
          Function name -> Just (getLoc name)
          _ -> Nothing
        Just pats -> (Just . getLoc . NE.last) pats
      isCase = \case
        Case -> True
        LambdaCase -> True
        _ -> False
      hasGuards = withGuards grhssGRHSs
      grhssSpan =
        combineSrcSpans' $
          getGRHSSpan . unLoc <$> NE.fromList grhssGRHSs
      patGrhssSpan =
        maybe
          grhssSpan
          (combineSrcSpans grhssSpan . srcLocSpan . srcSpanEnd)
          endOfPats
      placement =
        case endOfPats of
          Nothing -> blockPlacement placer grhssGRHSs
          Just spn ->
            if onTheSameLine spn grhssSpan
              then blockPlacement placer grhssGRHSs
              else Normal
      p_body = do
        let groupStyle =
              if isCase style && hasGuards
                then RightArrow
                else EqualSign
        sep newline (located' (p_grhs' placer render groupStyle)) grhssGRHSs
      p_where = do
        let whereIsEmpty = eqEmptyLocalBinds (unLoc grhssLocalBinds)
        unless (eqEmptyLocalBinds (unLoc grhssLocalBinds)) $ do
          breakpoint
          indentWhere <- getPrinterOpt poIndentWheres
          bool (inciByFrac $ -2) id indentWhere $ txt "where"
          unless whereIsEmpty breakpoint
          inciIf indentWhere $ located grhssLocalBinds p_hsLocalBinds
  inciIf indentBody $ do
    unless (length grhssGRHSs > 1) $
      case style of
        Function _ | hasGuards -> return ()
        Function _ -> space >> inci equals
        PatternBind -> space >> inci equals
        s | isCase s && hasGuards -> return ()
        _ -> space >> txt "->"
    switchLayout [patGrhssSpan] $
      placeHanging placement p_body
    inci p_where

p_grhs :: GroupStyle -> GRHS GhcPs (LHsExpr GhcPs) -> R ()
p_grhs = p_grhs' exprPlacement p_hsExpr

p_grhs' ::
  Data body =>
  -- | How to get body placement
  (body -> Placement) ->
  -- | How to print body
  (body -> R ()) ->
  GroupStyle ->
  GRHS GhcPs (Located body) ->
  R ()
p_grhs' placer render style (GRHS NoExtField guards body) =
  case guards of
    [] -> p_body
    xs -> do
      txt "|"
      space
      sitcc (sep commaDel (sitcc . located' p_stmt) xs)
      space
      inci $ case style of
        EqualSign -> equals
        RightArrow -> txt "->"
      placeHanging placement p_body
  where
    placement =
      case endOfGuards of
        Nothing -> placer (unLoc body)
        Just spn ->
          if onTheSameLine spn (getLoc body)
            then placer (unLoc body)
            else Normal
    endOfGuards =
      case NE.nonEmpty guards of
        Nothing -> Nothing
        Just gs -> (Just . getLoc . NE.last) gs
    p_body = located body render

p_hsCmd :: HsCmd GhcPs -> R ()
p_hsCmd = p_hsCmd' N

p_hsCmd' :: BracketStyle -> HsCmd GhcPs -> R ()
p_hsCmd' s = \case
  HsCmdArrApp NoExtField body input arrType rightToLeft -> do
    let (l, r) = if rightToLeft then (body, input) else (input, body)
    located l p_hsExpr
    breakpoint
    inci $ do
      case (arrType, rightToLeft) of
        (HsFirstOrderApp, True) -> txt "-<"
        (HsHigherOrderApp, True) -> txt "-<<"
        (HsFirstOrderApp, False) -> txt ">-"
        (HsHigherOrderApp, False) -> txt ">>-"
      placeHanging (exprPlacement (unLoc input)) $
        located r p_hsExpr
  HsCmdArrForm NoExtField form Prefix _ cmds -> banana s $ do
    located form p_hsExpr
    unless (null cmds) $ do
      breakpoint
      inci (sequence_ (intersperse breakpoint (located' p_hsCmdTop <$> cmds)))
  HsCmdArrForm NoExtField form Infix _ [left, right] ->
    let opTree = OpBranch (cmdOpTree left) form (cmdOpTree right)
     in p_cmdOpTree (reassociateOpTree getOpName opTree)
  HsCmdArrForm NoExtField _ Infix _ _ -> notImplemented "HsCmdArrForm"
  HsCmdApp NoExtField cmd expr -> do
    located cmd (p_hsCmd' s)
    space
    located expr p_hsExpr
  HsCmdLam NoExtField mgroup -> p_matchGroup' cmdPlacement p_hsCmd Lambda mgroup
  HsCmdPar NoExtField c -> parens N $ sitcc $ located c p_hsCmd
  HsCmdCase NoExtField e mgroup ->
    p_case cmdPlacement p_hsCmd e mgroup
  HsCmdLamCase NoExtField mgroup ->
    p_lamcase cmdPlacement p_hsCmd mgroup
  HsCmdIf NoExtField _ if' then' else' ->
    p_if cmdPlacement p_hsCmd if' then' else'
  HsCmdLet NoExtField localBinds c ->
    p_let p_hsCmd localBinds c
  HsCmdDo NoExtField es -> do
    txt "do"
    p_stmts cmdPlacement (p_hsCmd' S) es

p_hsCmdTop :: HsCmdTop GhcPs -> R ()
p_hsCmdTop (HsCmdTop NoExtField cmd) = located cmd p_hsCmd

-- | Render an expression preserving blank lines between such consecutive
-- expressions found in the original source code.
withSpacing ::
  -- | Rendering function
  (a -> R ()) ->
  -- | Entity to render
  Located a ->
  R ()
withSpacing f l = located l $ \x -> do
  case getLoc l of
    UnhelpfulSpan _ -> f x
    RealSrcSpan currentSpn _ -> do
      getSpanMark >>= \case
        -- Spacing before comments will be handled by the code
        -- that prints comments, so we just have to deal with
        -- blank lines between statements here.
        Just (StatementSpan lastSpn) ->
          if srcSpanStartLine currentSpn > srcSpanEndLine lastSpn + 1
            then newline
            else return ()
        _ -> return ()
      f x
      -- In some cases the (f x) expression may insert a new mark. We want
      -- to be careful not to override comment marks.
      getSpanMark >>= \case
        Just (HaddockSpan _ _) -> return ()
        Just (CommentSpan _) -> return ()
        _ -> setSpanMark (StatementSpan currentSpn)

p_stmt :: Stmt GhcPs (LHsExpr GhcPs) -> R ()
p_stmt = p_stmt' exprPlacement p_hsExpr

p_stmt' ::
  Data body =>
  -- | Placer
  (body -> Placement) ->
  -- | Render
  (body -> R ()) ->
  -- | Statement to render
  Stmt GhcPs (Located body) ->
  R ()
p_stmt' placer render = \case
  LastStmt NoExtField body _ _ -> located body render
  BindStmt NoExtField p f@(L l x) -> do
    located p p_pat
    space
    txt "<-"
    let loc = getLoc p
        placement
          | isOneLineSpan (mkSrcSpan (srcSpanEnd loc) (srcSpanStart l)) = placer x
          | otherwise = Normal
    switchLayout [loc, l] $
      placeHanging placement (located f render)
  ApplicativeStmt {} -> notImplemented "ApplicativeStmt" -- generated by renamer
  BodyStmt NoExtField body _ _ -> located body render
  LetStmt NoExtField binds -> do
    txt "let"
    space
    sitcc $ located binds p_hsLocalBinds
  ParStmt {} ->
    -- 'ParStmt' should always be eliminated in 'gatherStmt' already, such
    -- that it never occurs in 'p_stmt''. Consequently, handling it here
    -- would be redundant.
    notImplemented "ParStmt"
  TransStmt {..} ->
    -- 'TransStmt' only needs to account for render printing itself, since
    -- pretty printing of relevant statements (e.g., in 'trS_stmts') is
    -- handled through 'gatherStmt'.
    case (trS_form, trS_by) of
      (ThenForm, Nothing) -> do
        txt "then"
        breakpoint
        inci $ located trS_using p_hsExpr
      (ThenForm, Just e) -> do
        txt "then"
        breakpoint
        inci $ located trS_using p_hsExpr
        breakpoint
        txt "by"
        breakpoint
        inci $ located e p_hsExpr
      (GroupForm, Nothing) -> do
        txt "then group using"
        breakpoint
        inci $ located trS_using p_hsExpr
      (GroupForm, Just e) -> do
        txt "then group by"
        breakpoint
        inci $ located e p_hsExpr
        breakpoint
        txt "using"
        breakpoint
        inci $ located trS_using p_hsExpr
  RecStmt {..} -> do
    txt "rec"
    space
    sitcc $ sepSemi (withSpacing (p_stmt' placer render)) recS_stmts

p_stmts ::
  Data body =>
  -- | Placer
  (body -> Placement) ->
  -- | Render
  (body -> R ()) ->
  -- | Statements to render
  Located [Located (Stmt GhcPs (Located body))] ->
  R ()
p_stmts placer render es = do
  breakpoint
  ub <- layoutToBraces <$> getLayout
  inci . located es $
    sepSemi
      (ub . withSpacing (p_stmt' placer render))

gatherStmt :: ExprLStmt GhcPs -> [[ExprLStmt GhcPs]]
gatherStmt (L _ (ParStmt NoExtField block _ _)) =
  foldr ((<>) . gatherStmtBlock) [] block
gatherStmt (L s stmt@TransStmt {..}) =
  foldr liftAppend [] ((gatherStmt <$> trS_stmts) <> pure [[L s stmt]])
gatherStmt stmt = [[stmt]]

gatherStmtBlock :: ParStmtBlock GhcPs GhcPs -> [[ExprLStmt GhcPs]]
gatherStmtBlock (ParStmtBlock _ stmts _ _) =
  foldr (liftAppend . gatherStmt) [] stmts

p_hsLocalBinds :: HsLocalBindsLR GhcPs GhcPs -> R ()
p_hsLocalBinds = \case
  HsValBinds NoExtField (ValBinds NoExtField bag lsigs) -> do
    -- When in a single-line layout, there is a chance that the inner
    -- elements will also contain semicolons and they will confuse the
    -- parser. so we request braces around every element except the last.
    br <- layoutToBraces <$> getLayout
    let items =
          let injectLeft (L l x) = L l (Left x)
              injectRight (L l x) = L l (Right x)
           in (injectLeft <$> bagToList bag) ++ (injectRight <$> lsigs)
        positionToBracing = \case
          SinglePos -> id
          FirstPos -> br
          MiddlePos -> br
          LastPos -> id
        p_item' (p, item) =
          positionToBracing p $
            withSpacing (either p_valDecl p_sigDecl) item
        binds = sortBy (leftmost_smallest `on` getLoc) items
    sitcc $ sepSemi p_item' (attachRelativePos binds)
  HsValBinds NoExtField _ -> notImplemented "HsValBinds"
  HsIPBinds NoExtField (IPBinds NoExtField xs) ->
    -- Second argument of IPBind is always Left before type-checking.
    let p_ipBind (IPBind NoExtField (Left name) expr) = do
          atom name
          space
          equals
          breakpoint
          useBraces $ inci $ located expr p_hsExpr
        p_ipBind (IPBind NoExtField (Right _) _) =
          -- Should only occur after the type checker
          notImplemented "IPBind _ (Right _) _"
     in sepSemi (located' p_ipBind) xs
  EmptyLocalBinds NoExtField -> return ()

p_hsRecField ::
  HsRecField' RdrName (LHsExpr GhcPs) ->
  R ()
p_hsRecField HsRecField {..} = do
  p_rdrName hsRecFieldLbl
  unless hsRecPun $ do
    space
    equals
    let placement =
          if onTheSameLine (getLoc hsRecFieldLbl) (getLoc hsRecFieldArg)
            then exprPlacement (unLoc hsRecFieldArg)
            else Normal
    placeHanging placement (located hsRecFieldArg p_hsExpr)

p_hsExpr :: HsExpr GhcPs -> R ()
p_hsExpr = p_hsExpr' N

p_hsExpr' :: BracketStyle -> HsExpr GhcPs -> R ()
p_hsExpr' s = \case
  HsVar NoExtField name -> p_rdrName name
  HsUnboundVar NoExtField occ -> atom occ
  HsConLikeOut NoExtField _ -> notImplemented "HsConLikeOut"
  HsRecFld NoExtField x ->
    case x of
      Unambiguous NoExtField name -> p_rdrName name
      Ambiguous NoExtField name -> p_rdrName name
  HsOverLabel NoExtField _ v -> do
    txt "#"
    atom v
  HsIPVar NoExtField (HsIPName name) -> do
    txt "?"
    atom name
  HsOverLit NoExtField v -> atom (ol_val v)
  HsLit NoExtField lit ->
    case lit of
      HsString (SourceText stxt) _ -> p_stringLit stxt
      HsStringPrim (SourceText stxt) _ -> p_stringLit stxt
      r -> atom r
  HsLam NoExtField mgroup ->
    p_matchGroup Lambda mgroup
  HsLamCase NoExtField mgroup ->
    p_lamcase exprPlacement p_hsExpr mgroup
  HsApp NoExtField f x -> do
    let -- In order to format function applications with multiple parameters
        -- nicer, traverse the AST to gather the function and all the
        -- parameters together.
        gatherArgs f' knownArgs =
          case f' of
            L _ (HsApp _ l r) -> gatherArgs l (r <| knownArgs)
            _ -> (f', knownArgs)
        (func, args) = gatherArgs f (x :| [])
        -- We need to handle the last argument specially if it is a
        -- hanging construct, so separate it from the rest.
        (initp, lastp) = (NE.init args, NE.last args)
        initSpan =
          combineSrcSpans' $
            getLoc f :| [(srcLocSpan . srcSpanStart . getLoc) lastp]
        -- Hang the last argument only if the initial arguments span one
        -- line.
        placement =
          if isOneLineSpan initSpan
            then exprPlacement (unLoc lastp)
            else Normal
    -- If the last argument is not hanging, just separate every argument as
    -- usual. If it is hanging, print the initial arguments and hang the
    -- last one. Also, use braces around the every argument except the last
    -- one.
    case placement of
      Normal -> do
        let -- Usually we want to bump indentation for arguments for the
            -- sake of readability. However:
            -- When the function is itself a multi line do-block or a case
            -- expression, we can't indent by indentStep or more.
            -- When we are on the other hand *in* a do block, we have to
            -- indent by at least 1.
            -- Thus, we indent by half of indentStep when the function is
            -- a multi line do block or case expression.
            indentArg
              | isOneLineSpan (getLoc func) = case unLoc func of
                HsDo {} -> inciBy 2
                _ -> inci
              | otherwise = case unLoc func of
                HsDo {} -> inciHalf
                HsCase {} -> inciHalf
                HsLamCase {} -> inciHalf
                _ -> inci
        ub <-
          getLayout <&> \case
            SingleLine -> useBraces
            MultiLine -> id
        ub $ do
          located func (p_hsExpr' s)
          breakpoint
          indentArg $ sep breakpoint (located' p_hsExpr) initp
        indentArg $ do
          unless (null initp) breakpoint
          located lastp p_hsExpr
      Hanging -> do
        useBraces . switchLayout [initSpan] $ do
          located func (p_hsExpr' s)
          breakpoint
          sep breakpoint (located' p_hsExpr) initp
        placeHanging placement . dontUseBraces $
          located lastp p_hsExpr
  HsAppType NoExtField e a -> do
    located e p_hsExpr
    breakpoint
    inci $ do
      txt "@"
      -- Insert a space when the type is represented as a TH splice to avoid
      -- gluing @ and $ together.
      case unLoc (hswc_body a) of
        HsSpliceTy {} -> space
        _ -> return ()
      located (hswc_body a) p_hsType
  OpApp NoExtField x op y -> do
    let opTree = OpBranch (exprOpTree x) op (exprOpTree y)
    p_exprOpTree s (reassociateOpTree getOpName opTree)
  NegApp NoExtField e NoExtField -> do
    negativeLiterals <- isExtensionEnabled NegativeLiterals
    let isLiteral = case unLoc e of
          HsLit {} -> True
          HsOverLit {} -> True
          _ -> False
    txt "-"
    -- If NegativeLiterals is enabled, we have to insert a space before
    -- negated literals, as `- 1` and `-1` have differing AST.
    when (negativeLiterals && isLiteral) space
    located e p_hsExpr
  HsPar NoExtField e -> do
    parens s $ sitcc $ located e $ dontUseBraces . p_hsExpr
  SectionL NoExtField x op -> do
    located x p_hsExpr
    breakpoint
    inci (located op p_hsExpr)
  SectionR NoExtField op x -> do
    located op p_hsExpr
    useRecordDot' <- useRecordDot
    let isRecordDot' = isRecordDot (unLoc op) (getLoc x)
    unless (useRecordDot' && isRecordDot') breakpoint
    inci (located x p_hsExpr)
  ExplicitTuple NoExtField args boxity ->
    let isSection = any (isMissing . unLoc) args
        isMissing = \case
          Missing NoExtField -> True
          _ -> False
        p_arg = \case
          Present NoExtField x -> located x p_hsExprListItem
          Missing NoExtField -> pure ()
        p_larg = sitcc . located' p_arg
        parens' =
          case boxity of
            Boxed -> parens
            Unboxed -> parensHash
     in if isSection
          then
            switchLayout [] . parens' s $
              sep comma p_larg args
          else
            switchLayout (getLoc <$> args) . parens' s $
              sep commaDel p_larg args
  ExplicitSum NoExtField tag arity e ->
    p_unboxedSum N tag arity (located e p_hsExpr)
  HsCase NoExtField e mgroup ->
    p_case exprPlacement p_hsExpr e mgroup
  HsIf NoExtField if' then' else' ->
    p_if exprPlacement p_hsExpr if' then' else'
  HsMultiIf NoExtField guards -> do
    txt "if"
    breakpoint
    inci . inci $ sep newline (located' (p_grhs RightArrow)) guards
  HsLet NoExtField localBinds e ->
    p_let p_hsExpr localBinds e
  HsDo NoExtField ctx es -> do
    let doBody moduleName header = do
          forM_ moduleName $ \m -> atom m *> txt "."
          txt header
          p_stmts exprPlacement (p_hsExpr' S) es
        compBody = brackets N . located es $ \xs -> do
          let p_parBody =
                sep
                  (breakpoint >> txt "|" >> space)
                  p_seqBody
              p_seqBody =
                sitcc'
                  . sep
                    commaDel
                    (located' (sitcc . p_stmt))
              sitcc' x =
                getPrinterOpt poCommaStyle >>= \case
                  Leading -> id x
                  Trailing -> sitcc x
              stmts = init xs
              yield = last xs
              lists = foldr (liftAppend . gatherStmt) [] stmts
          located yield p_stmt
          breakpoint
          txt "|"
          space
          p_parBody lists
    case ctx of
      DoExpr moduleName -> doBody moduleName "do"
      MDoExpr moduleName -> doBody moduleName "mdo"
      ListComp -> compBody
      MonadComp -> compBody
      ArrowExpr -> notImplemented "ArrowExpr"
      GhciStmtCtxt -> notImplemented "GhciStmtCtxt"
      PatGuard _ -> notImplemented "PatGuard"
      ParStmtCtxt _ -> notImplemented "ParStmtCtxt"
      TransStmtCtxt _ -> notImplemented "TransStmtCtxt"
  ExplicitList _ _ xs ->
    brackets s $
      sep commaDel (sitcc . located' p_hsExprListItem) xs
  RecordCon {..} -> do
    located rcon_con_name atom
    breakpointPreRecordBrace
    let HsRecFields {..} = rcon_flds
        updName f =
          (f :: HsRecField GhcPs (LHsExpr GhcPs))
            { hsRecFieldLbl = case unLoc $ hsRecFieldLbl f of
                FieldOcc _ n -> n
            }
        fields = located' (p_hsRecField . updName) <$> rec_flds
        dotdot =
          case rec_dotdot of
            Just {} -> [txt ".."]
            Nothing -> []
    inci . braces N $
      sep commaDel sitcc (fields <> dotdot)
  RecordUpd {..} -> do
    located rupd_expr p_hsExpr
    useRecordDot' <- useRecordDot
    let mrs sp = case getLoc sp of
          RealSrcSpan r _ -> Just r
          _ -> Nothing
    let isPluginForm =
          ((1 +) . srcSpanEndCol <$> mrs rupd_expr)
            == (srcSpanStartCol <$> mrs (head rupd_flds))
            && onTheSameLine (getLoc rupd_expr) (getLoc $ head rupd_flds)
    unless (useRecordDot' && isPluginForm) breakpointPreRecordBrace
    let updName f =
          (f :: HsRecUpdField GhcPs)
            { hsRecFieldLbl = case unLoc $ hsRecFieldLbl f of
                Ambiguous _ n -> n
                Unambiguous _ n -> n
            }
        updBraces =
          if useRecordDot' && isPluginForm
            then recordDotBraces
            else inci . braces N
    updBraces $
      sep
        commaDel
        (sitcc . located' (p_hsRecField . updName))
        rupd_flds
  ExprWithTySig NoExtField x HsWC {hswc_body = HsIB {..}} -> sitcc $ do
    located x p_hsExpr
    space
    txt "::"
    breakpoint
    inci $ located hsib_body p_hsType
  ArithSeq NoExtField _ x ->
    case x of
      From from -> brackets s $ do
        located from p_hsExpr
        breakpoint
        txt ".."
      FromThen from next -> brackets s $ do
        sep commaDel (located' p_hsExpr) [from, next]
        breakpoint
        txt ".."
      FromTo from to -> brackets s $ do
        located from p_hsExpr
        breakpoint
        txt ".."
        space
        located to p_hsExpr
      FromThenTo from next to -> brackets s $ do
        sep commaDel (located' p_hsExpr) [from, next]
        breakpoint
        txt ".."
        space
        located to p_hsExpr
  HsBracket NoExtField x -> p_hsBracket x
  HsRnBracketOut {} -> notImplemented "HsRnBracketOut"
  HsTcBracketOut {} -> notImplemented "HsTcBracketOut"
  HsSpliceE NoExtField splice -> p_hsSplice splice
  HsProc NoExtField p e -> do
    txt "proc"
    located p $ \x -> do
      breakpoint
      inci (p_pat x)
      breakpoint
    txt "->"
    placeHanging (cmdTopPlacement (unLoc e)) $
      located e p_hsCmdTop
  HsStatic _ e -> do
    txt "static"
    breakpoint
    inci (located e p_hsExpr)
  HsTick {} -> notImplemented "HsTick"
  HsBinTick {} -> notImplemented "HsBinTick"
  HsPragE NoExtField prag x -> case prag of
    HsPragSCC NoExtField _ name -> do
      txt "{-# SCC "
      atom name
      txt " #-}"
      breakpoint
      located x p_hsExpr
    HsPragTick {} -> notImplemented "HsTickPragma"

p_patSynBind :: PatSynBind GhcPs GhcPs -> R ()
p_patSynBind PSB {..} = do
  let rhs = do
        space
        case psb_dir of
          Unidirectional -> do
            txt "<-"
            breakpoint
            located psb_def p_pat
          ImplicitBidirectional -> do
            equals
            breakpoint
            located psb_def p_pat
          ExplicitBidirectional mgroup -> do
            txt "<-"
            breakpoint
            located psb_def p_pat
            breakpoint
            txt "where"
            breakpoint
            inci (p_matchGroup (Function psb_id) mgroup)
  txt "pattern"
  case psb_args of
    PrefixCon xs -> do
      space
      p_rdrName psb_id
      inci $ do
        switchLayout (getLoc <$> xs) $ do
          unless (null xs) breakpoint
          sitcc (sep breakpoint p_rdrName xs)
        rhs
    RecCon xs -> do
      space
      p_rdrName psb_id
      inci $ do
        switchLayout (getLoc . recordPatSynPatVar <$> xs) $ do
          unless (null xs) breakpointPreRecordBrace
          braces N $
            sep commaDel (p_rdrName . recordPatSynPatVar) xs
        rhs
    InfixCon l r -> do
      switchLayout [getLoc l, getLoc r] $ do
        space
        p_rdrName l
        breakpoint
        inci $ do
          p_rdrName psb_id
          space
          p_rdrName r
      inci rhs

p_case ::
  Data body =>
  -- | Placer
  (body -> Placement) ->
  -- | Render
  (body -> R ()) ->
  -- | Expression
  LHsExpr GhcPs ->
  -- | Match group
  MatchGroup GhcPs (Located body) ->
  R ()
p_case placer render e mgroup = do
  txt "case"
  space
  located e p_hsExpr
  space
  txt "of"
  breakpoint
  inci (p_matchGroup' placer render Case mgroup)

p_lamcase ::
  Data body =>
  -- | Placer
  (body -> Placement) ->
  -- | Render
  (body -> R ()) ->
  -- | Expression
  MatchGroup GhcPs (Located body) ->
  R ()
p_lamcase placer render mgroup = do
  txt "\\case"
  breakpoint
  inci (p_matchGroup' placer render LambdaCase mgroup)

p_if ::
  Data body =>
  -- | Placer
  (body -> Placement) ->
  -- | Render
  (body -> R ()) ->
  -- | If
  LHsExpr GhcPs ->
  -- | Then
  Located body ->
  -- | Else
  Located body ->
  R ()
p_if placer render if' then' else' = do
  txt "if"
  space
  located if' p_hsExpr
  breakpoint
  inci $ do
    txt "then"
    space
    located then' $ \x ->
      placeHanging (placer x) (render x)
    breakpoint
    txt "else"
    space
    located else' $ \x ->
      placeHanging (placer x) (render x)

p_let ::
  Data body =>
  -- | Render
  (body -> R ()) ->
  Located (HsLocalBindsLR GhcPs GhcPs) ->
  Located body ->
  R ()
p_let render localBinds e = sitcc $ do
  txt "let"
  space
  dontUseBraces $ sitcc (located localBinds p_hsLocalBinds)
  vlayout space (newline >> txt " ")
  txt "in"
  space
  sitcc (located e render)

p_pat :: Pat GhcPs -> R ()
p_pat = \case
  WildPat NoExtField -> txt "_"
  VarPat NoExtField name -> p_rdrName name
  LazyPat NoExtField pat -> do
    txt "~"
    located pat p_pat
  AsPat NoExtField name pat -> do
    p_rdrName name
    txt "@"
    located pat p_pat
  ParPat NoExtField pat ->
    located pat (parens S . sitcc . p_pat)
  BangPat NoExtField pat -> do
    txt "!"
    located pat p_pat
  ListPat NoExtField pats ->
    brackets S $ sep commaDel (located' p_pat) pats
  TuplePat NoExtField pats boxing -> do
    let parens' =
          case boxing of
            Boxed -> parens S
            Unboxed -> parensHash S
    parens' $ sep commaDel (sitcc . located' p_pat) pats
  SumPat NoExtField pat tag arity ->
    p_unboxedSum S tag arity (located pat p_pat)
  ConPat NoExtField pat details ->
    case details of
      PrefixCon xs -> sitcc $ do
        p_rdrName pat
        unless (null xs) $ do
          breakpoint
          inci . sitcc $ sep breakpoint (sitcc . located' p_pat) xs
      RecCon (HsRecFields fields dotdot) -> do
        p_rdrName pat
        breakpointPreRecordBrace
        let f = \case
              Nothing -> txt ".."
              Just x -> located x p_pat_hsRecField
        inci . braces N . sep commaDel f $
          case dotdot of
            Nothing -> Just <$> fields
            Just (L _ n) -> (Just <$> take n fields) ++ [Nothing]
      InfixCon l r -> do
        switchLayout [getLoc l, getLoc r] $ do
          located l p_pat
          breakpoint
          inci $ do
            p_rdrName pat
            space
            located r p_pat
  ViewPat NoExtField expr pat -> sitcc $ do
    located expr p_hsExpr
    space
    txt "->"
    breakpoint
    inci (located pat p_pat)
  SplicePat NoExtField splice -> p_hsSplice splice
  LitPat NoExtField p -> atom p
  NPat NoExtField v (isJust -> isNegated) NoExtField -> do
    when isNegated $ do
      txt "-"
      negativeLiterals <- isExtensionEnabled NegativeLiterals
      when negativeLiterals space
    located v (atom . ol_val)
  NPlusKPat NoExtField n k _ _ _ -> sitcc $ do
    p_rdrName n
    breakpoint
    inci $ do
      txt "+"
      space
      located k (atom . ol_val)
  SigPat NoExtField pat HsPS {..} -> do
    located pat p_pat
    p_typeAscription (HsWC NoExtField (HsIB NoExtField hsps_body))

p_pat_hsRecField :: HsRecField' (FieldOcc GhcPs) (LPat GhcPs) -> R ()
p_pat_hsRecField HsRecField {..} = do
  located hsRecFieldLbl $ \x ->
    p_rdrName (rdrNameFieldOcc x)
  unless hsRecPun $ do
    space
    equals
    breakpoint
    inci (located hsRecFieldArg p_pat)

p_unboxedSum :: BracketStyle -> ConTag -> Arity -> R () -> R ()
p_unboxedSum s tag arity m = do
  let before = tag - 1
      after = arity - before - 1
      args = replicate before Nothing <> [Just m] <> replicate after Nothing
      f x =
        case x :: Maybe (R ()) of
          Nothing ->
            space
          Just m' -> do
            space
            m'
            space
  parensHash s $ sep (txt "|") f args

p_hsSplice :: HsSplice GhcPs -> R ()
p_hsSplice = \case
  HsTypedSplice NoExtField deco _ expr -> p_hsSpliceTH True expr deco
  HsUntypedSplice NoExtField deco _ expr -> p_hsSpliceTH False expr deco
  HsQuasiQuote NoExtField _ quoterName srcSpan str -> do
    txt "["
    p_rdrName (L srcSpan quoterName)
    txt "|"
    -- QuasiQuoters often rely on precise custom strings. We cannot do any
    -- formatting here without potentially breaking someone's code.
    atom str
    txt "|]"
  HsSpliced {} -> notImplemented "HsSpliced"

p_hsSpliceTH ::
  -- | Typed splice?
  Bool ->
  -- | Splice expression
  LHsExpr GhcPs ->
  -- | Splice decoration
  SpliceDecoration ->
  R ()
p_hsSpliceTH isTyped expr = \case
  DollarSplice -> do
    txt decoSymbol
    located expr (sitcc . p_hsExpr)
  BareSplice ->
    located expr (sitcc . p_hsExpr)
  where
    decoSymbol = if isTyped then "$$" else "$"

p_hsBracket :: HsBracket GhcPs -> R ()
p_hsBracket = \case
  ExpBr NoExtField expr -> do
    anns <- getEnclosingAnns
    let name = case anns of
          AnnOpenEQ : _ -> ""
          _ -> "e"
    quote name (located expr p_hsExpr)
  PatBr NoExtField pat -> located pat (quote "p" . p_pat)
  DecBrL NoExtField decls -> quote "d" (handleStarIsType decls (p_hsDecls Free decls))
  DecBrG NoExtField _ -> notImplemented "DecBrG" -- result of renamer
  TypBr NoExtField ty -> quote "t" (located ty (handleStarIsType ty . p_hsType))
  VarBr NoExtField isSingleQuote name -> do
    txt (bool "''" "'" isSingleQuote)
    -- HACK As you can see we use 'noLoc' here to be able to pass name into
    -- 'p_rdrName' since the latter expects a "located" thing. The problem
    -- is that 'VarBr' doesn't provide us with location of the name. This in
    -- turn makes it impossible to detect if there are parentheses around
    -- it, etc. So we have to add parentheses manually assuming they are
    -- necessary for all operators.
    let isOperator =
          all
            (\i -> isPunctuation i || isSymbol i)
            (showOutputable (rdrNameOcc name))
            && not (doesNotNeedExtraParens name)
        wrapper = if isOperator then parens N else id
    wrapper $ p_rdrName (noLoc name)
  TExpBr NoExtField expr -> do
    txt "[||"
    breakpoint'
    located expr p_hsExpr
    breakpoint'
    txt "||]"
  where
    quote :: Text -> R () -> R ()
    quote name body = do
      txt "["
      txt name
      txt "|"
      breakpoint'
      inci $ do
        dontUseBraces body
        breakpoint'
        txt "|]"
    -- With StarIsType, type and declaration brackets might end with a *,
    -- so we have to insert a space in the end to prevent the (mis)parsing
    -- of an (*|) operator.
    -- The detection is a bit overcautious, as it adds the spaces as soon as
    -- HsStarTy is anywhere in the type/declaration.
    handleStarIsType :: Data a => a -> R () -> R ()
    handleStarIsType a p
      | containsHsStarTy a = space *> p <* space
      | otherwise = p
      where
        containsHsStarTy = everything (||) $ \b -> case cast @_ @(HsType GhcPs) b of
          Just HsStarTy {} -> True
          _ -> False

-- Print the source text of a string literal while indenting
-- gaps correctly.

p_stringLit :: String -> R ()
p_stringLit src =
  let s = splitGaps src
      singleLine =
        txt $ Text.pack (mconcat s)
      multiLine =
        sitcc $ sep breakpoint (txt . Text.pack) (backslashes s)
   in vlayout singleLine multiLine
  where
    -- Split a string on gaps (backslash delimited whitespaces)
    --
    -- > splitGaps "bar\\  \\fo\\&o" == ["bar", "fo\\&o"]
    splitGaps :: String -> [String]
    splitGaps "" = []
    splitGaps s =
      let -- A backslash and a whitespace starts a "gap"
          p (Just '\\', _, _) = True
          p (_, '\\', Just c) | ghcSpace c = False
          p _ = True
       in case span p (zipPrevNext s) of
            (l, r) ->
              let -- drop the initial '\', any amount of 'ghcSpace', and another '\'
                  r' = drop 1 . dropWhile ghcSpace . drop 1 $ map orig r
               in map orig l : splitGaps r'
    -- GHC's definition of whitespaces in strings
    -- See: https://gitlab.haskell.org/ghc/ghc/blob/86753475/compiler/parser/Lexer.x#L1653
    ghcSpace :: Char -> Bool
    ghcSpace c = c <= '\x7f' && is_space c
    -- Add backslashes to the inner side of the strings
    --
    -- > backslashes ["a", "b", "c"] == ["a\\", "\\b\\", "\\c"]
    backslashes :: [String] -> [String]
    backslashes (x : y : xs) = (x ++ "\\") : backslashes (('\\' : y) : xs)
    backslashes xs = xs
    -- Attaches previous and next items to each list element
    zipPrevNext :: [a] -> [(Maybe a, a, Maybe a)]
    zipPrevNext xs =
      let z =
            zip
              (zip (Nothing : map Just xs) xs)
              (map Just (tail xs) ++ repeat Nothing)
       in map (\((p, x), n) -> (p, x, n)) z
    orig (_, x, _) = x

----------------------------------------------------------------------------
-- Helpers

-- | Return the wrapping function controlling the use of braces according to
-- the current layout.
layoutToBraces :: Layout -> R () -> R ()
layoutToBraces = \case
  SingleLine -> useBraces
  MultiLine -> id

-- | Append each element in both lists with semigroups. If one list is shorter
-- than the other, return the rest of the longer list unchanged.
liftAppend :: Semigroup a => [a] -> [a] -> [a]
liftAppend [] [] = []
liftAppend [] (y : ys) = y : ys
liftAppend (x : xs) [] = x : xs
liftAppend (x : xs) (y : ys) = x <> y : liftAppend xs ys

getGRHSSpan :: GRHS GhcPs (Located body) -> SrcSpan
getGRHSSpan (GRHS NoExtField guards body) =
  combineSrcSpans' $ getLoc body :| map getLoc guards

-- | Place a thing that may have a hanging form. This function handles how
-- to separate it from preceding expressions and whether to bump indentation
-- depending on what sort of expression we have.
placeHanging :: Placement -> R () -> R ()
placeHanging placement m =
  case placement of
    Hanging -> do
      space
      m
    Normal -> do
      breakpoint
      inci m

-- | Check if given block contains single expression which has a hanging
-- form.
blockPlacement ::
  (body -> Placement) ->
  [LGRHS GhcPs (Located body)] ->
  Placement
blockPlacement placer [L _ (GRHS NoExtField _ (L _ x))] = placer x
blockPlacement _ _ = Normal

-- | Check if given command has a hanging form.
cmdPlacement :: HsCmd GhcPs -> Placement
cmdPlacement = \case
  HsCmdLam NoExtField _ -> Hanging
  HsCmdCase NoExtField _ _ -> Hanging
  HsCmdLamCase NoExtField _ -> Hanging
  HsCmdDo NoExtField _ -> Hanging
  _ -> Normal

cmdTopPlacement :: HsCmdTop GhcPs -> Placement
cmdTopPlacement (HsCmdTop NoExtField (L _ x)) = cmdPlacement x

-- | Check if given expression has a hanging form.
exprPlacement :: HsExpr GhcPs -> Placement
exprPlacement = \case
  -- Only hang lambdas with single line parameter lists
  HsLam NoExtField mg -> case mg of
    MG _ (L _ [L _ (Match NoExtField _ (x : xs) _)]) _
      | isOneLineSpan (combineSrcSpans' $ fmap getLoc (x :| xs)) ->
        Hanging
    _ -> Normal
  HsLamCase NoExtField _ -> Hanging
  HsCase NoExtField _ _ -> Hanging
  HsDo NoExtField (DoExpr _) _ -> Hanging
  HsDo NoExtField (MDoExpr _) _ -> Hanging
  OpApp NoExtField _ op y ->
    case (fmap getOpNameStr . getOpName . unLoc) op of
      Just "$" -> exprPlacement (unLoc y)
      _ -> Normal
  HsApp NoExtField _ y -> exprPlacement (unLoc y)
  HsProc NoExtField p _ ->
    -- Indentation breaks if pattern is longer than one line and left
    -- hanging. Consequently, only apply hanging when it is safe.
    if isOneLineSpan (getLoc p)
      then Hanging
      else Normal
  _ -> Normal

withGuards :: [LGRHS GhcPs (Located body)] -> Bool
withGuards = any (checkOne . unLoc)
  where
    checkOne :: GRHS GhcPs (Located body) -> Bool
    checkOne (GRHS NoExtField [] _) = False
    checkOne _ = True

exprOpTree :: LHsExpr GhcPs -> OpTree (LHsExpr GhcPs) (LHsExpr GhcPs)
exprOpTree (L _ (OpApp NoExtField x op y)) = OpBranch (exprOpTree x) op (exprOpTree y)
exprOpTree n = OpNode n

getOpName :: HsExpr GhcPs -> Maybe RdrName
getOpName = \case
  HsVar NoExtField (L _ a) -> Just a
  _ -> Nothing

getOpNameStr :: RdrName -> String
getOpNameStr = occNameString . rdrNameOcc

p_exprOpTree ::
  -- | Bracket style to use
  BracketStyle ->
  OpTree (LHsExpr GhcPs) (LHsExpr GhcPs) ->
  R ()
p_exprOpTree s (OpNode x) = located x (p_hsExpr' s)
p_exprOpTree s (OpBranch x op y) = do
  let placement = opBranchPlacement exprPlacement x y
      -- Distinguish holes used in infix notation.
      -- eg. '1 _foo 2' and '1 `_foo` 2'
      opWrapper = case unLoc op of
        HsUnboundVar NoExtField _ -> backticks
        _ -> id
  ub <- opBranchBraceStyle placement
  let opNameStr = (fmap getOpNameStr . getOpName . unLoc) op
      gotDollar = opNameStr == Just "$"
      gotColon = opNameStr == Just ":"
      gotRecordDot = isRecordDot (unLoc op) (opTreeLoc y)
      lhs =
        switchLayout [opTreeLoc x] $
          p_exprOpTree s x
      p_op = located op (opWrapper . p_hsExpr)
      p_y = switchLayout [opTreeLoc y] (p_exprOpTree N y)
      isSection = case (opTreeLoc x, getLoc op) of
        (RealSrcSpan treeSpan _, RealSrcSpan opSpan _) ->
          srcSpanEndCol treeSpan /= srcSpanStartCol opSpan
        _ -> False
      isDoBlock = \case
        OpNode (L _ HsDo {}) -> True
        _ -> False
  useRecordDot' <- useRecordDot
  if
      | gotColon -> do
        ub lhs
        space
        p_op
        case placement of
          Hanging -> do
            space
            p_y
          Normal -> do
            breakpoint
            inciIf (isDoBlock y) p_y
      | gotDollar
          && isOneLineSpan (opTreeLoc x)
          && placement == Normal -> do
        useBraces lhs
        space
        p_op
        breakpoint
        inci p_y
      | useRecordDot' && gotRecordDot -> do
        lhs
        when isSection space
        p_op
        p_y
      | otherwise -> do
        ub lhs
        let opAndRhs = do
              p_op
              space
              p_y
        case x of
          -- This case prevents an operator from being indented past the start of a `do` block
          -- constituting its left operand, thus altering the AST.
          -- This is only relevant when the `do` block is on one line, as otherwise we will
          -- insert a newline after `do` anyway.
          OpNode (unLoc -> HsDo _ _ _) | isOneLineSpan (opTreeLoc x) -> breakpoint >> opAndRhs
          _ -> placeHanging placement opAndRhs

pattern CmdTopCmd :: HsCmd GhcPs -> LHsCmdTop GhcPs
pattern CmdTopCmd cmd <- (L _ (HsCmdTop NoExtField (L _ cmd)))

cmdOpTree :: LHsCmdTop GhcPs -> OpTree (LHsCmdTop GhcPs) (LHsExpr GhcPs)
cmdOpTree = \case
  CmdTopCmd (HsCmdArrForm NoExtField op Infix _ [x, y]) ->
    OpBranch (cmdOpTree x) op (cmdOpTree y)
  n -> OpNode n

p_cmdOpTree :: OpTree (LHsCmdTop GhcPs) (LHsExpr GhcPs) -> R ()
p_cmdOpTree = \case
  OpNode n -> located n p_hsCmdTop
  OpBranch x op y -> do
    let placement = opBranchPlacement cmdTopPlacement x y
    ub <- opBranchBraceStyle placement
    ub $ p_cmdOpTree x
    placeHanging placement $ do
      located op p_hsExpr
      space
      p_cmdOpTree y

opBranchPlacement ::
  -- | Placement of nodes
  (ty -> Placement) ->
  -- | Left branch
  OpTree (Located ty) op ->
  -- | Right branch
  OpTree (Located ty) op ->
  Placement
opBranchPlacement f x y
  -- If the beginning of the first argument and the second argument are on
  -- the same line, and the second argument has a hanging form, use hanging
  -- placement.
  | isOneLineSpan (mkSrcSpan (srcSpanStart (opTreeLoc x)) (srcSpanStart (opTreeLoc y))),
    OpNode (L _ n) <- y =
    f n
  | otherwise = Normal

opBranchBraceStyle :: Placement -> R (R () -> R ())
opBranchBraceStyle placement =
  getLayout <&> \case
    SingleLine -> useBraces
    MultiLine -> case placement of
      Hanging -> useBraces
      Normal -> dontUseBraces

-- | Return 'True' if given expression is a record-dot operator expression.
isRecordDot ::
  -- | Operator expression
  HsExpr GhcPs ->
  -- | Span of the expression on the right-hand side of the operator
  SrcSpan ->
  Bool
isRecordDot op (RealSrcSpan ySpan _) = case op of
  HsVar NoExtField (L (RealSrcSpan opSpan _) opName) ->
    (getOpNameStr opName == ".") && (srcSpanEndCol opSpan == srcSpanStartCol ySpan)
  _ -> False
isRecordDot _ _ = False

-- | Get annotations for the enclosing element.
getEnclosingAnns :: R [AnnKeywordId]
getEnclosingAnns = do
  e <- getEnclosingSpan (const True)
  case e of
    Nothing -> return []
    Just e' -> getAnns (RealSrcSpan e' Nothing)

-- | For use before record braces. Collapse to empty if not 'poRecordBraceSpace'.
breakpointPreRecordBrace :: R ()
breakpointPreRecordBrace = do
  useSpace <- getPrinterOpt poRecordBraceSpace
  if useSpace
    then breakpoint
    else breakpoint'

-- | For nested lists/tuples, pad with whitespace so that we always indent correctly,
-- rather than sometimes indenting by 2 regardless of 'poIndentation'.
p_hsExprListItem :: HsExpr GhcPs -> R ()
p_hsExprListItem e = do
  indent <- getPrinterOpt poIndentation
  when (listLike e) $ do
    getPrinterOpt poCommaStyle >>= \case
      Leading -> breakpoint'
      Trailing -> pure ()
    vlayout (pure ()) (spaces $ indent - 2)
  p_hsExpr e
  where
    spaces n = txt $ Text.replicate n " "
    listLike = \case
      ExplicitList {} -> True
      ExplicitTuple {} -> True
      _ -> False