nix-lang-0.1.0.0: src/Nix/Lang/ExactPrint/Prepare/Utils.hs
{-# LANGUAGE RankNTypes #-}
-- | Shared span, cursor, and translation helpers for exact-print preparation.
module Nix.Lang.ExactPrint.Prepare.Utils
( bindingSpan,
bindingRenderSpan,
bindingComments,
staticAttrKeyText,
staticAttrPathText,
renderDoubleQuotedSourceText,
renderIndentedStringSourceText,
shiftSpanRight,
shiftComments,
applyDeltaToAnchor,
tokenSpanAt,
setAnnSpan,
preserveGapTarget,
preserveGapTargetForSetPat,
preserveGapSpan,
spanStartCursor,
spanEndCursor,
cursorSpan,
textSpanAt,
cursorFallbackFile,
cursorFile,
lastMay,
listSpan,
listSpanOr,
translateFromTo,
)
where
import Data.Data (Data)
import Data.Generics (everywhere, mkT)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Nix.Lang.Annotation
import Nix.Lang.ExactPrint.Operations
import Nix.Lang.Span
import Nix.Lang.Types
import Nix.Lang.Types.Ps
import Nix.Lang.Utils
--------------------------------------------------------------------------------
-- | Recover the full render span of a binding.
--
-- This prefers token-backed spans when they are available so rebuilt bindings
-- can preserve delimiter ownership precisely.
bindingSpan :: Binding -> SrcSpan
bindingSpan = \case
NixNormalBinding ann path expr ->
fromMaybe fallback $ do
equalSpan <- annTokenSrcSpan (anbEqual ann)
semicolonSpan <- annTokenSrcSpan (anbSemicolon ann)
pure $ attrPathSpan (unLoc path) `combineSrcSpans` equalSpan `combineSrcSpans` getLoc expr `combineSrcSpans` semicolonSpan
where
fallback = fromMaybe (attrPathSpan (unLoc path) `combineSrcSpans` getLoc expr) (annSrcSpan ann)
NixInheritBinding ann mScope names ->
fromMaybe fallback $ do
inheritSpan <- annTokenSrcSpan (aibInherit ann)
semicolonSpan <- annTokenSrcSpan (aibSemicolon ann)
let base = inheritSpan `combineSrcSpans` semicolonSpan
pure $ foldr combineSrcSpans base (maybe [] ((: []) . getLoc) mScope <> fmap getLoc names)
where
fallback = fromMaybe (foldr combineSrcSpans (mkSrcSpan "<edited>" (1, 1) (1, 1)) (maybe [] ((: []) . getLoc) mScope <> fmap getLoc names)) (annSrcSpan ann)
-- | Extend 'bindingSpan' to include comments owned before the binding.
bindingRenderSpan :: Binding -> SrcSpan
bindingRenderSpan binding =
foldr combineSrcSpans (bindingSpan binding) (getLoc <$> priorComments (bindingComments binding))
-- | Get the comments owned by a binding node.
bindingComments :: Binding -> NodeComments
bindingComments = \case
NixNormalBinding ann _ _ -> annComments ann
NixInheritBinding ann _ _ -> annComments ann
-- | Get the concrete text of a static attribute key.
staticAttrKeyText :: AttrKey -> Maybe Text
staticAttrKeyText = \case
NixStaticAttrKey _ (L _ key) -> Just key
_ -> Nothing
-- | Get the concrete text segments of a fully static attribute path.
staticAttrPathText :: AttrPath -> Maybe [Text]
staticAttrPathText (NixAttrPath _ keys) = traverse (staticAttrKeyText . unLoc) keys
--------------------------------------------------------------------------------
-- | Move a list of comments from one span anchor to another.
--
-- Comment spans are translated structurally, preserving their relative offsets
-- from the owning token or delimiter.
shiftComments :: SrcSpan -> SrcSpan -> [Located Comment] -> [Located Comment]
shiftComments oldSpan newSpan = fmap (translateFromTo oldSpan newSpan)
-- | Shift a span horizontally on the same line by a fixed number of columns.
shiftSpanRight :: Int -> SrcSpan -> SrcSpan
shiftSpanRight delta span' =
mkSrcSpan
(srcSpanFilename span')
(srcSpanStartLine span', srcSpanStartColumn span' + delta)
(srcSpanEndLine span', srcSpanEndColumn span' + delta)
-- | Render the exact parsed source of a double-quoted string.
renderDoubleQuotedSourceText :: SourceText -> Text
renderDoubleQuotedSourceText (SourceText src) = "\"" <> src <> "\""
-- | Render the exact parsed source of an indented string.
renderIndentedStringSourceText :: SourceText -> Text
renderIndentedStringSourceText (SourceText src) = "''" <> src <> "''"
--------------------------------------------------------------------------------
-- | Apply a relative delta to the end of an anchor span.
--
-- This is the inverse of storing token positions as deltas: once a new anchor is
-- known, the target span can be reconstructed deterministically from the delta.
applyDeltaToAnchor :: SrcSpan -> DeltaPos -> SrcSpan
applyDeltaToAnchor anchor delta =
case delta of
DeltaPos 0 col ->
mkSrcSpan
(srcSpanFilename anchor)
(srcSpanEndLine anchor, srcSpanEndColumn anchor + col)
(srcSpanEndLine anchor, srcSpanEndColumn anchor + col + 1)
DeltaPos line col ->
mkSrcSpan
(srcSpanFilename anchor)
(srcSpanEndLine anchor + line, col + 1)
(srcSpanEndLine anchor + line, col + 2)
--------------------------------------------------------------------------------
-- | Place a token at a concrete render cursor.
--
-- The token width comes from the concrete token text so synthesized spans match
-- the way exact printing will emit the token.
tokenSpanAt :: RenderCursor -> AnnToken -> SrcSpan
tokenSpanAt cursor tok =
mkSrcSpan fileName (rcLine cursor, rcColumn cursor) (rcLine cursor, rcColumn cursor + tokenWidth tok)
where
fileName = maybe "<edited>" srcSpanFilename (annTokenSrcSpan tok)
--------------------------------------------------------------------------------
-- | Preserve the relative gap from an old anchor to an old target at a new anchor.
--
-- This is the core cursor-preservation primitive used throughout repair: it says
-- "where should the new target start if it should keep the same relative gap it
-- had from the old anchor?".
preserveGapTarget :: SrcSpan -> SrcSpan -> SrcSpan -> RenderCursor
preserveGapTarget oldAnchor oldTarget newAnchor = spanStartCursor (applyDeltaToAnchor newAnchor (deltaFromAnchor oldAnchor oldTarget))
-- | Preserve the default-expression gap for a set-pattern binding.
preserveGapTargetForSetPat :: SetPatBinding -> LBinderName -> LExpr -> RenderCursor
preserveGapTargetForSetPat binding var' oldDef =
case aspbQuestion (nspbAnn binding) of
Just qTok -> preserveGapTarget (expectTokenSpan "set pattern question" qTok) (getLoc oldDef) (preserveGapSpan (getLoc (nspbVar binding)) (expectTokenSpan "set pattern question" qTok) var')
Nothing -> spanStartCursor (getLoc oldDef)
-- | Preserve the relative placement of a target span at a new anchor node.
--
-- Unlike 'preserveGapTarget', this returns a span rather than a cursor and is
-- typically used for tokens whose concrete width is already known.
preserveGapSpan :: SrcSpan -> SrcSpan -> Located a -> SrcSpan
preserveGapSpan oldAnchor oldTarget newAnchor = applyDeltaToAnchor (getLoc newAnchor) (deltaFromAnchor oldAnchor oldTarget)
--------------------------------------------------------------------------------
-- | Convert a span start into a render cursor.
spanStartCursor :: SrcSpan -> RenderCursor
spanStartCursor = cursorAtSpanStart
-- | Convert a span end into a render cursor.
spanEndCursor :: SrcSpan -> RenderCursor
spanEndCursor span' = RenderCursor (srcSpanEndLine span') (srcSpanEndColumn span')
-- | Create a one-column span at a render cursor.
cursorSpan :: RenderCursor -> String -> SrcSpan
cursorSpan cursor file = mkSrcSpan file (rcLine cursor, rcColumn cursor) (rcLine cursor, rcColumn cursor + 1)
-- | Compute the concrete span occupied by some rendered text at a cursor.
textSpanAt :: String -> RenderCursor -> Text -> SrcSpan
textSpanAt file cursor txt =
let end = advanceCursor cursor txt
in mkSrcSpan file (rcLine cursor, rcColumn cursor) (rcLine end, rcColumn end)
-- | Fallback pseudo-file name used for synthesized leaf spans.
cursorFallbackFile :: String
cursorFallbackFile = "<edited>"
-- | Get the file associated with a located value.
cursorFile :: Located a -> String
cursorFile locd = srcSpanFilename (getLoc locd)
--------------------------------------------------------------------------------
-- | Safe @last@ as a 'Maybe'.
lastMay :: [a] -> Maybe a
lastMay [] = Nothing
lastMay xs = Just (last xs)
-- | Span covering an entire non-empty located list.
listSpan :: [Located a] -> SrcSpan
listSpan [] = mkSrcSpan "<edited>" (1, 1) (1, 1)
listSpan xs = foldr1 combineSrcSpans (getLoc <$> xs)
-- | 'listSpan' with a fallback for the empty case.
listSpanOr :: SrcSpan -> [Located a] -> SrcSpan
listSpanOr fallback [] = fallback
listSpanOr _ xs = listSpan xs
--------------------------------------------------------------------------------
-- | Translate every span in a tree from one anchor span to another.
--
-- This is the core structural move operation used by both tree-moving callers and
-- layout repair. The algorithm keeps every point's line/column offset relative
-- to the original anchor, then replays that offset from the target anchor.
translateFromTo :: (Data a) => SrcSpan -> SrcSpan -> a -> a
translateFromTo origin target = everywhere (mkT translate)
where
originLine = srcSpanStartLine origin
originColumn = srcSpanStartColumn origin
targetLine = srcSpanStartLine target
targetColumn = srcSpanStartColumn target
targetFile = srcSpanFilename target
translate :: SrcSpan -> SrcSpan
translate src =
mkSrcSpan
targetFile
(shiftPoint (srcSpanStartLine src) (srcSpanStartColumn src))
(shiftPoint (srcSpanEndLine src) (srcSpanEndColumn src))
shiftPoint line column =
( targetLine + (line - originLine),
targetColumn + (column - originColumn)
)
--------------------------------------------------------------------------------
tokenWidth :: AnnToken -> Int
tokenWidth tok = max 1 . T.length $ fromMaybe " " (showToken (annToken tok))