packages feed

moonlight-pale-0.1.0.0: src-ghc-surface/Moonlight/Pale/Ghc/Expr/Render/Document.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module Moonlight.Pale.Ghc.Expr.Render.Document
  ( PageWidth (..),
    defaultPageWidth,
    mkPageWidth,
    LayoutPolicy (..),
    RenderMode (..),
    RenderDocument (..),
    CompactDocument (..),
    PrettyDocument (..),
    intercalateDocument,
    (<+>),
    nest,
    renderCompactDocument,
    renderPrettyDocument,
    renderDelimitedExpression,
    renderBlock,
    wrapParen,
    hcat,
    intercalateDoc
  )
where

import Data.Kind (Constraint, Type)
import Data.Text qualified as Text
import Data.Text.Lazy qualified as LazyText
import Data.Text.Lazy.Builder qualified as TextBuilder
import Prettyprinter qualified as Pretty
import Prettyprinter.Render.Text qualified as PrettyText
type PageWidth :: Type
newtype PageWidth = PageWidth Int
  deriving stock (Eq, Ord, Show)

defaultPageWidth :: PageWidth
defaultPageWidth =
  PageWidth 80

mkPageWidth :: Int -> Maybe PageWidth
mkPageWidth columnCount
  | columnCount > 0 =
      Just (PageWidth columnCount)
  | otherwise =
      Nothing

type LayoutPolicy :: Type
data LayoutPolicy
  = CompactLayout
  | PrettyLayout !PageWidth
  deriving stock (Eq, Ord, Show)

type RenderMode :: Type
data RenderMode
  = CompactRender
  | GeneratedRender

type RenderDocument :: Type -> Constraint
class Monoid document => RenderDocument document where
  text :: String -> document
  hangingIndent :: Int -> document -> document
  vcat :: [document] -> document
  hsep :: [document] -> document
  group :: document -> document
  line :: document
  parenthesizeDoc :: document -> document

newtype CompactDocument = CompactDocument
  { compactDocumentBuilder :: TextBuilder.Builder
  }
  deriving newtype (Semigroup, Monoid)

instance RenderDocument CompactDocument where
  text =
    CompactDocument . TextBuilder.fromString
  hangingIndent _ documentValue =
    documentValue
  vcat =
    intercalateDocument (text "\n")
  hsep =
    intercalateDocument (text " ")
  group =
    id
  line =
    text "\n"
  parenthesizeDoc documentValue =
    text "(" <> documentValue <> text ")"

newtype PrettyDocument = PrettyDocument
  { prettyDocumentValue :: Pretty.Doc ()
  }
  deriving newtype (Semigroup, Monoid)

instance RenderDocument PrettyDocument where
  text =
    PrettyDocument . Pretty.pretty
  hangingIndent indentationAmount (PrettyDocument documentValue) =
    PrettyDocument (Pretty.nest indentationAmount documentValue)
  vcat documentValues =
    PrettyDocument
      ( Pretty.concatWith
          (\leftDocument rightDocument -> leftDocument <> Pretty.hardline <> rightDocument)
          (fmap prettyDocumentValue documentValues)
      )
  hsep =
    PrettyDocument . Pretty.hsep . fmap prettyDocumentValue
  group =
    PrettyDocument . Pretty.group . prettyDocumentValue
  line =
    PrettyDocument Pretty.line
  parenthesizeDoc =
    PrettyDocument . Pretty.parens . prettyDocumentValue

intercalateDocument ::
  Monoid document =>
  document ->
  [document] ->
  document
intercalateDocument separatorDocument documentValues =
  case documentValues of
    [] ->
      mempty
    firstDocument : remainingDocuments ->
      firstDocument
        <> foldMap (separatorDocument <>) remainingDocuments

(<+>) ::
  RenderDocument document =>
  document ->
  document ->
  document
leftDocument <+> rightDocument =
  leftDocument <> text " " <> rightDocument

-- Indent a block, first line included; 'hangingIndent' alone moves only the
-- continuation lines and so must not be substituted here.
nest ::
  RenderDocument document =>
  Int ->
  document ->
  document
nest indentationAmount documentValue =
  text (replicate indentationAmount ' ')
    <> hangingIndent indentationAmount documentValue

renderCompactDocument :: CompactDocument -> Text.Text
renderCompactDocument =
  LazyText.toStrict
    . TextBuilder.toLazyText
    . compactDocumentBuilder

renderPrettyDocument :: PageWidth -> PrettyDocument -> Text.Text
renderPrettyDocument (PageWidth columnCount) =
  PrettyText.renderStrict
    . Pretty.layoutPretty
      (Pretty.LayoutOptions (Pretty.AvailablePerLine columnCount 1.0))
    . prettyDocumentValue

renderDelimitedExpression ::
  RenderDocument document =>
  RenderMode ->
  document ->
  String ->
  document ->
  document
renderDelimitedExpression renderMode lhsDoc delimiter bodyDoc =
  case renderMode of
    CompactRender ->
      lhsDoc <+> text delimiter <+> bodyDoc
    GeneratedRender ->
      group
        ( lhsDoc
            <+> text delimiter
            <> hangingIndent 2 (line <> bodyDoc)
        )

-- Shared brace-and-semicolon (compact) / indented (generated) block layout for a
-- keyword followed by a list of item docs (@let@, @where@, @\\case@, @\\cases@, @do@).
renderBlock ::
  RenderDocument document =>
  RenderMode ->
  String ->
  [document] ->
  document
renderBlock renderMode keyword itemDocs =
  case renderMode of
    CompactRender ->
      text (keyword <> " { ") <> intercalateDoc (text "; ") itemDocs <> text " }"
    GeneratedRender ->
      vcat [text keyword, nest 2 (vcat itemDocs)]

wrapParen ::
  RenderDocument document =>
  Bool ->
  document ->
  document
wrapParen shouldWrap innerDoc =
  if shouldWrap then parenthesizeDoc innerDoc else innerDoc

hcat ::
  Monoid document =>
  [document] ->
  document
hcat =
  mconcat

intercalateDoc ::
  Monoid document =>
  document ->
  [document] ->
  document
intercalateDoc separatorDoc docValues =
  intercalateDocument separatorDoc docValues