packages feed

tilia-0.0.1.0: src/Tilia/Render/Body.hs

{-# LANGUAGE LambdaCase #-}

-- | Which constructs absorb the line break that introduces them.
--
-- A body is a node together with the site it stands at, and the one question
-- an enclosing construct has to ask of it is where to put it: on the line it
-- has already started, or on the next one indented. "Tilia.Doc.Body"
-- states that question as a class; this module answers it, for the two kinds
-- of node that can stand as a body.
--
-- The answers are a table, not an argument. Threading a @body -> Placement@
-- callback through every construct that has a body—equations, guards, @if@,
-- @let@, @case@, lambdas, statements—spreads one small piece of knowledge
-- across a dozen signatures and makes each of them carry a second parameter
-- that only ever has two possible values. Here it is written down once, and
-- what the constructs pass around is the body itself.
module Tilia.Render.Body
  ( -- * Bodies
    ExprBody (..),
    CmdBody (..),
    CmdTopBody (..),

    -- * The table
    exprHangs,
    operatorName,
    cmdTopHangs,
  )
where

import GHC.Hs
import GHC.Types.Name.Occurrence (occNameString)
import GHC.Types.Name.Reader (RdrName, rdrNameOcc)
import GHC.Types.SrcLoc (GenLocated (..), unLoc)
import Tilia.Doc.Body
import Tilia.Doc.Combinators
import Tilia.Render.Context
import Tilia.Span
import Tilia.Span.Ghc

----------------------------------------------------------------------------
-- Bodies

-- | An expression standing as the body of an enclosing construct.
data ExprBody = ExprBody Ctx Site (LHsExpr GhcPs)

instance Body ExprBody where
  printBody (ExprBody ctx site e) = knotExpr (ctxKnot ctx) ctx site e
  bodyPlacement (ExprBody _ _ e) = exprHangs (unLoc e)

-- | A command standing as the body of an enclosing construct.
data CmdBody = CmdBody Ctx Site (LHsCmd GhcPs)

instance Body CmdBody where
  printBody (CmdBody ctx site c) = knotCmd (ctxKnot ctx) ctx site c
  bodyPlacement (CmdBody _ _ c) = cmdHangs (unLoc c)

-- | A command at the top of an arrow form.
data CmdTopBody = CmdTopBody Ctx Site (LHsCmdTop GhcPs)

instance Body CmdTopBody where
  printBody (CmdTopBody ctx site l) =
    at ctx l (\(HsCmdTop _ cmd) -> knotCmd (ctxKnot ctx) ctx site cmd)
  bodyPlacement (CmdTopBody _ _ l) = cmdTopHangs (unLoc l)

----------------------------------------------------------------------------
-- The table

-- | Does this expression absorb the line break that introduces it?
--
-- A @do@ block, a @case@ and a lambda all begin with a keyword and continue
-- on the lines below, so @f = do@ costs nothing and saves a line. Everything
-- not named here has to start on a line of its own.
exprHangs :: HsExpr GhcPs -> Placement
exprHangs = \case
  HsDo _ (DoExpr _) _ -> Hanging
  HsDo _ (MDoExpr _) _ -> Hanging
  HsCase {} -> Hanging
  HsLam _ lamVariant mg -> case lamVariant of
    LamCase -> Hanging
    LamCases -> Hanging
    -- A lambda whose parameters ran over several lines leaves its body
    -- indented under nothing legible, so only a compact one hangs.
    LamSingle -> case mg of
      MG _ (L _ [L _ (Match _ _ (L _ ps@(_ : _)) _)])
        | maybe False isSingleLine (spansOf ps) -> Hanging
      _ -> Normal
  HsProc _ p _
    -- The indentation breaks when the pattern runs over more than one line,
    -- so hanging is only safe when it does not.
    | maybe False isSingleLine (spanOf p) -> Hanging
    | otherwise -> Normal
  -- An application hangs on its last argument, and a chain through @$@ on
  -- its right operand: both of those are the thing that would be introduced.
  -- No other operator qualifies, @$@ being the one whose whole purpose is to
  -- hand a block to what precedes it.
  HsApp _ _ y -> exprHangs (unLoc y)
  OpApp _ _ op y
    | Just n <- operatorName op,
      occNameString (rdrNameOcc n) == "$" ->
        exprHangs (unLoc y)
  _ -> Normal

-- | Does this command absorb the line break that introduces it?
cmdHangs :: HsCmd GhcPs -> Placement
cmdHangs = \case
  HsCmdDo {} -> Hanging
  HsCmdCase {} -> Hanging
  HsCmdLam {} -> Hanging
  _ -> Normal

cmdTopHangs :: HsCmdTop GhcPs -> Placement
cmdTopHangs (HsCmdTop _ c) = cmdHangs (unLoc c)

-- | The name of an operator, when the expression standing as one is a name.
operatorName :: LHsExpr GhcPs -> Maybe RdrName
operatorName e = case unLoc e of
  HsVar _ (L _ n) -> Just n
  _ -> Nothing