packages feed

nix-lang-0.1.0.0: src/Nix/Lang/ExactPrint/Operations.hs

-- | Low-level cursor, span, and token operations for exact printing.
--
-- This is the low-level geometry layer under the exact printer: cursor movement,
-- token spans, whitespace gaps, and token retagging after subtree repair.
--
-- It knows how to:
--
-- * render gaps between already-anchored spans,
-- * recover structural spans from annotated nodes,
-- * convert absolute token spans back into relative deltas,
-- * and retag layout-sensitive tokens after subtree repair.
module Nix.Lang.ExactPrint.Operations
  ( -- * Cursor and gap operations
    RenderCursor (..),
    cursorAtSpanStart,
    cursorAtTokenStart,
    advanceCursor,
    renderGapFromDeltaText,
    renderGapFromCursorToSpan,
    renderGapFromCursorToSpanText,

    -- * Span and token queries
    exprSpan,
    funcPatBodySpan,
    funcPatRenderSpan,
    attrPathRenderSpan,
    attrPathSpan,
    expectTokenSpan,
    aspaAtSpan,
    setPatAsRenderSpan,

    -- * Layout repair helpers
    prepareListLayout,
    prepareParLayout,
    prepareSetLayout,
    prepareLetLayout,
    prepareIfLayout,
    prepareWithLayout,
    prepareAssertLayout,
    prepareHasAttrLayout,
    prepareSelectLayout,
    deltaFromAnchor,
  )
where

import Control.Applicative ((<|>))
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Nix.Lang.Annotation
import Nix.Lang.Span
import Nix.Lang.Types
import Nix.Lang.Types.Ps
import Nix.Lang.Utils
import Prettyprinter (Doc, pretty)

--------------------------------------------------------------------------------

-- | Logical cursor used by exact-layout operations.
data RenderCursor = RenderCursor
  { rcLine :: !Int,
    rcColumn :: !Int
  }
  deriving (Show, Eq)

--------------------------------------------------------------------------------

-- | Cursor at the start of a span.
cursorAtSpanStart :: SrcSpan -> RenderCursor
cursorAtSpanStart src = RenderCursor (srcSpanStartLine src) (srcSpanStartColumn src)

-- | Cursor at the start of a token span.
cursorAtTokenStart :: SrcSpan -> RenderCursor
cursorAtTokenStart = cursorAtSpanStart

-- | Advance a render cursor through concrete output text.
advanceCursor :: RenderCursor -> Text -> RenderCursor
advanceCursor cursor = T.foldl' step cursor
  where
    step RenderCursor {..} ch = if ch == '\n' then RenderCursor (rcLine + 1) 1 else RenderCursor rcLine (rcColumn + 1)

--------------------------------------------------------------------------------

-- | Render the gap from a cursor to a span.
renderGapFromCursorToSpan :: RenderCursor -> SrcSpan -> Doc ann
renderGapFromCursorToSpan cursor span' = pretty (renderGapFromCursorToSpanText cursor span')

-- | Compute the textual gap from a cursor to a span.
renderGapFromCursorToSpanText :: RenderCursor -> SrcSpan -> Text
renderGapFromCursorToSpanText RenderCursor {..} next
  | rcLine == srcSpanStartLine next = T.replicate (max 0 (srcSpanStartColumn next - rcColumn)) " "
  | otherwise = T.replicate (max 1 (srcSpanStartLine next - rcLine)) "\n" <> T.replicate (max 0 (srcSpanStartColumn next - 1)) " "

-- | Render whitespace described by a relative token delta.
renderGapFromDeltaText :: DeltaPos -> Text
renderGapFromDeltaText DeltaPos {..}
  | deltaLine <= 0 = T.replicate deltaColumn " "
  | otherwise = T.replicate deltaLine "\n" <> T.replicate deltaColumn " "

--------------------------------------------------------------------------------

-- | Recover the concrete span of an expression.
--
-- This prefers token-backed spans where possible and falls back to annotation or
-- child spans when an exact token span is missing.
exprSpan :: Expr -> SrcSpan
exprSpan = \case
  NixVar _ ident -> getLoc ident
  NixLit _ lit -> getLoc lit
  NixPar ann expr -> fromMaybe (fallbackExprSpan (annSrcSpan ann) [getLoc expr]) $ do
    openSpan <- annTokenSrcSpan (apnOpenP ann)
    closeSpan <- annTokenSrcSpan (apnCloseP ann)
    pure (openSpan `combineSrcSpans` getLoc expr `combineSrcSpans` closeSpan)
  NixString _ str -> getLoc str
  NixPath _ path -> getLoc path
  NixEnvPath ann path -> fromMaybe (fallbackExprSpan (annSrcSpan ann) [getLoc path]) $ do
    openSpan <- annTokenSrcSpan (aenvOpen ann)
    closeSpan <- annTokenSrcSpan (aenvClose ann)
    pure (openSpan `combineSrcSpans` getLoc path `combineSrcSpans` closeSpan)
  NixLam ann pat body -> foldr combineSrcSpans (funcPatRenderSpan (unLoc pat) `combineSrcSpans` getLoc body) (getLoc <$> priorComments (annComments ann))
  NixApp _ lhs rhs -> getLoc lhs `combineSrcSpans` getLoc rhs
  NixBinApp _ _ lhs rhs -> getLoc lhs `combineSrcSpans` getLoc rhs
  NixNotApp ann expr -> maybe (fallbackExprSpan (annSrcSpan ann) [getLoc expr]) (`combineSrcSpans` getLoc expr) (annTokenSrcSpan (apfxToken ann))
  NixNegApp ann expr -> maybe (fallbackExprSpan (annSrcSpan ann) [getLoc expr]) (`combineSrcSpans` getLoc expr) (annTokenSrcSpan (apfxToken ann))
  NixList ann xs ->
    fromMaybe (fallbackExprSpan (annSrcSpan ann) (getLoc <$> xs)) $ do
      openSpan <- annTokenSrcSpan (alnOpenS ann)
      closeSpan <- annTokenSrcSpan (alnCloseS ann)
      pure (foldr combineSrcSpans (openSpan `combineSrcSpans` closeSpan) (getLoc <$> xs))
  NixSet ann _ bindings ->
    fromMaybe (fallbackExprSpan (annSrcSpan ann) [getLoc bindings]) $ do
      openSpan <- maybe (annTokenSrcSpan (asOpenC ann)) annTokenSrcSpan (asRec ann)
      closeSpan <- annTokenSrcSpan (asCloseC ann)
      pure (openSpan `combineSrcSpans` getLoc bindings `combineSrcSpans` closeSpan)
  NixLet ann bindings expr ->
    fromMaybe (fallbackExprSpan (annSrcSpan ann) [getLoc bindings, getLoc expr]) $ do
      letSpan <- annTokenSrcSpan (alLet ann)
      inSpan <- annTokenSrcSpan (alIn ann)
      pure (letSpan `combineSrcSpans` getLoc bindings `combineSrcSpans` inSpan `combineSrcSpans` getLoc expr)
  NixHasAttr ann expr path ->
    maybe (fallbackExprSpan (annSrcSpan ann) [getLoc expr, getLoc path]) (getLoc expr `combineSrcSpans`) (annTokenSrcSpan (ahaQuestion ann))
      `combineSrcSpans` getLoc path
  NixSelect _ expr path def -> maybe (getLoc expr `combineSrcSpans` getLoc path) ((getLoc expr `combineSrcSpans` getLoc path) `combineSrcSpans`) (getLoc <$> def)
  NixIf ann cond thenExpr elseExpr ->
    fromMaybe (fallbackExprSpan (annSrcSpan ann) [getLoc cond, getLoc thenExpr, getLoc elseExpr]) $ do
      ifSpan <- annTokenSrcSpan (aifIf ann)
      thenSpan <- annTokenSrcSpan (aifThen ann)
      elseSpan <- annTokenSrcSpan (aifElse ann)
      pure (ifSpan `combineSrcSpans` getLoc cond `combineSrcSpans` thenSpan `combineSrcSpans` getLoc thenExpr `combineSrcSpans` elseSpan `combineSrcSpans` getLoc elseExpr)
  NixWith ann scope expr ->
    fromMaybe (fallbackExprSpan (annSrcSpan ann) [getLoc scope, getLoc expr]) $ do
      withSpan <- annTokenSrcSpan (awWith ann)
      semiSpan <- annTokenSrcSpan (awSemicolon ann)
      pure (withSpan `combineSrcSpans` getLoc scope `combineSrcSpans` semiSpan `combineSrcSpans` getLoc expr)
  NixAssert ann assertion expr ->
    fromMaybe (fallbackExprSpan (annSrcSpan ann) [getLoc assertion, getLoc expr]) $ do
      assertSpan <- annTokenSrcSpan (aaAssert ann)
      semiSpan <- annTokenSrcSpan (aaSemicolon ann)
      pure (assertSpan `combineSrcSpans` getLoc assertion `combineSrcSpans` semiSpan `combineSrcSpans` getLoc expr)

-- | Fallback span computation for expressions when token spans are missing.
fallbackExprSpan :: Maybe SrcSpan -> [SrcSpan] -> SrcSpan
fallbackExprSpan maybeAnnSpan childSpans =
  case maybeAnnSpan of
    Just span' -> span'
    Nothing -> foldr1 combineSrcSpans childSpans

-- | Recover the concrete body span of a function pattern.
funcPatBodySpan :: FuncPat -> SrcSpan
funcPatBodySpan = \case
  NixVarPat _ ident -> getLoc ident
  NixSetPat ann _ mAs bindings ->
    let base = expectTokenSpan "set pattern open" (aspOpenC ann) `combineSrcSpans` expectTokenSpan "set pattern close" (aspCloseC ann)
        withAs = maybe base (combineSrcSpans base . getLoc) mAs
     in foldr (combineSrcSpans . getLoc) withAs bindings

-- | Extend 'funcPatBodySpan' to include leading comments owned by the pattern.
funcPatRenderSpan :: FuncPat -> SrcSpan
funcPatRenderSpan pat = foldr combineSrcSpans (funcPatBodySpan pat) (getLoc <$> priorComments (funcPatComments pat))

-- | Get the comment payload owned by a function pattern.
funcPatComments :: FuncPat -> NodeComments
funcPatComments = \case
  NixVarPat ann _ -> annComments ann
  NixSetPat ann _ _ _ -> annComments ann

-- | Extend 'attrPathSpan' to include leading comments owned by the path.
attrPathRenderSpan :: AttrPath -> SrcSpan
attrPathRenderSpan path@(NixAttrPath ann _) = foldr combineSrcSpans (attrPathSpan path) (getLoc <$> priorComments (annComments ann))

-- | Recover the concrete span of an attribute path.
--
-- Leading-dot forms are anchored from the first dot token when present; other
-- forms start at the first key.
attrPathSpan :: AttrPath -> SrcSpan
attrPathSpan (NixAttrPath ann keys) =
  foldr combineSrcSpans base (getLoc <$> keys)
  where
    base = case aapDots ann of
      dotTok : _ -> fromMaybe keyBase (annTokenSrcSpan dotTok)
      [] -> case keys of
        key : _ -> getLoc key
        [] -> error "attrPathSpan: empty attribute path"
    keyBase = case keys of
      key : _ -> getLoc key
      [] -> error "attrPathSpan: empty attribute path"

-- | Recover the render span of a set-pattern @as@ binding.
setPatAsRenderSpan :: AnnSetPatAs -> LBinderName -> NixSetPatAsLocation -> SrcSpan
setPatAsRenderSpan ann var loc =
  case loc of
    NixSetPatAsLeading -> getLoc var `combineSrcSpans` aspaAtSpan ann (getLoc var)
    NixSetPatAsTrailing -> aspaAtSpan ann (getLoc var) `combineSrcSpans` getLoc var

--------------------------------------------------------------------------------

-- | Read a token span or fail loudly when an invariant is broken.
--
-- Exact-print repair expects certain tokens to remain present. This helper makes
-- those assumptions explicit and gives failures a label.
expectTokenSpan :: Text -> AnnToken -> SrcSpan
expectTokenSpan label tok = fromMaybe (error (T.unpack label <> ": missing token span")) (annTokenSrcSpan tok)

-- | Recover the span of the @at@ token in a set-pattern @as@ binding.
aspaAtSpan :: AnnSetPatAs -> SrcSpan -> SrcSpan
aspaAtSpan ann anchor = fromMaybe anchor (annTokenSrcSpan (aspaAt ann))

--------------------------------------------------------------------------------

-- | Retag the close token of a list after its elements have been repaired.
prepareListLayout :: AnnListNode -> [LExpr] -> AnnListNode
prepareListLayout ann xs = ann {alnCloseS = closeToken}
  where
    -- if the lisst is empty, anchor ] to [
    -- otherwise anchor ] to the last element
    closeToken = rewriteToken (alnCloseS ann) closeDelta
    closeDelta closeSpan = case xs of
      [] -> case annTokenSrcSpan (alnOpenS ann) of
        Just openSpan -> deltaFromAnchor openSpan closeSpan
        Nothing -> DeltaPos 0 0
      _ -> deltaFromAnchor (getLoc (last xs)) closeSpan

--------------------------------------------------------------------------------

-- | Re-anchor a token relative to a concrete span.
anchorToken :: SrcSpan -> AnnToken -> AnnToken
anchorToken anchor tok = rewriteToken tok (deltaFromAnchor anchor)

-- | Re-anchor an optional token relative to a concrete span.
anchorMaybeToken :: SrcSpan -> Maybe AnnToken -> Maybe AnnToken
anchorMaybeToken anchor = fmap (anchorToken anchor)

-- | Rewrite a token by deriving a new delta from its current concrete span.
-- If the token has a concrete span, convert it to a delta; otherwise, leave it unchanged.
rewriteToken :: AnnToken -> (SrcSpan -> DeltaPos) -> AnnToken
rewriteToken tok mkDelta =
  case annTokenSrcSpan tok of
    Just span' -> setAnnTokenDelta (mkDelta span') tok
    Nothing -> tok

--------------------------------------------------------------------------------

-- | Retag the close token of a parenthesized expression.
prepareParLayout :: AnnParNode -> Expr -> AnnParNode
prepareParLayout ann expr = ann {apnCloseP = closeToken}
  where
    closeToken = rewriteToken (apnCloseP ann) $ \closeSpan ->
      if srcSpanEndLine (exprSpan expr) < srcSpanStartLine closeSpan
        then deltaFromAnchor (exprSpan expr) closeSpan
        else tokenDeltaOrZero (apnCloseP ann)

--------------------------------------------------------------------------------

-- | Retag the close token of a set after its bindings have been repaired.
prepareSetLayout :: AnnSet -> [LBinding] -> AnnSet
prepareSetLayout ann bindings = ann {asCloseC = closeToken}
  where
    closeToken = rewriteToken (asCloseC ann) $ \closeSpan ->
      let anchor = case bindings of
            [] -> maybe closeSpan id (annTokenSrcSpan =<< (asRec ann <|> Just (asOpenC ann)))
            _ -> getLoc (last bindings)
       in deltaFromAnchor anchor closeSpan

--------------------------------------------------------------------------------

-- | Retag the @in@ token of a @let@ after its bindings have been repaired.
prepareLetLayout :: AnnLetNode -> [LBinding] -> Expr -> AnnLetNode
prepareLetLayout ann bindings _ = ann {alIn = inTok}
  where
    -- if there are bindings, anchor in to the last binding
    -- otherwise anchor in to let
    inTok = rewriteToken (alIn ann) $ \inSpan ->
      let anchor = case bindings of
            [] -> maybe inSpan id (annTokenSrcSpan (alLet ann))
            _ -> getLoc (last bindings)
       in deltaFromAnchor anchor inSpan

--------------------------------------------------------------------------------

-- | Retag the @then@ and @else@ tokens after repairing an @if@ chain.
prepareIfLayout :: AnnIfNode -> Expr -> Expr -> Expr -> AnnIfNode
prepareIfLayout ann cond thenExpr _ = ann {aifThen = thenTok, aifElse = elseTok}
  where
    -- anchor then to the condition span
    thenTok = anchorToken (exprSpan cond) (aifThen ann)
    -- anchor else to the then expression span
    elseTok = anchorToken (exprSpan thenExpr) (aifElse ann)

--------------------------------------------------------------------------------

-- | Retag the semicolon in a @with@ expression after repairing its scope.
prepareWithLayout :: AnnWithNode -> Expr -> Expr -> AnnWithNode
prepareWithLayout ann scope _ = ann {awSemicolon = semTok}
  where
    -- anchor ; to the scope span
    semTok = anchorToken (exprSpan scope) (awSemicolon ann)

--------------------------------------------------------------------------------

-- | Retag the semicolon in an @assert@ expression after repairing its assertion.
prepareAssertLayout :: AnnAssertNode -> Expr -> Expr -> AnnAssertNode
prepareAssertLayout ann assertion _ = ann {aaSemicolon = semTok}
  where
    -- anchor ; to the assertion span
    semTok = anchorToken (exprSpan assertion) (aaSemicolon ann)

--------------------------------------------------------------------------------

-- | Retag the question-mark token in a @hasAttr@ expression.
prepareHasAttrLayout :: AnnHasAttr -> Expr -> AttrPath -> AnnHasAttr
prepareHasAttrLayout ann expr _ = ann {ahaQuestion = qTok}
  where
    -- anchor ? to the expression span
    qTok = anchorToken (exprSpan expr) (ahaQuestion ann)

--------------------------------------------------------------------------------

-- | Retag the optional @or@ token in a selection expression.
prepareSelectLayout :: AnnSelect -> Expr -> AttrPath -> Maybe LExpr -> AnnSelect
prepareSelectLayout ann _ path def = ann {aslOr = orTok}
  where
    -- if or is present, anchor it to the attribute path span
    orTok = case def of
      Just _ -> anchorMaybeToken (attrPathSpan path) (aslOr ann)
      Nothing -> aslOr ann

--------------------------------------------------------------------------------

-- | Read a token's stored delta, defaulting to zero when it has no delta.
tokenDeltaOrZero :: AnnToken -> DeltaPos
tokenDeltaOrZero = fromMaybe (DeltaPos 0 0) . annTokenDelta

-- | Compute the relative delta from one anchor span to a target span.
deltaFromAnchor :: SrcSpan -> SrcSpan -> DeltaPos
deltaFromAnchor anchor target
  | srcSpanFilename anchor /= srcSpanFilename target = DeltaPos 0 0
  | srcSpanEndLine anchor == srcSpanStartLine target = DeltaPos 0 (max 0 (srcSpanStartColumn target - srcSpanEndColumn anchor))
  | otherwise = DeltaPos (max 1 (srcSpanStartLine target - srcSpanEndLine anchor)) (max 0 (srcSpanStartColumn target - 1))