packages feed

nix-lang-0.1.0.0: src/Nix/Lang/RFCPrint/LayoutHints.hs

-- | Layout preferences for 'Nix.Lang.RFCPrint', plus helpers for deriving them
-- from parsed source.

-- At the moment the preserved information is limited to container-shaped inline
-- versus multiline choices such as lists, sets, lets, and set patterns, plus a
-- narrow canonical trivia subset used by RFCPrint for selected parsed-source
-- comments and blank lines.
module Nix.Lang.RFCPrint.LayoutHints
  ( NodePath,
    LayoutHints,
    LayoutHint (..),
    ContainerLayout (..),
    CanonicalTrivia (..),
    emptyLayoutHints,
    singletonLayoutHint,
    lookupLayoutHint,
    childNodePath,
    collectLayoutHints,
    lowerParsedExpr,
    fromParsedExpr,
  )
where

import Data.Bifunctor (first)
import Data.List (nub)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import Nix.Lang.Annotation
import Nix.Lang.ExactPrint.Operations (exprSpan)
import Nix.Lang.Span (Located (..), SrcSpan, srcSpanEndLine, srcSpanStartLine)
import Nix.Lang.Types
import qualified Nix.Lang.Types.Ps as Parsed
import Nix.Lang.Types.Syn
import Nix.Lang.Utils (getLoc, unLoc)

-- | A structural path from the root expression to a nested child.
--
-- Each integer selects the next child in the formatter traversal.
type NodePath = [Int]

-- | A collection of layout preferences for particular nodes.
newtype LayoutHints = LayoutHints (Map NodePath LayoutHint)
  deriving (Eq, Show)

type HintMap = Map NodePath LayoutHint

type LowerResult a = (a, HintMap)

instance Semigroup LayoutHints where
  LayoutHints left <> LayoutHints right = LayoutHints (left <> right)

instance Monoid LayoutHints where
  mempty = emptyLayoutHints

-- | Layout preferences for a single node.
data LayoutHint = LayoutHint
  { -- | Preferred rendering strategy for container-like nodes.
    lhContainerLayout :: Maybe ContainerLayout,
    -- | Canonical trivia that should be emitted before the node.
    lhPriorTrivia :: [CanonicalTrivia],
    -- | Canonical trivia that should be emitted after the node.
    lhFollowingTrivia :: [CanonicalTrivia]
  }
  deriving (Eq, Show)

-- | Preferred shape for a list, set, let-body, or similar container.
data ContainerLayout
  = PreferInline
  | PreferMultiline
  deriving (Eq, Show)

-- | Canonicalized trivia carried from parsed source into RFCPrint.
data CanonicalTrivia
  = CanonicalLineComment Text
  | CanonicalBlockComment Text
  | CanonicalBlankLine
  deriving (Eq, Show)

-- | No layout preferences.
emptyLayoutHints :: LayoutHints
emptyLayoutHints = LayoutHints Map.empty

-- | A single hint at one 'NodePath'.
singletonLayoutHint :: NodePath -> LayoutHint -> LayoutHints
singletonLayoutHint path hint = LayoutHints (Map.singleton path hint)

-- | Look up the hint for a node path.
lookupLayoutHint :: NodePath -> LayoutHints -> Maybe LayoutHint
lookupLayoutHint key (LayoutHints hints) = Map.lookup key hints

-- | Extend a node path with one child index.
childNodePath :: NodePath -> Int -> NodePath
childNodePath path index = path <> [index]

-- | Extract layout hints from a parsed expression while discarding the lowered
-- expression tree.
collectLayoutHints :: Parsed.Expr -> LayoutHints
collectLayoutHints = snd . fromParsedExpr

-- | Lower a parsed expression to 'Expr' while discarding any extracted layout
-- hints.
lowerParsedExpr :: Parsed.Expr -> Expr
lowerParsedExpr = fst . fromParsedExpr

-- | Lower a parsed expression to formatter input and collect structural layout
-- hints keyed by 'NodePath'.
fromParsedExpr :: Parsed.Expr -> (Expr, LayoutHints)
fromParsedExpr expr = wrapHints (goExpr [] expr)

goExpr :: NodePath -> Parsed.Expr -> LowerResult Expr
goExpr path parsed = addExprHint path parsed $ case parsed of
  NixVar _ name -> pureResult (mkVar (unLoc name))
  NixLit _ lit -> pureResult (mkLit (lowerLit (unLoc lit)))
  NixPar _ inner -> first mkPar (goExpr (childNodePath path 0) (unLoc inner))
  NixString _ str -> first mkString (goString path (unLoc str))
  NixPath _ p -> first mkPath (goPath path (unLoc p))
  NixEnvPath _ p -> pureResult (mkEnvPath (unLoc p))
  NixLam _ pat body ->
    let (pat', patHints) = goFuncPat (childNodePath path 0) (unLoc pat)
        (body', bodyHints) = goExpr (childNodePath path 1) (unLoc body)
     in (mkLam pat' body', patHints <> bodyHints)
  NixApp _ f x ->
    let (f', fHints) = goExpr (childNodePath path 0) (unLoc f)
        (x', xHints) = goExpr (childNodePath path 1) (unLoc x)
     in (mkApp f' x', fHints <> xHints)
  NixBinApp _ op lhs rhs ->
    let (lhs', lhsHints) = goExpr (childNodePath path 0) (unLoc lhs)
        (rhs', rhsHints) = goExpr (childNodePath path 1) (unLoc rhs)
     in (mkBinApp op lhs' rhs', lhsHints <> rhsHints)
  NixNotApp _ inner -> first mkNot (goExpr (childNodePath path 0) (unLoc inner))
  NixNegApp _ inner -> first mkNeg (goExpr (childNodePath path 0) (unLoc inner))
  NixList _ xs -> first mkList (goLocated path goExpr xs)
  NixSet ann recFlag bindings ->
    let (bindings', hints) = goSetBindings path ann (unLoc bindings)
     in (case recFlag of NixSetRecursive -> mkRecSet bindings'; NixSetNonRecursive -> mkSet bindings', hints)
  NixLet _ bindings body ->
    let parsedBindings = unLoc bindings
        (bindings', bindingHints) = goLocated path goBinding parsedBindings
        (body', bodyHints) = goExpr (childNodePath path (length parsedBindings)) (unLoc body)
     in (mkLet bindings' body', bindingHints <> bodyHints)
  NixHasAttr _ inner attrPath ->
    let (inner', innerHints) = goExpr (childNodePath path 0) (unLoc inner)
        (attrPath', attrHints) = goAttrPath Nothing (childNodePath path 1) (unLoc attrPath)
     in (mkHasAttr inner' attrPath', innerHints <> attrHints)
  NixSelect _ inner attrPath mDefault ->
    let (inner', innerHints) = goExpr (childNodePath path 0) (unLoc inner)
        (attrPath', attrHints) = goAttrPath Nothing (childNodePath path 1) (unLoc attrPath)
        (mDefault', defaultHints) = maybe (Nothing, mempty) (first Just . goExpr (childNodePath path 2) . unLoc) mDefault
     in (maybe (mkSelect inner' attrPath') (mkSelectOr inner' attrPath') mDefault', innerHints <> attrHints <> defaultHints)
  NixIf _ c t f ->
    let (c', cHints) = goExpr (childNodePath path 0) (unLoc c)
        (t', tHints) = goExpr (childNodePath path 1) (unLoc t)
        (f', fHints) = goExpr (childNodePath path 2) (unLoc f)
     in (mkIf c' t' f', cHints <> tHints <> fHints)
  NixWith _ scope body ->
    let (scope', scopeHints) = goExpr (childNodePath path 0) (unLoc scope)
        (body', bodyHints) = goExpr (childNodePath path 1) (unLoc body)
     in (mkWith scope' body', scopeHints <> bodyHints)
  NixAssert _ assertion body ->
    let (assertion', assertionHints) = goExpr (childNodePath path 0) (unLoc assertion)
        (body', bodyHints) = goExpr (childNodePath path 1) (unLoc body)
     in (mkAssert assertion' body', assertionHints <> bodyHints)

goString :: NodePath -> Parsed.NString -> LowerResult NString
goString path = \case
  NixDoubleQuotesString _ parts -> first mkDoubleQuotedString (goLocated path goStringPart parts)
  NixDoubleSingleQuotesString _ parts -> first mkIndentedNString (goLocated path goStringPart parts)

goPath :: NodePath -> Parsed.Path -> LowerResult Path
goPath path = \case
  NixLiteralPath _ p -> pureResult (mkLiteralPath p)
  NixInterpolPath _ parts -> first mkInterpolatedPath (goLocated path goStringPart parts)

goStringPart :: NodePath -> NixStringPart Parsed.Ps -> LowerResult StringPart
goStringPart path = \case
  NixStringLiteral _ text -> pureResult (mkStringLiteral text)
  NixStringInterpol _ inner -> first mkInterpol (goExpr path (unLoc inner))

goAttrKey :: NodePath -> Parsed.AttrKey -> LowerResult AttrKey
goAttrKey path = \case
  NixStaticAttrKey _ name -> pureResult (mkAttr (unLoc name))
  NixDynamicStringAttrKey _ parts ->
    let (parts', hints) = goLocated path goStringPart parts
     in (case parts' of [NixStringLiteral _ text] -> mkQuotedAttr text; _ -> mkDynamicAttr parts', hints)
  NixDynamicInterpolAttrKey _ inner -> first mkInterpolatedAttr (goExpr path (unLoc inner))

goAttrPath :: Maybe SrcSpan -> NodePath -> Parsed.AttrPath -> LowerResult AttrPath
goAttrPath mBeforeSpan path attrPath = addAttrPathHint mBeforeSpan path attrPath $ case attrPath of
  NixAttrPath _ keys -> first mkAttrPath (goLocated path goAttrKey keys)

goBinding :: NodePath -> Parsed.Binding -> LowerResult Binding
goBinding = goBindingWithBefore Nothing

goBindingWithBefore :: Maybe SrcSpan -> NodePath -> Parsed.Binding -> LowerResult Binding
goBindingWithBefore mBeforeSpan path binding = addBindingHint path binding $ case binding of
  NixNormalBinding _ attrPath value ->
    let (attrPath', attrHints) = goAttrPath mBeforeSpan (childNodePath path 0) (unLoc attrPath)
        (value', valueHints) = goExpr (childNodePath path 1) (unLoc value)
     in (mkNormalBinding attrPath' value', attrHints <> valueHints)
  NixInheritBinding _ mScope keys ->
    let (mScope', scopeHints) = maybe (Nothing, mempty) (first Just . goExpr (childNodePath path 0) . stripParens . unLoc) mScope
        (keys', keyHints) = goLocated path goAttrKey keys
     in (maybe mkInheritKeys mkInheritFromKeys mScope' keys', scopeHints <> keyHints)

goSetBindings :: NodePath -> AnnSet -> [Located Parsed.Binding] -> LowerResult [Binding]
goSetBindings path ann bindings = (reverse reversedBindings, accHints)
  where
    openSpan = annTokenSrcSpan (asOpenC ann)
    (reversedBindings, _, accHints) =
      foldl' step ([], openSpan, mempty) (zip [0 :: Int ..] bindings)

    step (!bindingsSoFar, mPreviousAnchorSpan, !hints) (i, locatedBinding) =
      let (binding', bindingHints) = goBindingWithBefore mPreviousAnchorSpan (childNodePath path i) (unLoc locatedBinding)
        in (binding' : bindingsSoFar, Just (getLoc locatedBinding), hints <> bindingHints)

goFuncPat :: NodePath -> Parsed.FuncPat -> LowerResult FuncPat
goFuncPat path pat = addPatHint path pat $ case pat of
  NixVarPat _ name -> pureResult (mkVarPat (unLoc name))
  NixSetPat _ ellipses mAs bindings ->
    let mAs' = lowerSetPatAs . unLoc <$> mAs
        (bindings', bindingHints) = goLocated path goSetPatBinding bindings
     in (mkSetPat ellipses mAs' bindings', bindingHints)

goSetPatBinding :: NodePath -> Parsed.SetPatBinding -> LowerResult SetPatBinding
goSetPatBinding path NixSetPatBinding {..} =
  let (mDefault', hints) = maybe (Nothing, mempty) (first Just . goExpr (childNodePath path 0) . unLoc) nspbDefault
   in (mkSetPatBinding (unLoc nspbVar) mDefault', hints)

goLocated :: NodePath -> (NodePath -> a -> LowerResult b) -> [Located a] -> LowerResult [b]
goLocated path lowerAtPath = foldr step ([], mempty) . zip [0 :: Int ..]
  where
    step (i, located) (loweredItems, hints) =
      let (loweredItem, itemHints) = lowerAtPath (childNodePath path i) (unLoc located)
       in (loweredItem : loweredItems, itemHints <> hints)

addExprHint :: NodePath -> Parsed.Expr -> LowerResult Expr -> LowerResult Expr
addExprHint path parsedExpr = addHint path (exprLayout parsedExpr) (nodeCommentHint parsedExpr)

addPatHint :: NodePath -> Parsed.FuncPat -> LowerResult FuncPat -> LowerResult FuncPat
addPatHint path pat = addHint path (patLayout pat) (funcPatCommentHint pat)

addBindingHint :: NodePath -> Parsed.Binding -> LowerResult Binding -> LowerResult Binding
addBindingHint path binding = addHint path Nothing (bindingCommentHint binding)

addAttrPathHint :: Maybe SrcSpan -> NodePath -> Parsed.AttrPath -> LowerResult AttrPath -> LowerResult AttrPath
addAttrPathHint mBefore path attrPath = addHint path Nothing (attrPathCommentHint mBefore attrPath)

addHint :: NodePath -> Maybe ContainerLayout -> ([CanonicalTrivia], [CanonicalTrivia]) -> LowerResult a -> LowerResult a
addHint path mLayout (prior, following) (x, hints) =
  let maybeHint = case (mLayout, prior, following) of
        (Nothing, [], []) -> Nothing
        _ -> Just (LayoutHint mLayout prior following)
   in (x, maybe hints (\hint -> Map.insert path hint hints) maybeHint)

wrapHints :: LowerResult Expr -> (Expr, LayoutHints)
wrapHints (expr, hints) = (expr, LayoutHints hints)

commentTrivia :: [Located Comment] -> [CanonicalTrivia]
commentTrivia = fmap render
  where
    render (L _ comment) = case comment of
      LineComment txt -> CanonicalLineComment txt
      BlockComment txt -> CanonicalBlockComment txt

nodeCommentHint :: Parsed.Expr -> ([CanonicalTrivia], [CanonicalTrivia])
nodeCommentHint expr =
  case extractExprComments expr of
    NodeComments prior following ->
      (spacedTrivia Nothing prior (Just (exprSpan expr)), spacedTrivia (Just (exprSpan expr)) following Nothing)

funcPatCommentHint :: Parsed.FuncPat -> ([CanonicalTrivia], [CanonicalTrivia])
funcPatCommentHint pat =
  case extractFuncPatComments pat of
    NodeComments prior following ->
      let anchor = funcPatSpan pat
       in (spacedTrivia Nothing prior anchor, spacedTrivia anchor following Nothing)

noTrivia :: ([CanonicalTrivia], [CanonicalTrivia])
noTrivia = ([], [])

bindingCommentHint :: Parsed.Binding -> ([CanonicalTrivia], [CanonicalTrivia])
bindingCommentHint = \case
  NixNormalBinding {} -> noTrivia
  NixInheritBinding ann _ _ ->
    let NodeComments prior following = annComments ann
        anchor = annSrcSpan ann
     in (spacedTrivia Nothing prior anchor, spacedTrivia anchor following Nothing)

attrPathCommentHint :: Maybe SrcSpan -> Parsed.AttrPath -> ([CanonicalTrivia], [CanonicalTrivia])
attrPathCommentHint mBefore attrPath =
  case extractAttrPathComments attrPath of
    NodeComments prior following ->
      let anchor = attrPathSpan attrPath
       in (spacedTrivia mBefore prior anchor, spacedTrivia anchor following Nothing)

spacedTrivia :: Maybe SrcSpan -> [Located Comment] -> Maybe SrcSpan -> [CanonicalTrivia]
spacedTrivia mBefore comments mAfter =
  prependGap mBefore uniqueComments
    <> interleaveComments uniqueComments
    <> appendGap uniqueComments mAfter
  where
    uniqueComments = nub comments

    interleaveComments [] = []
    interleaveComments [comment] = commentTrivia [comment]
    interleaveComments (left@(L leftSpan _) : rest@(L rightSpan _ : _)) =
      commentTrivia [left]
        <> [CanonicalBlankLine | srcSpanStartLine rightSpan - srcSpanEndLine leftSpan >= 2]
        <> interleaveComments rest

    prependGap (Just before) (L firstSpan _ : _) | srcSpanStartLine firstSpan - srcSpanEndLine before >= 2 = [CanonicalBlankLine]
    prependGap _ _ = []

    appendGap [L lastSpan _] (Just after)
      | srcSpanStartLine after - srcSpanEndLine lastSpan >= 2 = [CanonicalBlankLine]
    appendGap [] (Just _) = []
    appendGap _ _ = []

funcPatSpan :: Parsed.FuncPat -> Maybe SrcSpan
funcPatSpan = \case
  NixVarPat ann _ -> annSrcSpan ann
  NixSetPat ann _ _ _ -> annSrcSpan ann

attrPathSpan :: Parsed.AttrPath -> Maybe SrcSpan
attrPathSpan = \case
  NixAttrPath ann _ -> annSrcSpan ann

extractExprComments :: Parsed.Expr -> NodeComments
extractExprComments = \case
  NixVar ann _ -> annComments ann
  NixLit ann _ -> annComments ann
  NixPar ann _ -> annComments ann
  NixString ann _ -> annComments ann
  NixPath ann _ -> annComments ann
  NixEnvPath ann _ -> annComments ann
  NixLam ann _ _ -> annComments ann
  NixApp ann _ _ -> annComments ann
  NixBinApp ann _ _ _ -> annComments ann
  NixNotApp ann _ -> annComments ann
  NixNegApp ann _ -> annComments ann
  NixList ann _ -> annComments ann
  NixSet ann _ _ -> annComments ann
  NixLet ann _ _ -> annComments ann
  NixHasAttr ann _ _ -> annComments ann
  NixSelect ann _ _ _ -> annComments ann
  NixIf ann _ _ _ -> annComments ann
  NixWith ann _ _ -> annComments ann
  NixAssert ann _ _ -> annComments ann

extractFuncPatComments :: Parsed.FuncPat -> NodeComments
extractFuncPatComments = \case
  NixVarPat ann _ -> annComments ann
  NixSetPat ann _ _ _ -> annComments ann

extractAttrPathComments :: Parsed.AttrPath -> NodeComments
extractAttrPathComments = \case
  NixAttrPath ann _ -> annComments ann

exprLayout :: Parsed.Expr -> Maybe ContainerLayout
exprLayout = \case
  NixList ann _ -> Just (tokenPairLayout (alnOpenS ann) (alnCloseS ann))
  NixSet ann _ bindings -> Just (if null (unLoc bindings) then PreferInline else tokenPairLayout (asOpenC ann) (asCloseC ann))
  NixLet ann bindings body -> Just (letLayout ann (unLoc bindings) (unLoc body))
  NixLam _ pat _ -> patLayout (unLoc pat)
  _ -> Nothing

patLayout :: Parsed.FuncPat -> Maybe ContainerLayout
patLayout = \case
  NixSetPat ann _ _ bindings -> Just (if null bindings then PreferInline else tokenPairLayout (aspOpenC ann) (aspCloseC ann))
  _ -> Nothing

tokenPairLayout :: AnnToken -> AnnToken -> ContainerLayout
tokenPairLayout left right =
  case (annTokenSrcSpan left, annTokenSrcSpan right) of
    (Just l, Just r) | srcSpanStartLine l == srcSpanStartLine r -> PreferInline
    _ -> PreferMultiline

letLayout :: AnnLetNode -> [Parsed.LBinding] -> Parsed.Expr -> ContainerLayout
letLayout ann bindings body =
  case (bindings, annTokenSrcSpan (alIn ann)) of
    ([], _) -> PreferInline
    (_, Just inSpan) | srcSpanStartLine inSpan == srcSpanStartLine (exprSpan body) -> PreferInline
    _ -> PreferMultiline

stripParens :: Parsed.Expr -> Parsed.Expr
stripParens (NixPar _ expr') = unLoc expr'
stripParens expr' = expr'

pureResult :: a -> LowerResult a
pureResult x = (x, mempty)

lowerSetPatAs :: Parsed.SetPatAs -> SetPatAs
lowerSetPatAs NixSetPatAs {..} = mkSetPatAs nspaLocation (unLoc nspaVar)

lowerLit :: Parsed.Lit -> Lit
lowerLit = \case
  NixUri _ uri -> mkUriLit uri
  NixInteger _ int -> mkIntegerLit int
  NixFloat _ float -> mkFloatLit float
  NixBoolean _ bool -> mkBooleanLit bool
  NixNull _ -> mkNullLit