nix-lang-0.1.0.0: src/Nix/Lang/ExactPrint/Prepare/Rebuild.hs
-- | Container rebuilding and layout normalization for exact-print preparation.
--
-- This is the part of the repair pipeline that puts lists, sets, and @let@
-- bindings back into shape after their children have moved.
module Nix.Lang.ExactPrint.Prepare.Rebuild
( BindingSequenceAnchor (..),
rebuildSetLayout,
rebuildSetLayoutWithAnchor,
rebuildLetLayout,
rebuildLetLayoutWithAnchor,
rebuildListLayout,
rebuildListLayoutWithAnchor,
normalizeBindingLayout,
normalizeAttrPathLayout,
normalizeExprLayout,
)
where
import Data.Data (Data)
import Nix.Lang.Annotation
import Nix.Lang.ExactPrint.Prepare.Reflow
import Nix.Lang.ExactPrint.Prepare.Types
import Nix.Lang.ExactPrint.Prepare.Utils
import Nix.Lang.ExactPrint.Operations
import Nix.Lang.Outputable (Outputable, renderToText)
import Nix.Lang.Span
import Nix.Lang.Types
import Nix.Lang.Types.Ps
import Nix.Lang.Utils
-- | Layout style inferred for set-like binding containers.
data SetLayoutStyle = SetInline | SetMultiline
-- | Layout style inferred for list containers.
data ListLayoutStyle = ListInline | ListMultiline
-- | Layout style inferred for the body following an @in@ token.
data BodyLayoutStyle = BodyInline | BodyMultiline
-- | How rebuild should choose the first anchor for a sequence.
data BindingSequenceAnchor = AnchorPreserveExisting | AnchorStartAtFirstSlot
-- | Rebuild a set from its current bindings while preserving exact-print metadata.
rebuildSetLayout :: AnnSet -> NixSetIsRecursive -> [LBinding] -> EPResult Expr
rebuildSetLayout ann kind bindings = rebuildSetLayoutWithAnchor AnchorPreserveExisting ann kind bindings
-- | Rebuild a set using an explicit anchor policy.
rebuildSetLayoutWithAnchor :: BindingSequenceAnchor -> AnnSet -> NixSetIsRecursive -> [LBinding] -> EPResult Expr
rebuildSetLayoutWithAnchor anchorMode ann kind bindings =
Right $ NixSet ann' kind (L bindingsSpan shiftedBindings)
where
openSpan = expectTokenSpan "set open brace" (asOpenC ann)
closeSpan = expectTokenSpan "set close brace" (asCloseC ann)
targetFile = srcSpanFilename openSpan
-- infer inline vs multiline style
style = inferSetLayoutStyle ann bindings
-- choose first element cursor
firstCursor = firstBindingCursor anchorMode style ann bindings
-- move elements left-to-right
shiftedBindings = rebuildBindingsLayout targetFile style firstCursor bindings
-- compute new } cursor
closeStart = closeCursor style closeSpan shiftedBindings
-- make span for }
newCloseSpan = tokenSpanAt closeStart (asCloseC ann)
-- shift comments attached to the old }
closeShiftedComments = shiftComments closeSpan newCloseSpan (followingComments (annComments ann))
annWithComments = setAnnCommon commonWithComments ann
annWithClose = annWithComments {asCloseC = closeToken}
-- rewrite } as delta
preparedAnn = prepareSetLayout annWithClose shiftedBindings
ann' = preparedAnn {asCloseC = closeToken}
closeToken = (asCloseC ann) {annTokenPos = AnnSpan newCloseSpan}
commonWithComments =
(getAnnCommon ann)
{ acComments =
(annComments ann)
{ followingComments = closeShiftedComments
},
acPos = AnnSpan setSpan
}
setSpan =
foldr combineSrcSpans base (bindingSpan . unLoc <$> shiftedBindings)
base = maybe openSpan (`combineSrcSpans` openSpan) (annTokenSrcSpan =<< asRec ann) `combineSrcSpans` newCloseSpan
bindingsSpan = listSpanOr base shiftedBindings
-- | Rebuild a @let ... in ...@ container from its current children.
rebuildLetLayout :: AnnLetNode -> [LBinding] -> LExpr -> EPResult Expr
rebuildLetLayout ann bindings body =
rebuildLetLayoutWithAnchor AnchorPreserveExisting ann bindings body
-- | Rebuild a @let ... in ...@ container using an explicit anchor policy.
--
-- This follows the same sequence algorithm as sets, but also computes a fresh
-- @in@ token position and then reanchors the body relative to that token.
rebuildLetLayoutWithAnchor :: BindingSequenceAnchor -> AnnLetNode -> [LBinding] -> LExpr -> EPResult Expr
rebuildLetLayoutWithAnchor anchorMode ann bindings body = Right $ NixLet finalAnn (L bindingsSpan shiftedBindings) shiftedBody
where
letSpan = expectTokenSpan "let keyword" (alLet ann)
oldInSpan = expectTokenSpan "in keyword" (alIn ann)
targetFile = srcSpanFilename letSpan
bindingStyle = inferLetBindingLayoutStyle ann bindings
bodyStyle = inferBodyLayoutStyle oldInSpan (getLoc body)
firstCursor = firstLetBindingCursor anchorMode bindingStyle ann bindings
shiftedBindings = rebuildBindingsLayout targetFile bindingStyle firstCursor bindings
inStart = letInCursor bindingStyle oldInSpan shiftedBindings
newInSpan = tokenSpanAt inStart (alIn ann)
shiftedBody = rebuildLetBodyLayout bodyStyle oldInSpan newInSpan body
inToken = (alIn ann) {annTokenPos = AnnSpan newInSpan}
annWithIn = ann {alIn = inToken}
preparedAnn = prepareLetLayout annWithIn shiftedBindings (unLoc shiftedBody)
base = letSpan `combineSrcSpans` newInSpan `combineSrcSpans` getLoc shiftedBody
letSpan' = foldr combineSrcSpans base (bindingSpan . unLoc <$> shiftedBindings)
annCommon' = (getAnnCommon preparedAnn) {acPos = AnnSpan letSpan'}
finalAnn = setAnnCommon annCommon' (preparedAnn {alIn = inToken})
bindingsSpan = listSpanOr (letSpan `combineSrcSpans` newInSpan) shiftedBindings
-- | Rebuild a list expression from its current elements.
rebuildListLayout :: AnnListNode -> [LExpr] -> EPResult Expr
rebuildListLayout ann xs = rebuildListLayoutWithAnchor AnchorPreserveExisting ann xs
-- | Rebuild a list expression using an explicit anchor policy.
rebuildListLayoutWithAnchor :: BindingSequenceAnchor -> AnnListNode -> [LExpr] -> EPResult Expr
rebuildListLayoutWithAnchor anchorMode ann xs =
Right $ NixList ann' shiftedElems
where
openSpan = expectTokenSpan "list open bracket" (alnOpenS ann)
closeSpan = expectTokenSpan "list close bracket" (alnCloseS ann)
targetFile = srcSpanFilename openSpan
-- infer list style
style = inferListLayoutStyle ann xs
-- choose first element cursor
firstCursor = firstListElementCursor anchorMode style ann xs
-- move the elements
shiftedElems = rebuildListElements targetFile style firstCursor xs
-- compute ] cursor
closeStart = listCloseCursor style closeSpan shiftedElems
-- make ] span
newCloseSpan = tokenSpanAt closeStart (alnCloseS ann)
-- shift comments
closeShiftedComments = shiftComments closeSpan newCloseSpan (followingComments (annComments ann))
annWithComments = setAnnCommon commonWithComments ann
annWithClose = annWithComments {alnCloseS = closeToken}
-- rewrite ]
ann' = prepareListLayout annWithClose shiftedElems
closeToken = (alnCloseS ann) {annTokenPos = AnnSpan newCloseSpan}
base = openSpan `combineSrcSpans` newCloseSpan
listSpan' = foldr combineSrcSpans base (exprSpan . unLoc <$> shiftedElems)
commonWithComments =
(getAnnCommon ann)
{ acComments =
(annComments ann)
{ followingComments = closeShiftedComments
},
acPos = AnnSpan listSpan'
}
-- | Infer whether a @let@ binding list should stay inline or multiline.
inferLetBindingLayoutStyle :: AnnLetNode -> [LBinding] -> SetLayoutStyle
inferLetBindingLayoutStyle ann bindings =
-- if the first binding and the in keyword are on the same line as the let keyword, keep it inline
-- otherwise, make it multiline
case bindings of
firstBinding : _
| srcSpanStartLine (getLoc firstBinding) == srcSpanStartLine letSpan
&& srcSpanStartLine inSpan == srcSpanStartLine letSpan ->
SetInline
[]
| srcSpanStartLine inSpan == srcSpanStartLine letSpan -> SetInline
_ -> SetMultiline
where
letSpan = expectTokenSpan "let keyword" (alLet ann)
inSpan = expectTokenSpan "in keyword" (alIn ann)
-- | Choose the first binding cursor for a repaired @let@.
--
-- When preserving existing anchors, this prefers the old first binding span if
-- it is still compatible with the inferred layout style.
firstLetBindingCursor :: BindingSequenceAnchor -> SetLayoutStyle -> AnnLetNode -> [LBinding] -> RenderCursor
firstLetBindingCursor anchorMode style ann bindings =
case (style, bindings) of
(SetInline, firstBinding : _)
| preserveExisting
&& srcSpanStartLine (getLoc firstBinding) == srcSpanEndLine letSpan
&& srcSpanStartColumn (getLoc firstBinding) > srcSpanEndColumn letSpan ->
cursorAtSpanStart (getLoc firstBinding)
| otherwise -> inlineInsertCursor letSpan inSpan
(SetInline, []) -> inlineInsertCursor letSpan inSpan
(SetMultiline, firstBinding : _)
| preserveExisting -> cursorAtSpanStart (getLoc firstBinding)
| otherwise -> RenderCursor (srcSpanStartLine letSpan + 1) (srcSpanStartColumn letSpan + 2)
(SetMultiline, []) -> RenderCursor (srcSpanStartLine letSpan + 1) (srcSpanStartColumn letSpan + 2)
where
letSpan = expectTokenSpan "let keyword" (alLet ann)
inSpan = expectTokenSpan "in keyword" (alIn ann)
preserveExisting = case anchorMode of
AnchorPreserveExisting -> True
AnchorStartAtFirstSlot -> False
-- | Compute where the @in@ token should be placed after repairing bindings.
-- For inline let, in goes after the last binding plus a space.
-- For multiline let, in goes on a new line, indented to the same level as the old in.
letInCursor :: SetLayoutStyle -> SrcSpan -> [LBinding] -> RenderCursor
letInCursor style oldIn bindings =
case (style, reverse bindings) of
(_, []) -> cursorAtSpanStart oldIn
(SetInline, lastBinding : _) ->
advanceCursor (cursorAtSpanStart (getLoc lastBinding)) (renderToText (unLoc lastBinding) <> " ")
(SetMultiline, lastBinding : _) ->
let endCursor = advanceCursor (cursorAtSpanStart (getLoc lastBinding)) (renderToText (unLoc lastBinding))
in RenderCursor (rcLine endCursor + 1) (srcSpanStartColumn oldIn)
-- | Infer whether the body after @in@ should stay inline or move to a new line.
inferBodyLayoutStyle :: SrcSpan -> SrcSpan -> BodyLayoutStyle
inferBodyLayoutStyle anchor bodySpan
| srcSpanStartLine bodySpan == srcSpanEndLine anchor = BodyInline
| otherwise = BodyMultiline
-- | Reanchor the body of a @let@ relative to the repaired @in@ token.
rebuildLetBodyLayout :: BodyLayoutStyle -> SrcSpan -> SrcSpan -> LExpr -> LExpr
rebuildLetBodyLayout style oldInSpan newInSpan body =
normalizeExprLayout (translateFromTo (getLoc body) targetSpan body)
where
-- For inline body, place body right after in.
-- For multiline body, preserve the body's old relative gap from in.
targetSpan = case style of
BodyInline ->
mkSrcSpan
(srcSpanFilename newInSpan)
(srcSpanEndLine newInSpan, srcSpanEndColumn newInSpan + 1)
(srcSpanEndLine newInSpan, srcSpanEndColumn newInSpan + 2)
BodyMultiline ->
let bodyDelta = deltaFromAnchor oldInSpan (getLoc body)
in applyDeltaToAnchor newInSpan bodyDelta
-- | Infer whether a list should stay inline or multiline.
inferListLayoutStyle :: AnnListNode -> [LExpr] -> ListLayoutStyle
inferListLayoutStyle ann xs =
-- if the first element and ] are on ['s line, keep it inline
-- otherwise, make it multiline
case xs of
firstElem : _
| srcSpanStartLine (getLoc firstElem) == srcSpanStartLine openSpan
&& srcSpanStartLine closeSpan == srcSpanStartLine openSpan ->
ListInline
[]
| srcSpanStartLine closeSpan == srcSpanStartLine openSpan -> ListInline
_ -> ListMultiline
where
openSpan = expectTokenSpan "list open bracket" (alnOpenS ann)
closeSpan = expectTokenSpan "list close bracket" (alnCloseS ann)
-- | Choose the first element cursor for a repaired list.
firstListElementCursor :: BindingSequenceAnchor -> ListLayoutStyle -> AnnListNode -> [LExpr] -> RenderCursor
firstListElementCursor anchorMode style ann xs =
case (style, xs) of
(ListInline, firstElem : _)
| preserveExisting
&& srcSpanStartLine (getLoc firstElem) == srcSpanEndLine openSpan
&& srcSpanStartColumn (getLoc firstElem) > srcSpanEndColumn openSpan ->
cursorAtSpanStart (getLoc firstElem)
| otherwise -> inlineInsertCursor openSpan closeSpan
(ListInline, []) -> inlineInsertCursor openSpan closeSpan
(ListMultiline, firstElem : _)
| preserveExisting -> cursorAtSpanStart (getLoc firstElem)
| otherwise -> RenderCursor (srcSpanStartLine openSpan + 1) (srcSpanStartColumn openSpan + 2)
(ListMultiline, []) -> RenderCursor (srcSpanStartLine openSpan + 1) (srcSpanStartColumn openSpan + 2)
where
openSpan = expectTokenSpan "list open bracket" (alnOpenS ann)
closeSpan = expectTokenSpan "list close bracket" (alnCloseS ann)
preserveExisting = preservesExistingAnchors anchorMode
-- | Reflow list elements from left to right.
rebuildListElements :: String -> ListLayoutStyle -> RenderCursor -> [LExpr] -> [LExpr]
rebuildListElements targetFile style startCursor = rebuildSequenceLayout (listFlow style) reanchorLocated normalizeExprLayout targetFile startCursor
-- | Compute where the closing bracket should be placed after repairing a list.
listCloseCursor :: ListLayoutStyle -> SrcSpan -> [LExpr] -> RenderCursor
listCloseCursor style oldClose xs = closeAfter (listFlow style) renderToText oldClose xs
-- | Infer whether a set should stay inline or multiline.
inferSetLayoutStyle :: AnnSet -> [LBinding] -> SetLayoutStyle
inferSetLayoutStyle ann bindings =
case bindings of
firstBinding : _
| srcSpanStartLine (getLoc firstBinding) == srcSpanStartLine openSpan
&& srcSpanStartLine closeSpan == srcSpanStartLine openSpan ->
SetInline
[]
| srcSpanStartLine closeSpan == srcSpanStartLine openSpan -> SetInline
_ -> SetMultiline
where
openSpan = expectTokenSpan "set open brace" (asOpenC ann)
closeSpan = expectTokenSpan "set close brace" (asCloseC ann)
-- | Choose the first binding cursor for a repaired set.
firstBindingCursor :: BindingSequenceAnchor -> SetLayoutStyle -> AnnSet -> [LBinding] -> RenderCursor
firstBindingCursor anchorMode style ann bindings =
case (style, bindings) of
(SetInline, firstBinding : _)
| preserveExisting
&& srcSpanStartLine (getLoc firstBinding) == srcSpanEndLine openSpan
&& srcSpanStartColumn (getLoc firstBinding) > srcSpanEndColumn openSpan ->
cursorAtSpanStart (getLoc firstBinding)
| otherwise -> inlineInsertCursor openSpan closeSpan
(SetInline, []) ->
inlineInsertCursor openSpan closeSpan
(SetMultiline, firstBinding : _)
| preserveExisting -> cursorAtSpanStart (getLoc firstBinding)
| otherwise -> RenderCursor (srcSpanStartLine openSpan + 1) (srcSpanStartColumn openSpan + 2)
(SetMultiline, []) ->
RenderCursor (srcSpanStartLine openSpan + 1) (srcSpanStartColumn openSpan + 2)
where
openSpan = expectTokenSpan "set open brace" (asOpenC ann)
closeSpan = expectTokenSpan "set close brace" (asCloseC ann)
preserveExisting = preservesExistingAnchors anchorMode
-- | Choose the inline insertion cursor between an opening and closing token.
inlineInsertCursor :: SrcSpan -> SrcSpan -> RenderCursor
inlineInsertCursor openSpan closeSpan =
let inlineGap = max 1 (srcSpanStartColumn closeSpan - srcSpanEndColumn openSpan)
in RenderCursor (srcSpanEndLine openSpan) (srcSpanEndColumn openSpan + inlineGap)
-- | Whether a rebuild should reuse old anchors where possible.
preservesExistingAnchors :: BindingSequenceAnchor -> Bool
preservesExistingAnchors = \case
AnchorPreserveExisting -> True
AnchorStartAtFirstSlot -> False
-- | Reflow bindings from left to right.
rebuildBindingsLayout :: String -> SetLayoutStyle -> RenderCursor -> [LBinding] -> [LBinding]
rebuildBindingsLayout targetFile style startCursor = rebuildSequenceLayout (setFlow style) reanchorLocated normalizeBindingLayout targetFile startCursor
-- | Compute where the closing brace should be placed after repairing bindings.
closeCursor :: SetLayoutStyle -> SrcSpan -> [LBinding] -> RenderCursor
closeCursor style oldClose bindings = closeAfter (setFlow style) renderToText oldClose bindings
reanchorLocated :: (Data a) => String -> RenderCursor -> Located a -> Located a
reanchorLocated targetFile target located@(L originalSpan _) =
translateFromTo originalSpan targetSpan located
where
targetSpan = mkSrcSpan targetFile (rcLine target, rcColumn target) (rcLine target, rcColumn target + 1)
-- | Convert set layout style into the generic left-to-right flow mode.
setFlow :: SetLayoutStyle -> Flow
setFlow = \case
SetInline -> FlowInline
SetMultiline -> FlowMultiline
-- | Convert list layout style into the generic left-to-right flow mode.
listFlow :: ListLayoutStyle -> Flow
listFlow = \case
ListInline -> FlowInline
ListMultiline -> FlowMultiline
-- | Rebuild a sequence of located elements with a given layout style.
-- Each element is reanchored to a new cursor, normalized, and used to compute the next cursor.
rebuildSequenceLayout ::
(Outputable a) =>
Flow ->
(String -> RenderCursor -> Located a -> Located a) ->
(Located a -> Located a) ->
String ->
RenderCursor ->
[Located a] ->
[Located a]
rebuildSequenceLayout flow reanchor normalize targetFile startCursor =
reflow flow renderToText (\cursor -> normalize . reanchor targetFile cursor) startCursor
-- | Normalize a binding subtree after it has been structurally moved.
normalizeBindingLayout :: LBinding -> LBinding
normalizeBindingLayout (L l binding) = L l $ case binding of
NixNormalBinding ann path expr ->
let path' = normalizeAttrPathLayout path
equalTok = case annTokenSrcSpan (anbEqual ann) of
Just eqSpan -> setAnnTokenDelta (deltaFromAnchor (attrPathSpan (unLoc path')) eqSpan) (anbEqual ann)
Nothing -> anbEqual ann
expr' = case annTokenSrcSpan (anbEqual ann) of
Just eqSpan ->
let targetExprSpan = mkSrcSpan (srcSpanFilename eqSpan) (srcSpanEndLine eqSpan, srcSpanEndColumn eqSpan + 1) (srcSpanEndLine eqSpan, srcSpanEndColumn eqSpan + 2)
in normalizeExprLayout (translateFromTo (getLoc expr) targetExprSpan expr)
Nothing -> normalizeExprLayout expr
semTok = case annTokenSrcSpan (anbSemicolon ann) of
Just _ -> setAnnTokenDelta (DeltaPos 0 0) (anbSemicolon ann)
Nothing -> anbSemicolon ann
in NixNormalBinding ann {anbEqual = equalTok, anbSemicolon = semTok} path' expr'
other -> other
-- | Normalize an attribute-path subtree after it has been structurally moved.
normalizeAttrPathLayout :: LAttrPath -> LAttrPath
normalizeAttrPathLayout (L l (NixAttrPath ann keys)) = L l $ NixAttrPath ann' keys
where
ann' = ann {aapDots = normalizeDots keys (aapDots ann)}
normalizeDots pathKeys dots = zipWith mkDot pathKeys (take (max 0 (length pathKeys - 1)) (dots <> repeat fallbackDot))
mkDot key tok = case annTokenSrcSpan tok of
Just dotSpan -> setAnnTokenDelta (deltaFromAnchor (getLoc key) dotSpan) tok
Nothing -> deltaAnnToken AnnDot (DeltaPos 0 0)
fallbackDot = deltaAnnToken AnnDot (DeltaPos 0 0)
-- | Normalize an expression subtree after it has been structurally moved.
--
-- This rewrites layout-sensitive token annotations relative to the repaired
-- child spans while preserving the existing AST shape.
normalizeExprLayout :: LExpr -> LExpr
normalizeExprLayout (L l expr) = L l $ case expr of
NixPar ann inner -> NixPar (prepareParLayout ann (unLoc inner')) inner'
where
inner' = normalizeExprLayout inner
NixList ann xs -> NixList (prepareListLayout ann xs') xs'
where
xs' = fmap normalizeExprLayout xs
NixSet ann kind (L bindingsLoc bindings) ->
let bindings' = fmap normalizeBindingLayout bindings
ann' = prepareSetLayout ann bindings'
in NixSet ann' kind (L bindingsLoc bindings')
NixLet ann (L bindingsLoc bindings) body ->
let bindings' = fmap normalizeBindingLayout bindings
body' = normalizeExprLayout body
ann' = prepareLetLayout ann bindings' (unLoc body')
in NixLet ann' (L bindingsLoc bindings') body'
NixHasAttr ann lhs path ->
let lhs' = normalizeExprLayout lhs
path' = normalizeAttrPathLayout path
in NixHasAttr (prepareHasAttrLayout ann (unLoc lhs') (unLoc path')) lhs' path'
NixSelect ann lhs path def ->
let lhs' = normalizeExprLayout lhs
path' = normalizeAttrPathLayout path
def' = fmap normalizeExprLayout def
in NixSelect (prepareSelectLayout ann (unLoc lhs') (unLoc path') def') lhs' path' def'
NixIf ann cond thenExpr elseExpr ->
let cond' = normalizeExprLayout cond
then' = normalizeExprLayout thenExpr
else' = normalizeExprLayout elseExpr
in NixIf (prepareIfLayout ann (unLoc cond') (unLoc then') (unLoc else')) cond' then' else'
NixWith ann scope body ->
let scope' = normalizeExprLayout scope
body' = normalizeExprLayout body
in NixWith (prepareWithLayout ann (unLoc scope') (unLoc body')) scope' body'
NixAssert ann assertion body ->
let assertion' = normalizeExprLayout assertion
body' = normalizeExprLayout body
in NixAssert (prepareAssertLayout ann (unLoc assertion') (unLoc body')) assertion' body'
other -> other