packages feed

nix-lang (empty) → 0.1.0.0

raw patch · 66 files changed

+9948/−0 lines, 66 filesdep +basedep +containersdep +directory

Dependencies added: base, containers, directory, filepath, megaparsec, mtl, nix-lang, parser-combinators, prettyprinter, process, syb, tasty, tasty-hunit, template-haskell, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,8 @@+# Revision history for nix-lang++## 0.1.0.0 -- 2026-07-05++* First public release.+* Added a parser and exact printer for the Nix language.+* Added exact-print-preserving tree editing APIs.+* Added a fresh syntax tree together with an RFC 166-oriented formatter.
+ LICENSE view
@@ -0,0 +1,11 @@+Copyright (c) 2022-2026 berberman++Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:++   1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.++   2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.++   3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,43 @@+# nix-lang++`nix-lang` is a parser, exact printer, and formatter for the Nix language built around a [Trees that Grow](https://gitlab.haskell.org/ghc/ghc/-/wikis/implementing-trees-that-grow/trees-that-grow-guidance) style AST. Inspired by [`ghc-exactprint`](https://hackage.haskell.org/package/ghc-exactprint), it supports lossless round-tripping for parsed source while preserving comments, formatting, and multiline structure after modifying the syntax tree. It also provides a fresh syntax tree for generated Nix code together with an RFC 166-oriented formatter for canonical output.++`nix-lang-qq` provides quasiquoter for building fresh Nix expressions in Haskell file and supports embedding Haskell expressions.++```haskell+formatExpr [nixQQ| { value = 1; } |]+  -- => {+  --      value = 1; +  --    }+formatExpr [nixQQ| %%(foldl (\acc _ -> mkApp (mkVar "f") acc) (mkVar "x") [1..10 :: Int])]+  -- => f (f (f (f (f (f (f (f (f (f x)))))))))+```+++## Known unsupported syntax++Although it now parses the entire nixpkgs, some valid syntax is not supported.++* Legacy let++```nix+let { x = 233; body = x; }+```++* Floating number starting with a decimal point++```nix+.233+```++* Chained has-attribute expressions++```nix+a ? b ? c ? d+```++* Operator without white spaces++```nix+1+-1+```
+ nix-lang.cabal view
@@ -0,0 +1,136 @@+cabal-version:      3.0+name:               nix-lang+version:            0.1.0.0+synopsis:           Parser and printers for the Nix language+description:+  nix-lang provides a parser, exact printer, canonical RFC 166-oriented+  formatter, and editable Trees that Grow style AST for the Nix language.+  It supports lossless round-tripping for parsed syntax while also offering+  a fresh syntax tree and formatter for generated Nix expressions.++category:           Nix+license:            BSD-3-Clause+license-file:       LICENSE+author:             berberman+maintainer:         berberman <berberman@yandex.com>+copyright:          Copyright (c) berberman 2022-2026+stability:          alpha+homepage:           https://github.com/berberman/nix-lang+bug-reports:        https://github.com/berberman/nix-lang/issues+tested-with:        GHC ==9.10.3+extra-doc-files:+  CHANGELOG.md+  README.md++extra-source-files:+  test/fixtures/nixfmt/correct/*.nix+  test/fixtures/nixfmt/diff/comment/*.nix+  test/fixtures/nixfmt/diff/inherit_comment/*.nix+  test/fixtures/nixfmt/diff/leading_blank/*.nix+  test/fixtures/nixfmt/diff/string/*.nix+  test/fixtures/nixfmt/diff/string_interpol/*.nix+  test/fixtures/nixfmt/diff/strip_space/*.nix+  test/fixtures/nixfmt/invalid/*.nix+  test/sample.nix++common common-attrs+  build-depends:      base ^>=4.20+  default-language:   Haskell2010+  ghc-options:+    -Wall -Wcompat -Widentities -Wincomplete-uni-patterns+    -Wincomplete-record-updates++  if impl(ghc >=8.0)+    ghc-options: -Wredundant-constraints++  if impl(ghc >=8.2)+    ghc-options: -fhide-source-paths++  default-extensions:+    BangPatterns+    ConstraintKinds+    DataKinds+    DeriveAnyClass+    DeriveFunctor+    DeriveGeneric+    DerivingStrategies+    DerivingVia+    FlexibleContexts+    FlexibleInstances+    FunctionalDependencies+    KindSignatures+    LambdaCase+    NoStarIsType+    OverloadedStrings+    RecordWildCards+    ScopedTypeVariables+    TupleSections+    TypeApplications+    TypeFamilies+    TypeOperators+    ViewPatterns++library+  import:          common-attrs+  build-depends:+    , containers          ^>=0.7+    , megaparsec          ^>=9.7.0+    , mtl                 ^>=2.3.1+    , parser-combinators  ^>=1.3.1+    , prettyprinter       ^>=1.7.1+    , syb                 >=0.7.2   && <0.7.5+    , template-haskell    ^>=2.22.0+    , text                ^>=2.1.3++  exposed-modules:+    Nix.Lang.Annotation+    Nix.Lang.Edit+    Nix.Lang.ExactPrint+    Nix.Lang.ExactPrint.Operations+    Nix.Lang.ExactPrint.Prepare.Parse+    Nix.Lang.ExactPrint.Prepare.Rebuild+    Nix.Lang.ExactPrint.Prepare.Reflow+    Nix.Lang.ExactPrint.Prepare.Repair+    Nix.Lang.ExactPrint.Prepare.Types+    Nix.Lang.ExactPrint.Prepare.Utils+    Nix.Lang.Lexer.Text+    Nix.Lang.Outputable+    Nix.Lang.Parser+    Nix.Lang.QQ.Parsed+    Nix.Lang.RFCPrint+    Nix.Lang.RFCPrint.LayoutHints+    Nix.Lang.Span+    Nix.Lang.Types+    Nix.Lang.Types.Ps+    Nix.Lang.Types.Syn+    Nix.Lang.Utils++  other-modules:   Nix.Lang.Edit.Internal.TH+  hs-source-dirs:  src++test-suite nix-lang-test+  import:         common-attrs+  type:           exitcode-stdio-1.0+  build-depends:+    , directory    ^>=1.3.8+    , filepath     ^>=1.5.4+    , megaparsec+    , nix-lang+    , process+    , tasty+    , tasty-hunit+    , text++  other-modules:+    Edit+    ExactPrint+    Parser+    RFCPrint+    Utils++  hs-source-dirs: test+  main-is:        Spec.hs++source-repository head+  type:     git+  location: https://github.com/berberman/nix-lang
+ src/Nix/Lang/Annotation.hs view
@@ -0,0 +1,531 @@+{-# LANGUAGE DeriveDataTypeable #-}++-- | Annotation and exact-print support types for the parsed Nix AST.+--+-- These types record token spans, token deltas, and comment ownership for the+-- parsed pass. Exact printing and edit repair both work by preserving or+-- rebuilding this information.+module Nix.Lang.Annotation where++import Data.Data (Data)+import Data.Text (Text)+import Nix.Lang.Span++--------------------------------------------------------------------------------++data Ann+  = -- | @assert@+    AnnAssert+  | -- | @if@+    AnnIf+  | -- | @else@+    AnnElse+  | -- | @then@+    AnnThen+  | -- | @let@+    AnnLet+  | -- | @in@+    AnnIn+  | -- | @inherit@+    AnnInherit+  | -- | @rec@+    AnnRec+  | -- | @with@+    AnnWith+  | -- | @{@+    AnnOpenC+  | -- | @}@+    AnnCloseC+  | -- | @[@+    AnnOpenS+  | -- | @]@+    AnnCloseS+  | -- | @(@+    AnnOpenP+  | -- | @)@+    AnnCloseP+  | -- | @=@+    AnnAssign+  | -- | @@@+    AnnAt+  | -- | @:@+    AnnColon+  | -- | @,@+    AnnComma+  | -- | @.@+    AnnDot+  | -- | @...@+    AnnEllipsis+  | -- | @?@+    AnnQuestion+  | -- | @;@+    AnnSemicolon+  | -- | @++@+    AnnConcat+  | -- | @//@+    AnnUpdate+  | -- | @!@+    AnnEx+  | -- | @+@+    AnnAdd+  | -- | @-@+    AnnSub+  | -- | @*@+    AnnMul+  | -- | @/@+    AnnDiv+  | -- | @&&@+    AnnAnd+  | -- | @||@+    AnnOr+  | -- | @->@+    AnnImpl+  | -- | @==@+    AnnEqual+  | -- | @!=@+    AnnNEqual+  | -- | @>@+    AnnGT+  | -- | @>=@+    AnnGE+  | -- | @<@+    AnnLT+  | -- | @<=@+    AnnLE+  | -- | Identifier-like annotation marker.+    AnnId+  | -- | Value-like annotation marker.+    AnnVal+  | -- | @${@+    AnnInterpolOpen+  | -- | @}@ closing an interpolation.+    AnnInterpolClose+  | -- | @<@ opening an environment path.+    AnnEnvPathOpen+  | -- | @>@ closing an environment path.+    AnnEnvPathClose+  | -- | Unary negation token @-@.+    AnnNeg+  | -- | @''@+    AnnDoubleSingleQuotes+  | -- | @"@+    AnnDoubleQuote+  | -- | End-of-file marker.+    AnnEof+  deriving (Show, Eq, Enum, Data)++--------------------------------------------------------------------------------++-- | Source comment as parsed from the input stream.+data Comment+  = BlockComment Text+  | LineComment Text+  deriving (Show, Eq, Data)++-- | Comments owned by a node.+--+-- 'priorComments' are comments that conceptually appear before the node and+-- should be emitted before the node in an exact printer. 'followingComments'+-- are reserved for later transformations/moves where comments need to travel+-- with a node after it.+data NodeComments = NodeComments+  { priorComments :: [Located Comment],+    followingComments :: [Located Comment]+  }+  deriving (Show, Eq, Data)++-- | Empty comment ownership for nodes that do not currently carry comments.+emptyComments :: NodeComments+emptyComments = NodeComments [] []++--------------------------------------------------------------------------------++-- | Relative position used by exact-print-aware annotations.+--+-- 'DeltaPos' is the compact form used once a token or node no longer needs to+-- remember its original absolute span. It describes how far the next emitted+-- syntax element is from the current printing anchor.+data DeltaPos = DeltaPos+  { deltaLine :: Int,+    deltaColumn :: Int+  }+  deriving (Show, Eq, Data)++-- | Position model shared by node-local annotations.+--+-- Parsed trees initially use 'AnnSpan'. Later exact-print-oriented passes may+-- rewrite positions to 'AnnDelta' while preserving the same typed annotation+-- payloads and AST shape.+data AnnPos+  = AnnSpan SrcSpan+  | AnnDelta DeltaPos+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Exact-print metadata for a single concrete syntax token.+data AnnToken = AnnToken+  { annToken :: Ann,+    annTokenPos :: AnnPos+  }+  deriving (Show, Eq, Data)++-- | Construct a parser-produced token annotation backed by an absolute span.+parsedAnnToken :: Ann -> SrcSpan -> AnnToken+parsedAnnToken tok src = AnnToken tok (AnnSpan src)++-- | Construct a token annotation backed by a relative exact-print delta.+deltaAnnToken :: Ann -> DeltaPos -> AnnToken+deltaAnnToken tok delta = AnnToken tok (AnnDelta delta)++-- | Recover the absolute span when a token still carries parser-originated+-- location data.+annTokenSrcSpan :: AnnToken -> Maybe SrcSpan+annTokenSrcSpan AnnToken {annTokenPos = AnnSpan src} = Just src+annTokenSrcSpan _ = Nothing++-- | Recover the relative delta when a token has been normalized for exact+-- printing.+annTokenDelta :: AnnToken -> Maybe DeltaPos+annTokenDelta AnnToken {annTokenPos = AnnDelta delta} = Just delta+annTokenDelta _ = Nothing++setAnnTokenDelta :: DeltaPos -> AnnToken -> AnnToken+setAnnTokenDelta delta tok = tok {annTokenPos = AnnDelta delta}++--------------------------------------------------------------------------------++-- | Shared payload reused across many node-local annotation records.+data AnnCommon = AnnCommon+  { acComments :: NodeComments,+    acPos :: AnnPos+  }+  deriving (Show, Eq, Data)++class HasAnnCommon a where+  getAnnCommon :: a -> AnnCommon+  setAnnCommon :: AnnCommon -> a -> a++-- | Get the owned comments for a typed annotation payload.+annComments :: (HasAnnCommon a) => a -> NodeComments+annComments = acComments . getAnnCommon++-- | Get the common exact-print position for a typed annotation payload.+annPos :: (HasAnnCommon a) => a -> AnnPos+annPos = acPos . getAnnCommon++-- | Get the absolute source span when the common position is parser-produced.+annSrcSpan :: (HasAnnCommon a) => a -> Maybe SrcSpan+annSrcSpan ann = case annPos ann of+  AnnSpan l -> Just l+  AnnDelta _ -> Nothing++setAnnSpan :: (HasAnnCommon a) => SrcSpan -> a -> a+setAnnSpan span' ann = setAnnCommon ((getAnnCommon ann) {acPos = AnnSpan span'}) ann++--------------------------------------------------------------------------------++-- | Annotation payload for sets and recursive sets.+data AnnSet = AnnSet+  { asCommon :: AnnCommon,+    asRec :: Maybe AnnToken,+    asOpenC :: AnnToken,+    asCloseC :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for @let ... in ...@.+data AnnLetNode = AnnLetNode+  { alCommon :: AnnCommon,+    alLet :: AnnToken,+    alIn :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for @if ... then ... else ...@.+data AnnIfNode = AnnIfNode+  { aifCommon :: AnnCommon,+    aifIf :: AnnToken,+    aifThen :: AnnToken,+    aifElse :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for @with ...; ...@.+data AnnWithNode = AnnWithNode+  { awCommon :: AnnCommon,+    awWith :: AnnToken,+    awSemicolon :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for @assert ...; ...@.+data AnnAssertNode = AnnAssertNode+  { aaCommon :: AnnCommon,+    aaAssert :: AnnToken,+    aaSemicolon :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for selection expressions, including optional @or@.+data AnnSelect = AnnSelect+  { aslCommon :: AnnCommon,+    aslOr :: Maybe AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for @a ? b@.+data AnnHasAttr = AnnHasAttr+  { ahaCommon :: AnnCommon,+    ahaQuestion :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for normal bindings like @x = e;@.+data AnnNormalBinding = AnnNormalBinding+  { anbCommon :: AnnCommon,+    anbEqual :: AnnToken,+    anbSemicolon :: AnnToken+  }+  deriving (Show, Eq, Data)++-- | Annotation payload for @inherit ...;@ bindings.+data AnnInheritBinding = AnnInheritBinding+  { aibCommon :: AnnCommon,+    aibInherit :: AnnToken,+    aibSemicolon :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for variable patterns like @x:@.+data AnnVarPat = AnnVarPat+  { avpCommon :: AnnCommon,+    avpId :: SrcSpan+  }+  deriving (Show, Eq, Data)++-- | Annotation payload for set patterns.+data AnnSetPatNode = AnnSetPatNode+  { aspCommon :: AnnCommon,+    aspOpenC :: AnnToken,+    aspCloseC :: AnnToken,+    aspEllipsis :: Maybe AnnToken,+    aspCommas :: [AnnToken]+  }+  deriving (Show, Eq, Data)++data AnnSetPatAs = AnnSetPatAs+  { aspaCommon :: AnnCommon,+    aspaAt :: AnnToken+  }+  deriving (Show, Eq, Data)++data AnnSetPatBinding = AnnSetPatBinding+  { aspbCommon :: AnnCommon,+    aspbQuestion :: Maybe AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for dotted attribute paths.+data AnnAttrPath = AnnAttrPath+  { aapCommon :: AnnCommon,+    aapDots :: [AnnToken]+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for parenthesized expressions.+data AnnParNode = AnnParNode+  { apnCommon :: AnnCommon,+    apnOpenP :: AnnToken,+    apnCloseP :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for list expressions.+data AnnListNode = AnnListNode+  { alnCommon :: AnnCommon,+    alnOpenS :: AnnToken,+    alnCloseS :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for environment paths like @<nixpkgs>@.+data AnnEnvPathNode = AnnEnvPathNode+  { aenvCommon :: AnnCommon,+    aenvOpen :: AnnToken,+    aenvClose :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for string wrapper nodes.+newtype AnnStringNode = AnnStringNode+  { astrCommon :: AnnCommon+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for path wrapper nodes.+newtype AnnPathNode = AnnPathNode+  { apathCommon :: AnnCommon+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for lambda wrapper nodes.+data AnnLamNode = AnnLamNode+  { alamCommon :: AnnCommon,+    alamColon :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for application wrapper nodes.+data AnnAppNode = AnnAppNode+  { aappCommon :: AnnCommon+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for binary operator applications.+data AnnBinAppNode = AnnBinAppNode+  { abinCommon :: AnnCommon,+    abinOperator :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++-- | Annotation payload for prefix operator applications.+data AnnPrefixNode = AnnPrefixNode+  { apfxCommon :: AnnCommon,+    apfxToken :: AnnToken+  }+  deriving (Show, Eq, Data)++--------------------------------------------------------------------------------++instance HasAnnCommon AnnCommon where+  getAnnCommon = id+  setAnnCommon = const++instance HasAnnCommon AnnSet where+  getAnnCommon = asCommon+  setAnnCommon common ann = ann {asCommon = common}++instance HasAnnCommon AnnLetNode where+  getAnnCommon = alCommon+  setAnnCommon common ann = ann {alCommon = common}++instance HasAnnCommon AnnIfNode where+  getAnnCommon = aifCommon+  setAnnCommon common ann = ann {aifCommon = common}++instance HasAnnCommon AnnWithNode where+  getAnnCommon = awCommon+  setAnnCommon common ann = ann {awCommon = common}++instance HasAnnCommon AnnAssertNode where+  getAnnCommon = aaCommon+  setAnnCommon common ann = ann {aaCommon = common}++instance HasAnnCommon AnnSelect where+  getAnnCommon = aslCommon+  setAnnCommon common ann = ann {aslCommon = common}++instance HasAnnCommon AnnHasAttr where+  getAnnCommon = ahaCommon+  setAnnCommon common ann = ann {ahaCommon = common}++instance HasAnnCommon AnnNormalBinding where+  getAnnCommon = anbCommon+  setAnnCommon common ann = ann {anbCommon = common}++instance HasAnnCommon AnnInheritBinding where+  getAnnCommon = aibCommon+  setAnnCommon common ann = ann {aibCommon = common}++instance HasAnnCommon AnnVarPat where+  getAnnCommon = avpCommon+  setAnnCommon common ann = ann {avpCommon = common}++instance HasAnnCommon AnnSetPatNode where+  getAnnCommon = aspCommon+  setAnnCommon common ann = ann {aspCommon = common}++instance HasAnnCommon AnnSetPatAs where+  getAnnCommon = aspaCommon+  setAnnCommon common ann = ann {aspaCommon = common}++instance HasAnnCommon AnnSetPatBinding where+  getAnnCommon = aspbCommon+  setAnnCommon common ann = ann {aspbCommon = common}++instance HasAnnCommon AnnAttrPath where+  getAnnCommon = aapCommon+  setAnnCommon common ann = ann {aapCommon = common}++instance HasAnnCommon AnnParNode where+  getAnnCommon = apnCommon+  setAnnCommon common ann = ann {apnCommon = common}++instance HasAnnCommon AnnListNode where+  getAnnCommon = alnCommon+  setAnnCommon common ann = ann {alnCommon = common}++instance HasAnnCommon AnnEnvPathNode where+  getAnnCommon = aenvCommon+  setAnnCommon common ann = ann {aenvCommon = common}++instance HasAnnCommon AnnStringNode where+  getAnnCommon = astrCommon+  setAnnCommon common ann = ann {astrCommon = common}++instance HasAnnCommon AnnPathNode where+  getAnnCommon = apathCommon+  setAnnCommon common ann = ann {apathCommon = common}++instance HasAnnCommon AnnLamNode where+  getAnnCommon = alamCommon+  setAnnCommon common ann = ann {alamCommon = common}++instance HasAnnCommon AnnAppNode where+  getAnnCommon = aappCommon+  setAnnCommon common ann = ann {aappCommon = common}++instance HasAnnCommon AnnBinAppNode where+  getAnnCommon = abinCommon+  setAnnCommon common ann = ann {abinCommon = common}++instance HasAnnCommon AnnPrefixNode where+  getAnnCommon = apfxCommon+  setAnnCommon common ann = ann {apfxCommon = common}
+ src/Nix/Lang/Edit.hs view
@@ -0,0 +1,647 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TemplateHaskell #-}++-- | Structured tree editing for annotated Nix ASTs.+--+-- This is the high-level API for changing a parsed Nix tree without throwing+-- away its comments and layout.+--+-- The core model is:+--+-- * a 'Selector root focus' navigates from a root tree to one focused subtree,+-- * an 'Update focus' describes how to replace or modify that focused value,+-- * 'editExpr', 'editBinding', 'editAttrPath', and 'editFuncPat' rebuild the+--   outer tree and repair exact-print metadata after the change.+--+-- Selector composition with '(//)' builds a path through the tree. Some updates+-- work directly on AST values, such as 'replace' and 'modify'. Others, such as+-- 'replaceExprText' or 'insertBindingText', accept a Nix fragment and splice the+-- parsed result into the existing tree.+--+-- Most failures are either selection failures, update/prepare failures, or a+-- type-specific mismatch such as applying an integer-only update to a+-- non-integer literal.+--+-- A typical edit is “find this one thing, change it, leave the rest alone”.+-- For example, this updates one binding value while preserving the surrounding+-- comments and layout.+--+-- @+-- import Nix.Lang.Edit+-- import Nix.Lang.ExactPrint (renderExactText)+--+-- -- Given an annotated expression parsed from:+-- -- { # keep+-- --   version = "1.0";+-- --   name = "pkg";+-- -- }+-- --+-- -- expr :: Nix.Lang.Types.Ps.Expr+--+-- case editExpr expr (root // bindingByKey "version" // bindingValue) (replaceExprText "\"2.0\"") of+--   Right edited -> renderExactText edited+--   Left err -> error (show err)+-- @+--+-- If you only need rendering, not tree surgery, use 'Nix.Lang.ExactPrint' for+-- parsed trees or 'Nix.Lang.RFCPrint' for fresh syntax trees.+module Nix.Lang.Edit+  ( Selector,+    Update,+    EPT.BindingInsertPosition (..),+    EditError (..),+    SelectError (..),+    root,+    (//),+    letBody,+    lambdaPattern,+    lambdaBody,+    appFunction,+    appArgument,+    ifCondition,+    ifThen,+    ifElse,+    withScope,+    withBody,+    assertCondition,+    assertBody,+    selectExpr,+    selectPath,+    selectDefault,+    hasAttrExpr,+    hasAttrPath,+    bindingAt,+    bindingByKey,+    bindingByPath,+    listElement,+    attrKeyAt,+    bindingPath,+    bindingValue,+    inheritScope,+    inheritKeyAt,+    inheritKey,+    literal,+    replace,+    modify,+    modifyLocated,+    replaceExprText,+    replaceBindingText,+    replaceAttrKeyText,+    setIntLiteral,+    renameAttrKey,+    insertBinding,+    insertBindingText,+    insertInheritKeyAt,+    insertInheritKeyTextAt,+    editExpr,+    editBinding,+    editAttrPath,+    editFuncPat,+  )+where++import Data.Data (Data, toConstr)+import Data.Generics (showConstr)+import Data.Maybe (mapMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import Nix.Lang.Annotation+import Nix.Lang.Edit.Internal.TH+import qualified Nix.Lang.ExactPrint.Prepare.Parse as EPP+import Nix.Lang.ExactPrint.Prepare.Rebuild+import Nix.Lang.ExactPrint.Prepare.Repair+import qualified Nix.Lang.ExactPrint.Prepare.Types as EPT+import Nix.Lang.ExactPrint.Prepare.Utils+import Nix.Lang.ExactPrint.Operations+import Nix.Lang.Span+import Nix.Lang.Types+import Nix.Lang.Types.Ps+import Nix.Lang.Utils++--------------------------------------------------------------------------------++data SelectError+  = SelectorMismatch+      { selectorName :: Text,+        actualConstructor :: Text,+        selectionSpan :: SrcSpan+      }+  | OptionalAbsent+      { selectorName :: Text,+        selectionSpan :: SrcSpan+      }+  | IndexOutOfBounds+      { selectorName :: Text,+        index :: Int,+        itemCount :: Int,+        selectionSpan :: SrcSpan+      }+  | BindingNotFound+      { selectorName :: Text,+        bindingQuery :: [Text],+        selectionSpan :: SrcSpan+      }+  deriving (Show, Eq)++data EditError+  = SelectionError SelectError+  | UpdateError EPT.EPError+  | PrepareError EPT.EPError+  | ExpectedIntegerLiteral SrcSpan+  deriving (Show, Eq)+--------------------------------------------------------------------------------++data Edited a+  = Raw (Located a)+  | Ready (Located a)++data Selected parent child = Selected+  { selected :: Located child,+    replaceSelected :: Edited child -> Either EditError (Edited parent)+  }++data Selector parent child = Selector+  { runSelector :: Located parent -> Either SelectError (Selected parent child)+  }++newtype Update focus = Update+  { runUpdate :: Located focus -> Either EditError (Edited focus)+  }++--------------------------------------------------------------------------------++withSelectionError :: Either SelectError a -> Either EditError a+withSelectionError = mapLeft SelectionError++withUpdateError :: Either EPT.EPError a -> Either EditError a+withUpdateError = mapLeft UpdateError++withPrepareError :: Either EPT.EPError a -> Either EditError a+withPrepareError = mapLeft PrepareError++--------------------------------------------------------------------------------++mapLeft :: (e -> e') -> Either e a -> Either e' a+mapLeft f result =+  case result of+    Left err -> Left (f err)+    Right value -> Right value++--------------------------------------------------------------------------------++root :: Selector a a+root = Selector {runSelector = \x -> Right Selected {selected = x, replaceSelected = Right}}++--------------------------------------------------------------------------------++infixl 5 //++(//) :: Selector a b -> Selector b c -> Selector a c+Selector ab // Selector bc =+  Selector+    { runSelector = \a -> do+        Selected b putB <- ab a+        Selected c putC <- bc b+        pure+          Selected+            { selected = c,+              replaceSelected = \c' -> putC c' >>= putB+            }+    }++--------------------------------------------------------------------------------++replace :: Located focus -> Update focus+replace replacement = Update (Right . const (Raw replacement))++modify :: (focus -> focus) -> Update focus+modify f = modifyLocated (fmap f)++modifyLocated :: (Located focus -> Located focus) -> Update focus+modifyLocated f = Update (Right . Raw . f)+--------------------------------------------------------------------------------++replaceExprText :: Text -> Update Expr+replaceExprText source =+  Update $ \focused -> do+    replacement <- withUpdateError $ EPP.parseExpr source+    pure (Raw (translateFromTo (getLoc replacement) (getLoc focused) replacement))++replaceBindingText :: Text -> Update Binding+replaceBindingText source =+  Update $ \focused -> do+    replacement <- withUpdateError $ EPP.parseBinding source+    pure (Raw (translateFromTo (getLoc replacement) (getLoc focused) replacement))++replaceAttrKeyText :: Text -> Update AttrKey+replaceAttrKeyText source =+  Update $ \focused -> do+    replacement <- withUpdateError $ EPP.parseAttrKey source+    pure (Raw (translateFromTo (getLoc replacement) (getLoc focused) replacement))++setIntLiteral :: Integer -> Update Expr+setIntLiteral value = Update $ \focused ->+  case unLoc focused of+    NixLit ann (L litSpan (NixInteger _ _)) ->+      Right (Raw (L (getLoc focused) (NixLit ann (L litSpan (NixInteger NoExtF value)))))+    _ -> Left $ ExpectedIntegerLiteral (getLoc focused)++renameAttrKey :: Text -> Update AttrKey+renameAttrKey = replaceAttrKeyText++insertBinding :: EPT.BindingInsertPosition -> LBinding -> Update Expr+insertBinding position newBinding =+  Update $ \focused ->+    case focused of+      L _ (NixSet ann kind (L _ bindings)) -> do+        idx <- withUpdateError $ normalizeBindingInsertIndex position (length bindings)+        binding' <- withPrepareError $ prepareEditedRoot newBinding+        repaired <- withPrepareError $ rebuildSetLayoutWithAnchor (bindingInsertAnchor idx) ann kind (insertAt idx binding' bindings)+        pure (Ready (L (exprSpan repaired) repaired))+      L _ (NixLet ann (L _ bindings) body) -> do+        idx <- withUpdateError $ normalizeBindingInsertIndex position (length bindings)+        binding' <- withPrepareError $ prepareEditedRoot newBinding+        let insertedBindings = insertAt idx binding' bindings+            body' = prepareInsertedLetBody ann insertedBindings body+        repaired <- withPrepareError $ rebuildLetLayoutWithAnchor (bindingInsertAnchor idx) ann insertedBindings body'+        pure (Ready (L (exprSpan repaired) repaired))+      _ -> Left (UpdateError EPT.NotABindingContainer)++insertBindingText :: EPT.BindingInsertPosition -> Text -> Update Expr+insertBindingText position source =+  Update $ \focused -> do+    newBinding <- withUpdateError $ EPP.parseBinding source+    runUpdate (insertBinding position newBinding) focused++insertInheritKeyAt :: Int -> LAttrKey -> Update Binding+insertInheritKeyAt idx newKey =+  Update $ \focused ->+    case focused of+      L span' (NixInheritBinding ann scope keys) -> do+        _ <- withUpdateError $ normalizeBindingInsertIndex (EPT.InsertBindingAt idx) (length keys)+        let anchoredKey = translateFromTo (getLoc newKey) (inheritKeyInsertSpan ann scope keys idx) newKey+            shiftedTail = shiftAttrKeysRight (inheritKeyShiftColumns anchoredKey) (drop idx keys)+        Ready <$> withPrepareError (prepareEditedRoot (L span' (NixInheritBinding ann scope (take idx keys <> [anchoredKey] <> shiftedTail))))+      _ -> Left (UpdateError EPT.NotABindingContainer)++insertInheritKeyTextAt :: Int -> Text -> Update Binding+insertInheritKeyTextAt idx source =+  Update $ \focused -> do+    newKey <- withUpdateError $ EPP.parseAttrKey source+    runUpdate (insertInheritKeyAt idx newKey) focused+--------------------------------------------------------------------------------++class EditableRoot root where+  prepareEditedRoot :: Located root -> Either EPT.EPError (Located root)++instance EditableRoot Expr where+  prepareEditedRoot (L _ value) = do+    repaired <- repairExprLayout value+    pure (L (exprSpan repaired) repaired)++instance EditableRoot Binding where+  prepareEditedRoot (L _ value) = do+    repaired <- repairBindingLayout value+    pure (L (bindingSpan repaired) repaired)++instance EditableRoot AttrPath where+  prepareEditedRoot (L _ value) = do+    repaired <- repairAttrPathLayout value+    pure (L (attrPathSpan repaired) repaired)++instance EditableRoot FuncPat where+  prepareEditedRoot (L _ value) = do+    repaired <- repairFuncPatLayout value+    pure (L (funcPatBodySpan repaired) repaired)+--------------------------------------------------------------------------------++editWith :: (EditableRoot root) => Selector root focus -> Update focus -> Located root -> Either EditError (Located root)+editWith selector update input = do+  Selected focus rebuild <- withSelectionError $ runSelector selector input+  focus' <- runUpdate update focus+  rebuilt <- rebuild focus'+  case rebuilt of+    Ready root' -> Right root'+    Raw root' -> withPrepareError $ prepareEditedRoot root'+--------------------------------------------------------------------------------++prepareAttrPathNode :: Located AttrPath -> Either EditError (Located AttrPath)+prepareAttrPathNode = withPrepareError . prepareEditedRoot+--------------------------------------------------------------------------------++editedValue :: Edited a -> Located a+editedValue = \case+  Raw x -> x+  Ready x -> x++bindingDefinesPath :: [Text] -> Binding -> Bool+bindingDefinesPath path = \case+  NixNormalBinding _ attrPath _ -> staticAttrPathText (unLoc attrPath) == Just path+  NixInheritBinding _ _ keys ->+    case path of+      [key] -> key `elem` mapMaybe (staticAttrKeyText . unLoc) keys+      _ -> False++bindingDefinesKey :: Text -> Binding -> Bool+bindingDefinesKey key = bindingDefinesPath [key]++bindingChildren :: Located Expr -> Maybe [LBinding]+bindingChildren (L _ expr) =+  case expr of+    NixSet _ _ (L _ bindings) -> Just bindings+    NixLet _ (L _ bindings) _ -> Just bindings+    _ -> Nothing++rebuildBindingContainer :: [LBinding] -> Located Expr -> Either EditError (Edited Expr)+rebuildBindingContainer children (L _ expr) =+  case expr of+    NixSet ann kind _ -> do+      repaired <- withPrepareError $ rebuildSetLayout ann kind children+      pure (Ready (L (exprSpan repaired) repaired))+    NixLet ann _ body -> do+      repaired <- withPrepareError $ rebuildLetLayout ann children body+      pure (Ready (L (exprSpan repaired) repaired))+    _ -> expr `seq` error "impossible: binding rebuild on non-binding container"++findBindingIndex :: (Binding -> Bool) -> [LBinding] -> Maybe Int+findBindingIndex predicate = go 0+  where+    go _ [] = Nothing+    go idx (binding : rest)+      | predicate (unLoc binding) = Just idx+      | otherwise = go (idx + 1) rest++findInheritKeyIndex :: Text -> [LAttrKey] -> Maybe Int+findInheritKeyIndex key = go 0+  where+    go _ [] = Nothing+    go idx (attrKey : rest)+      | staticAttrKeyText (unLoc attrKey) == Just key = Just idx+      | otherwise = go (idx + 1) rest++inheritScopeReplacement :: LExpr -> LExpr -> LExpr+inheritScopeReplacement oldScope replacement =+  case unLoc oldScope of+    NixPar ann _ -> L (getLoc oldScope) (NixPar ann replacement)+    _ -> translateFromTo (getLoc replacement) (getLoc oldScope) replacement++inheritKeyInsertSpan :: AnnInheritBinding -> Maybe LExpr -> [LAttrKey] -> Int -> SrcSpan+inheritKeyInsertSpan ann scope keys idx =+  let anchor+        | idx > 0 = getLoc (keys !! (idx - 1))+        | otherwise = maybe (expectTokenSpan "inherit keyword" (aibInherit ann)) getLoc scope+   in mkSrcSpan (srcSpanFilename anchor) (srcSpanEndLine anchor, srcSpanEndColumn anchor + 1) (srcSpanEndLine anchor, srcSpanEndColumn anchor + 2)++inheritKeyShiftColumns :: LAttrKey -> Int+inheritKeyShiftColumns key = (srcSpanEndColumn span' - srcSpanStartColumn span') + 1+  where+    span' = getLoc key++shiftAttrKeysRight :: Int -> [LAttrKey] -> [LAttrKey]+shiftAttrKeysRight delta = fmap shiftOne+  where+    shiftOne key = translateFromTo (getLoc key) (shiftSpanRight delta (getLoc key)) key++prepareInsertedLetBody :: AnnLetNode -> [LBinding] -> LExpr -> LExpr+prepareInsertedLetBody ann bindings body+  | srcSpanStartLine span' > srcSpanEndLine oldInSpan = translateFromTo span' shifted body+  | otherwise = body+  where+    span' = getLoc body+    oldInSpan = expectTokenSpan "in keyword" (alIn ann)+    newInLine = case reverse bindings of+      lastBinding : _ -> srcSpanEndLine (getLoc lastBinding) + 1+      [] -> srcSpanStartLine oldInSpan+    shifted =+      mkSrcSpan+        (srcSpanFilename oldInSpan)+        (newInLine + 1, srcSpanStartColumn span')+        (newInLine + 1, srcSpanStartColumn span' + 1)++normalizeBindingInsertIndex :: EPT.BindingInsertPosition -> Int -> EPT.EPResult Int+normalizeBindingInsertIndex EPT.AppendBinding len = Right len+normalizeBindingInsertIndex (EPT.InsertBindingAt idx) len+  | idx < 0 = Left (EPT.NegativeIndex idx)+  | idx > len = Left (EPT.IndexOutOfRange idx len)+  | otherwise = Right idx++bindingInsertAnchor :: Int -> BindingSequenceAnchor+bindingInsertAnchor idx+  | idx == 0 = AnchorStartAtFirstSlot+  | otherwise = AnchorPreserveExisting++insertAt :: Int -> a -> [a] -> [a]+insertAt idx x xs = take idx xs <> [x] <> drop idx xs++editExpr :: Selector Expr focus -> Update focus -> LExpr -> Either EditError LExpr+editExpr = editWith++editBinding :: Selector Binding focus -> Update focus -> LBinding -> Either EditError LBinding+editBinding = editWith++editAttrPath :: Selector AttrPath focus -> Update focus -> LAttrPath -> Either EditError LAttrPath+editAttrPath = editWith++editFuncPat :: Selector FuncPat focus -> Update focus -> LFuncPat -> Either EditError LFuncPat+editFuncPat = editWith++mismatch :: (Data node) => Text -> Located node -> SelectError+mismatch name focused =+  SelectorMismatch+    { selectorName = name,+      actualConstructor = T.pack (showConstr (toConstr (unLoc focused))),+      selectionSpan = getLoc focused+    }++required :: (Data parent) => Text -> (Located parent -> Maybe (Selected parent child)) -> Selector parent child+required name f =+  Selector+    { runSelector = \parent ->+        case f parent of+          Just child -> Right child+          Nothing -> Left (mismatch name parent)+    }++optional :: (Data parent) => Text -> (Located parent -> Maybe (Maybe (Selected parent child))) -> Selector parent child+optional name f =+  Selector+    { runSelector = \parent ->+        case f parent of+          Just (Just child) -> Right child+          Just Nothing -> Left OptionalAbsent {selectorName = name, selectionSpan = getLoc parent}+          Nothing -> Left (mismatch name parent)+    }++indexed :: (Data parent, Data child) => Text -> Int -> (Located parent -> Maybe [Located child]) -> ([Located child] -> Located parent -> Either EditError (Edited parent)) -> Selector parent child+indexed name idx getChildren rebuildParent =+  Selector+    { runSelector = \parent ->+        case getChildren parent of+          Nothing -> Left (mismatch name parent)+          Just children+            | idx < 0 || idx >= length children ->+                Left+                  IndexOutOfBounds+                    { selectorName = name,+                      index = idx,+                      itemCount = length children,+                      selectionSpan = getLoc parent+                    }+            | otherwise ->+                let target = children !! idx+                 in Right+                      Selected+                        { selected = target,+                          replaceSelected = \replacement ->+                            let replacement' = translateFromTo (getLoc (editedValue replacement)) (getLoc target) (editedValue replacement)+                                updated = take idx children <> [replacement'] <> drop (idx + 1) children+                             in rebuildParent updated parent+                        }+    }++bindingMatching :: Text -> [Text] -> (Binding -> Bool) -> Selector Expr Binding+bindingMatching name query matches =+  Selector+    { runSelector = \parent ->+        case bindingChildren parent of+          Nothing -> Left (mismatch name parent)+          Just children ->+            case findBindingIndex matches children of+              Nothing -> Left BindingNotFound {selectorName = name, bindingQuery = query, selectionSpan = getLoc parent}+              Just idx ->+                let target = children !! idx+                 in Right+                      Selected+                        { selected = target,+                          replaceSelected = \replacement ->+                            let replacement' = translateFromTo (getLoc (editedValue replacement)) (getLoc target) (editedValue replacement)+                             in do+                                  replacement'' <- withPrepareError $ prepareEditedRoot replacement'+                                  let updated = take idx children <> [replacement''] <> drop (idx + 1) children+                                  rebuildBindingContainer updated parent+                        }+    }++$( makeRequiredSelectors+     [ RequiredSelectorSpec "letBody" "let body" [t|Expr|] [t|Expr|] 'NixLet 2,+       RequiredSelectorSpec "lambdaBody" "lambda body" [t|Expr|] [t|Expr|] 'NixLam 2,+       RequiredSelectorSpec "appFunction" "application function" [t|Expr|] [t|Expr|] 'NixApp 1,+       RequiredSelectorSpec "appArgument" "application argument" [t|Expr|] [t|Expr|] 'NixApp 2,+       RequiredSelectorSpec "ifCondition" "if condition" [t|Expr|] [t|Expr|] 'NixIf 1,+       RequiredSelectorSpec "ifThen" "if then" [t|Expr|] [t|Expr|] 'NixIf 2,+       RequiredSelectorSpec "ifElse" "if else" [t|Expr|] [t|Expr|] 'NixIf 3,+       RequiredSelectorSpec "withScope" "with scope" [t|Expr|] [t|Expr|] 'NixWith 1,+       RequiredSelectorSpec "withBody" "with body" [t|Expr|] [t|Expr|] 'NixWith 2,+       RequiredSelectorSpec "assertCondition" "assert condition" [t|Expr|] [t|Expr|] 'NixAssert 1,+       RequiredSelectorSpec "assertBody" "assert body" [t|Expr|] [t|Expr|] 'NixAssert 2,+       RequiredSelectorSpec "selectExpr" "select expression" [t|Expr|] [t|Expr|] 'NixSelect 1,+       RequiredSelectorSpec "hasAttrExpr" "has-attr expression" [t|Expr|] [t|Expr|] 'NixHasAttr 1+     ]+ )++$(makeRequiredSelectors [RequiredSelectorSpec "lambdaPattern" "lambda pattern" [t|Expr|] [t|FuncPat|] 'NixLam 1])++$( makeRequiredSelectors+     [ RequiredSelectorSpec "selectPath" "select path" [t|Expr|] [t|AttrPath|] 'NixSelect 2,+       RequiredSelectorSpec "hasAttrPath" "has-attr path" [t|Expr|] [t|AttrPath|] 'NixHasAttr 2+     ]+ )++$(makeOptionalSelectors [OptionalSelectorSpec "selectDefault" "select default" [t|Expr|] [t|Expr|] 'NixSelect 3])++$(makeRequiredSelectors [RequiredSelectorSpec "bindingPath" "binding path" [t|Binding|] [t|AttrPath|] 'NixNormalBinding 1])++$(makeRequiredSelectors [RequiredSelectorSpec "bindingValue" "binding value" [t|Binding|] [t|Expr|] 'NixNormalBinding 2])++$(makeRequiredSelectors [RequiredSelectorSpec "literal" "literal" [t|Expr|] [t|Lit|] 'NixLit 1])++bindingByKey :: Text -> Selector Expr Binding+bindingByKey key = bindingMatching "binding" [key] (bindingDefinesKey key)++bindingByPath :: [Text] -> Selector Expr Binding+bindingByPath path = bindingMatching "binding" path (bindingDefinesPath path)++bindingAt :: Int -> Selector Expr Binding+bindingAt idx =+  indexed "binding" idx bindingChildren rebuildBindingContainer++inheritScope :: Selector Binding Expr+inheritScope =+  optional "inherit scope" $ \binding ->+    case binding of+      L _ (NixInheritBinding _ Nothing _) -> Just Nothing+      L _ (NixInheritBinding ann (Just scope) keys) ->+        Just . Just $+          Selected+            { selected = scope,+              replaceSelected = \replacement ->+                let replacement' = inheritScopeReplacement scope (editedValue replacement)+                 in Ready <$> withPrepareError (prepareEditedRoot (L (getLoc binding) (NixInheritBinding ann (Just replacement') keys)))+            }+      _ -> Nothing++inheritKeyAt :: Int -> Selector Binding AttrKey+inheritKeyAt idx =+  indexed "inherit key" idx getChildren rebuildParent+  where+    getChildren (L _ (NixInheritBinding _ _ keys)) = Just keys+    getChildren _ = Nothing+    rebuildParent keys (L span' (NixInheritBinding ann scope _)) =+      Ready <$> withPrepareError (prepareEditedRoot (L span' (NixInheritBinding ann scope keys)))+    rebuildParent _ binding = binding `seq` error "impossible: inherit key rebuild on non-inherit binding"++inheritKey :: Text -> Selector Binding AttrKey+inheritKey key =+  Selector+    { runSelector = \binding ->+        case binding of+          L _ (NixInheritBinding ann scope keys) ->+            case findInheritKeyIndex key keys of+              Nothing -> Left BindingNotFound {selectorName = "inherit key", bindingQuery = [key], selectionSpan = getLoc binding}+              Just idx ->+                let target = keys !! idx+                 in Right+                      Selected+                        { selected = target,+                          replaceSelected = \replacement ->+                            let replacement' = translateFromTo (getLoc (editedValue replacement)) (getLoc target) (editedValue replacement)+                                updated = take idx keys <> [replacement'] <> drop (idx + 1) keys+                             in Ready <$> withPrepareError (prepareEditedRoot (L (getLoc binding) (NixInheritBinding ann scope updated)))+                        }+          _ -> Left (mismatch "inherit key" binding)+    }++listElement :: Int -> Selector Expr Expr+listElement idx =+  Selector+    { runSelector = \parent ->+        case parent of+          L _ (NixList ann children)+            | idx < 0 || idx >= length children ->+                Left+                  IndexOutOfBounds+                    { selectorName = "list element",+                      index = idx,+                      itemCount = length children,+                      selectionSpan = getLoc parent+                    }+            | otherwise ->+                let target = children !! idx+                 in Right+                      Selected+                        { selected = target,+                          replaceSelected = \replacement -> do+                            let replacement' = translateFromTo (getLoc (editedValue replacement)) (getLoc target) (editedValue replacement)+                            replacement'' <- withPrepareError $ prepareEditedRoot replacement'+                            repaired <- withPrepareError $ rebuildListLayoutWithAnchor AnchorStartAtFirstSlot ann (take idx children <> [replacement''] <> drop (idx + 1) children)+                            pure (Ready (L (exprSpan repaired) repaired))+                        }+          _ -> Left (mismatch "list element" parent)+    }++attrKeyAt :: Int -> Selector AttrPath AttrKey+attrKeyAt idx =+  indexed "attr key" idx getChildren rebuildParent+  where+    getChildren (L _ (NixAttrPath _ keys)) = Just keys+    rebuildParent keys (L span' (NixAttrPath ann _)) = Ready <$> prepareAttrPathNode (L span' (NixAttrPath ann keys))
+ src/Nix/Lang/Edit/Internal/TH.hs view
@@ -0,0 +1,185 @@+-- | Template Haskell used to generate the selector combinators exported by+-- 'Nix.Lang.Edit'.+module Nix.Lang.Edit.Internal.TH+  ( RequiredSelectorSpec (..),+    OptionalSelectorSpec (..),+    makeRequiredSelectors,+    makeOptionalSelectors,+  )+where++import Control.Applicative ((<|>))+import Language.Haskell.TH++data RequiredSelectorSpec = RequiredSelectorSpec+  { requiredSelectorName :: String,+    requiredSelectorLabel :: String,+    requiredParentType :: Q Type,+    requiredChildType :: Q Type,+    requiredConstructor :: Name,+    requiredFieldIndex :: Int+  }++data OptionalSelectorSpec = OptionalSelectorSpec+  { optionalSelectorName :: String,+    optionalSelectorLabel :: String,+    optionalParentType :: Q Type,+    optionalChildType :: Q Type,+    optionalConstructor :: Name,+    optionalFieldIndex :: Int+  }++makeRequiredSelectors :: [RequiredSelectorSpec] -> Q [Dec]+makeRequiredSelectors = fmap concat . traverse makeRequiredSelector++makeOptionalSelectors :: [OptionalSelectorSpec] -> Q [Dec]+makeOptionalSelectors = fmap concat . traverse makeOptionalSelector++makeRequiredSelector :: RequiredSelectorSpec -> Q [Dec]+makeRequiredSelector RequiredSelectorSpec {..} = do+  fieldCount <- constructorArity requiredConstructor+  parentTy <- requiredParentType+  childTy <- requiredChildType+  spanName <- newName "span'"+  nodeName <- newName "node"+  replacementName <- newName "replacement"+  fieldNames <- traverse (newName . ("field" <>) . show) [0 .. fieldCount - 1]+  let targetName = fieldNames !! requiredFieldIndex+      replacedFields = replaceAt requiredFieldIndex (AppE (VarE (mkName "editedValue")) (VarE replacementName)) (map VarE fieldNames)+      selectorBody =+        AppE+          (AppE (VarE (mkName "required")) (LitE (StringL requiredSelectorLabel)))+          ( LamE+              [ConP (mkName "L") [] [VarP spanName, VarP nodeName]]+              ( CaseE+                  (VarE nodeName)+                  [ Match+                      (ConP requiredConstructor [] (map VarP fieldNames))+                      ( NormalB+                          (AppE (ConE (mkName "Just")) (requiredSelected spanName targetName replacementName requiredConstructor replacedFields))+                      )+                      [],+                    Match WildP (NormalB (ConE (mkName "Nothing"))) []+                  ]+              )+          )+  sig <- sigD (mkName requiredSelectorName) (pure (AppT (AppT (ConT (mkName "Selector")) parentTy) childTy))+  val <- valD (varP (mkName requiredSelectorName)) (normalB (pure selectorBody)) []+  pure [sig, val]++makeOptionalSelector :: OptionalSelectorSpec -> Q [Dec]+makeOptionalSelector OptionalSelectorSpec {..} = do+  fieldCount <- constructorArity optionalConstructor+  parentTy <- optionalParentType+  childTy <- optionalChildType+  spanName <- newName "span'"+  nodeName <- newName "node"+  replacementName <- newName "replacement"+  innerName <- newName "selectedValue"+  fieldNames <- traverse (newName . ("field" <>) . show) [0 .. fieldCount - 1]+  let targetName = fieldNames !! optionalFieldIndex+      replacedFields = replaceAt optionalFieldIndex (AppE (ConE (mkName "Just")) (AppE (VarE (mkName "editedValue")) (VarE replacementName))) (map VarE fieldNames)+      mappedSelected =+        AppE+          (AppE (VarE (mkName "flip")) (VarE (mkName "fmap")))+          (VarE targetName)+          `AppE` LamE [VarP innerName] (optionalSelected spanName innerName replacementName optionalConstructor replacedFields)+      selectorBody =+        AppE+          (AppE (VarE (mkName "optional")) (LitE (StringL optionalSelectorLabel)))+          ( LamE+              [ConP (mkName "L") [] [VarP spanName, VarP nodeName]]+              ( CaseE+                  (VarE nodeName)+                  [ Match+                      (ConP optionalConstructor [] (map VarP fieldNames))+                      ( NormalB+                          (AppE (ConE (mkName "Just")) mappedSelected)+                      )+                      [],+                    Match WildP (NormalB (ConE (mkName "Nothing"))) []+                  ]+              )+          )+  sig <- sigD (mkName optionalSelectorName) (pure (AppT (AppT (ConT (mkName "Selector")) parentTy) childTy))+  val <- valD (varP (mkName optionalSelectorName)) (normalB (pure selectorBody)) []+  pure [sig, val]++requiredSelected :: Name -> Name -> Name -> Name -> [Exp] -> Exp+requiredSelected spanName targetName replacementName conName replacedFields =+  RecConE+    (mkName "Selected")+      [ (mkName "selected", VarE targetName),+        ( mkName "replaceSelected",+          LamE+            [VarP replacementName]+            ( AppE+                (ConE (mkName "Right"))+                ( AppE+                    (ConE (mkName "Ready"))+                    ( AppE+                        (AppE (ConE (mkName "L")) (VarE spanName))+                        (foldl AppE (ConE conName) replacedFields)+                    )+                )+            )+        )+      ]++optionalSelected :: Name -> Name -> Name -> Name -> [Exp] -> Exp+optionalSelected spanName innerName replacementName conName replacedFields =+  RecConE+    (mkName "Selected")+      [ (mkName "selected", VarE innerName),+        ( mkName "replaceSelected",+          LamE+            [VarP replacementName]+            ( AppE+                (ConE (mkName "Right"))+                ( AppE+                    (ConE (mkName "Ready"))+                    ( AppE+                        (AppE (ConE (mkName "L")) (VarE spanName))+                        (foldl AppE (ConE conName) replacedFields)+                    )+                )+            )+        )+      ]++constructorArity :: Name -> Q Int+constructorArity name = do+  info <- reify name+  case info of+    DataConI _ _ parentName -> do+      parentInfo <- reify parentName+      case parentInfo of+        TyConI dec ->+          case findConstructor name dec of+            Just arity -> pure arity+            Nothing -> fail $ "Constructor not found in parent type: " <> show name+        _ -> fail $ "Unsupported parent type for constructor: " <> show name+    _ -> fail $ "Unsupported constructor: " <> show name+  where+    findConstructor conName = \case+      DataD _ _ _ _ cons _ -> foldr ((<|>) . matchCon conName) Nothing cons+      NewtypeD _ _ _ _ con _ -> matchCon conName con+      _ -> Nothing++    matchCon conName = \case+      NormalC n bangs+        | n == conName -> Just (length bangs)+      RecC n vars+        | n == conName -> Just (length vars)+      InfixC _ n _+        | n == conName -> Just 2+      ForallC _ _ con -> matchCon conName con+      GadtC names bangs _+        | conName `elem` names -> Just (length bangs)+      RecGadtC names vars _+        | conName `elem` names -> Just (length vars)+      _ -> Nothing++replaceAt :: Int -> a -> [a] -> [a]+replaceAt idx replacement xs =+  [if i == idx then replacement else x | (i, x) <- zip [0 ..] xs]
+ src/Nix/Lang/ExactPrint.hs view
@@ -0,0 +1,556 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE RecordWildCards #-}++-- | Exact printing for annotated Nix ASTs.+--+-- This module turns an annotated tree back into source text.+--+-- Its job is not "pretty printing" in the general sense. It follows the layout,+-- token positions, and comment ownership stored in the parsed tree, so it is the+-- renderer to use after parsing or after edits from 'Nix.Lang.Edit'.+--+-- For fresh syntax trees that do not carry exact-print annotations, use+-- 'Nix.Lang.RFCPrint' or 'Nix.Lang.Outputable' instead.+--+-- Minimal example:+--+-- @+-- import Nix.Lang.ExactPrint (renderExactText)+--+-- -- expr :: Nix.Lang.Types.Ps.Expr+-- rendered = renderExactText expr+-- @+module Nix.Lang.ExactPrint+  ( ExactPrint,+    EPError (..),+    exactPrint,+    renderExactText,+    renderExactTextM,+    renderExactDoc,+  )+where++import Control.Monad (unless)+import Control.Monad.Except+import Control.Monad.State.Strict+import Data.Text (Text)+import qualified Data.Text as T+import Nix.Lang.Annotation+import Nix.Lang.ExactPrint.Operations+import Nix.Lang.ExactPrint.Prepare.Utils+import Nix.Lang.Outputable (renderToText)+import Nix.Lang.Span+import Nix.Lang.Types+import Nix.Lang.Types.Ps+import Nix.Lang.Utils+import Prettyprinter (Doc, pretty)++--------------------------------------------------------------------------------++data EPError+  = MissingTokenSpan Text AnnToken+  | EmptyAttrPath+  | MismatchedAttrPathDots Int Int+  | MismatchedSetPatCommas Int Int+  | MissingSetPatQuestion SrcSpan+  deriving (Show, Eq)++-- | The printer accumulates exact output as concrete text chunks and tracks the+-- current logical cursor used to interpret relative gaps and token deltas.+data EPState = EPState+  { epsChunks :: ![Text],+    epsCursor :: !(Maybe RenderCursor)+  }++newtype EPM a = EPM+  { runExactM :: StateT EPState (Except EPError) a+  }+  deriving newtype (Functor, Applicative, Monad, MonadError EPError, MonadState EPState)++--------------------------------------------------------------------------------++class ExactPrint a where+  exactPrintM :: a -> EPM ()++--------------------------------------------------------------------------------++exactPrint :: (ExactPrint a) => a -> Doc ann+exactPrint = either (error . show) pretty . renderExactTextM++renderExactText :: (ExactPrint a) => a -> Text+renderExactText = either (error . show) id . renderExactTextM++renderExactTextM :: (ExactPrint a) => a -> Either EPError Text+renderExactTextM x = finishPrinterState . snd <$> runExactPrinter (exactPrintM x)++renderExactDoc :: (ExactPrint a) => a -> Either EPError (Doc ann)+renderExactDoc = fmap pretty . renderExactTextM++emptyPrinterState :: EPState+emptyPrinterState = EPState [] Nothing++runExactPrinter :: EPM a -> Either EPError (a, EPState)+runExactPrinter action = runExcept (runStateT (runExactM action) emptyPrinterState)++finishPrinterState :: EPState -> Text+finishPrinterState = T.concat . reverse . epsChunks++exactPrintLocated :: (ExactPrint a) => Located a -> EPM ()+exactPrintLocated = exactPrintM . unLoc++--------------------------------------------------------------------------------++-- | Emit a raw text fragment into the printer state.+emitText :: Text -> EPM ()+emitText txt =+  modify' $ \st@EPState {epsChunks, epsCursor} ->+    st+      { epsChunks = txt : epsChunks,+        epsCursor = fmap (`advanceCursor` txt) epsCursor+      }++setCursor :: RenderCursor -> EPM ()+setCursor cursor = modify' $ \st -> st {epsCursor = Just cursor}++-- | Ensure the printer has a current cursor, initializing it when rendering is+-- starting from an otherwise anchor-free context.+ensureCursor :: RenderCursor -> EPM RenderCursor+ensureCursor fallback = do+  mCursor <- gets epsCursor+  case mCursor of+    Just cursor -> pure cursor+    Nothing -> setCursor fallback >> pure fallback++-- | Move the output cursor to the start of a target span by emitting the exact+-- gap implied by the current cursor and that span.+emitGapToSpan :: SrcSpan -> EPM ()+emitGapToSpan span' = do+  mCursor <- gets epsCursor+  case mCursor of+    Nothing -> setCursor (cursorAtSpanStart span')+    Just cursor -> emitText (renderGapFromCursorToSpanText cursor span')++-- | Emit text whose first character is anchored at a particular span start.+emitAtSpan :: SrcSpan -> Text -> EPM ()+emitAtSpan span' txt = emitGapToSpan span' >> emitText txt++emitDelta :: DeltaPos -> EPM ()+emitDelta delta = do+  _ <- ensureCursor (RenderCursor 1 1)+  emitText (renderGapFromDeltaText delta)++-- | Recover the emitted surface text for a token from its annotation identity.+tokenTextM :: Text -> AnnToken -> EPM Text+tokenTextM label tok =+  case showToken (annToken tok) of+    Just txt -> pure txt+    Nothing -> throwError $ MissingTokenSpan (label <> ": token text unavailable") tok++-- | Emit a token using the canonical text associated with its annotation.+emitToken :: Text -> AnnToken -> EPM ()+emitToken label tok = do+  txt <- tokenTextM label tok+  emitTokenText tok txt++-- | Emit a token using explicit text supplied by the caller.+emitTokenText :: AnnToken -> Text -> EPM ()+emitTokenText tok txt = do+  case annTokenPos tok of+    AnnSpan src -> emitGapToSpan src+    AnnDelta delta -> emitDelta delta+  emitText txt++--------------------------------------------------------------------------------++emitCommentAt :: Located Comment -> EPM ()+emitCommentAt (L span' comment) = emitAtSpan span' (renderCommentText comment)++emitComments :: [Located Comment] -> EPM ()+emitComments = mapM_ emitCommentAt++emitPriorCommentsTo :: SrcSpan -> [Located Comment] -> EPM ()+emitPriorCommentsTo target comments = do+  emitComments comments+  unless (null comments) (emitGapToSpan target)++emitFollowingComments :: [Located Comment] -> EPM ()+emitFollowingComments = emitComments++-- | Emit a node together with the comments it owns.+emitWrappedNode :: (HasAnnCommon a) => SrcSpan -> a -> EPM () -> EPM ()+emitWrappedNode target ann body = do+  let comments = annComments ann+  emitPriorCommentsTo target (priorComments comments)+  body+  emitFollowingComments (followingComments comments)++--------------------------------------------------------------------------------++instance ExactPrint Expr where+  exactPrintM = \case+    NixVar ann ident -> emitWrappedNode (getLoc ident) ann $ emitAtSpan (getLoc ident) (unLoc ident)+    NixLit ann lit -> emitWrappedNode (getLoc lit) ann $ emitGapToSpan (getLoc lit) >> exactPrintM (unLoc lit)+    NixPar ann (L _ x) -> renderParM ann x+    NixString ann str -> emitWrappedNode (getLoc str) ann $ emitAtSpan (getLoc str) (renderStringText (unLoc str))+    NixPath ann path -> emitWrappedNode (getLoc path) ann $ emitAtSpan (getLoc path) (renderPathText (unLoc path))+    NixEnvPath ann path -> renderEnvPathM ann path+    NixLam ann (L _ pat) (L _ x) -> renderLamM ann pat x+    NixApp ann (L _ f) (L _ x) -> emitWrappedNode (exprSpan f) ann $ exactPrintM f >> exactPrintM x+    NixBinApp ann op (L _ x) (L _ y) -> renderBinAppM ann op x y+    NixNotApp ann (L _ x) -> renderPrefixAppM ann "!" x+    NixNegApp ann (L _ x) -> renderPrefixAppM ann "-" x+    NixList ann xs -> renderListM ann xs+    NixSet ann NixSetRecursive (L _ bindings) -> renderSetM ann True bindings+    NixSet ann NixSetNonRecursive (L _ bindings) -> renderSetM ann False bindings+    NixLet ann (L _ bindings) (L _ x) -> renderLetM ann bindings x+    NixHasAttr ann (L _ x) (L _ p) -> renderHasAttrM ann x p+    NixSelect ann (L _ x) (L _ p) mx -> renderSelectM ann x p mx+    NixIf ann (L _ cond) (L _ t) (L _ f) -> renderIfM ann cond t f+    NixWith ann (L _ scope) (L _ x) -> renderWithM ann scope x+    NixAssert ann (L _ assertion) (L _ x) -> renderAssertM ann assertion x++instance ExactPrint Lit where+  exactPrintM = emitText . renderLitText++instance ExactPrint AttrPath where+  exactPrintM path@(NixAttrPath ann keys) =+    emitWrappedNode (attrPathSpan path) ann (renderAttrPathM ann keys)++instance ExactPrint AttrKey where+  exactPrintM = emitText . renderAttrKeyText++instance ExactPrint Binding where+  exactPrintM = \case+    NixNormalBinding ann (L _ path) (L _ x) -> renderNormalBindingM ann path x+    NixInheritBinding ann mScope names -> renderInheritBindingM ann mScope names++instance ExactPrint SetPatAs where+  exactPrintM = renderSetPatAsM++instance ExactPrint FuncPat where+  exactPrintM = \case+    NixVarPat ann ident -> emitWrappedNode (getLoc ident) ann $ emitAtSpan (getLoc ident) (unLoc ident)+    pat@(NixSetPat ann ellipses mAs params) ->+      emitWrappedNode (funcPatBodySpan pat) ann $ renderSetPatM ann ellipses mAs params++--------------------------------------------------------------------------------++renderLitText :: Lit -> Text+renderLitText = \case+  NixUri _ uri -> uri+  NixInteger _ int -> T.pack (show int)+  NixFloat _ float -> T.pack (show float)+  NixBoolean _ True -> "true"+  NixBoolean _ False -> "false"+  NixNull _ -> "null"++renderStringText :: NString -> Text+renderStringText = \case+  NixDoubleQuotesString src _ -> renderDoubleQuotedSourceText src+  NixDoubleSingleQuotesString src _ -> renderIndentedStringSourceText src++renderPathText :: Path -> Text+renderPathText = \case+  NixLiteralPath _ path -> path+  NixInterpolPath _ parts -> renderInterpolatedPathText parts++renderAttrKeyText :: AttrKey -> Text+renderAttrKeyText = \case+  NixStaticAttrKey _ (L _ x) -> x+  NixDynamicStringAttrKey _ parts -> renderDoubleQuotedPartsText parts+  NixDynamicInterpolAttrKey _ expr -> renderInterpolatedExprText expr++renderInterpolatedPathText :: [LNixStringPart Ps] -> Text+renderInterpolatedPathText = T.concat . fmap (renderPathPartText . unLoc)++renderDoubleQuotedPartsText :: [LNixStringPart Ps] -> Text+renderDoubleQuotedPartsText parts = "\"" <> renderQuotedPartsText parts <> "\""++renderQuotedPartsText :: [LNixStringPart Ps] -> Text+renderQuotedPartsText = T.concat . fmap (renderQuotedPartText . unLoc)++renderPathPartText :: NixStringPart Ps -> Text+renderPathPartText = \case+  NixStringLiteral _ txt -> txt+  NixStringInterpol _ expr -> renderInterpolatedExprText expr++renderQuotedPartText :: NixStringPart Ps -> Text+renderQuotedPartText = \case+  NixStringLiteral _ txt -> txt+  NixStringInterpol _ expr -> renderInterpolatedExprText expr++renderInterpolatedExprText :: LExpr -> Text+renderInterpolatedExprText expr = "${" <> renderToText (unLoc expr) <> "}"++renderSetPatAsM :: SetPatAs -> EPM ()+renderSetPatAsM NixSetPatAs {..} =+  emitWrappedNode (setPatAsRenderSpan nspaAnn nspaVar nspaLocation) nspaAnn $ case nspaLocation of+    NixSetPatAsLeading -> do+      emitAtSpan (getLoc nspaVar) (unLoc nspaVar)+      emitToken "set pattern at" (aspaAt nspaAnn)+    NixSetPatAsTrailing -> do+      emitToken "set pattern at" (aspaAt nspaAnn)+      emitAtSpan (getLoc nspaVar) (unLoc nspaVar)++instance ExactPrint SetPatBinding where+  exactPrintM NixSetPatBinding {..} = do+    emitAtSpan (getLoc nspbVar) (unLoc nspbVar)+    case nspbDefault of+      Nothing -> pure ()+      Just defExpr -> case aspbQuestion nspbAnn of+        Just qTok -> emitToken "set pattern question" qTok >> exactPrintLocated defExpr+        Nothing -> throwError $ MissingSetPatQuestion (getLoc nspbVar)++--------------------------------------------------------------------------------++renderListM :: AnnListNode -> [LExpr] -> EPM ()+renderListM ann xs = do+  let comments = annComments ann+  emitPriorCommentsTo (expectTokenSpan "list open bracket" (alnOpenS ann)) (priorComments comments)+  emitToken "list open bracket" (alnOpenS ann)+  mapM_ exactPrintLocated xs+  emitFollowingComments (followingComments comments)+  emitToken "list close bracket" (alnCloseS ann)++--------------------------------------------------------------------------------++renderSetM :: AnnSet -> Bool -> [LBinding] -> EPM ()+renderSetM ann _ bindings = do+  let comments = annComments ann+      openAnchor = maybe (expectTokenSpan "set open brace" (asOpenC ann)) (expectTokenSpan "rec keyword") (asRec ann)+  emitPriorCommentsTo openAnchor (priorComments comments)+  case asRec ann of+    Just recTok -> emitToken "rec keyword" recTok+    Nothing -> pure ()+  emitToken "set open brace" (asOpenC ann)+  mapM_ exactPrintLocated bindings+  emitFollowingComments (followingComments comments)+  emitToken "set close brace" (asCloseC ann)++--------------------------------------------------------------------------------++renderEnvPathM :: AnnEnvPathNode -> Located Text -> EPM ()+renderEnvPathM ann path = do+  let comments = annComments ann+  emitPriorCommentsTo (expectTokenSpan "env path open" (aenvOpen ann)) (priorComments comments)+  emitToken "env path open" (aenvOpen ann)+  emitAtSpan (getLoc path) (unLoc path)+  emitFollowingComments (followingComments comments)+  emitToken "env path close" (aenvClose ann)++--------------------------------------------------------------------------------++renderLamM :: AnnLamNode -> FuncPat -> Expr -> EPM ()+renderLamM ann pat body = do+  let comments = annComments ann+  emitPriorCommentsTo (funcPatRenderSpan pat) (priorComments comments)+  exactPrintM pat+  emitToken "lambda colon" (alamColon ann)+  exactPrintM body+  emitFollowingComments (followingComments comments)++--------------------------------------------------------------------------------++renderBinAppM :: AnnBinAppNode -> BinaryOp -> Expr -> Expr -> EPM ()+renderBinAppM ann op lhs rhs = do+  let comments = annComments ann+  emitPriorCommentsTo (exprSpan lhs) (priorComments comments)+  exactPrintM lhs+  emitTokenText (abinOperator ann) (showBinOP op)+  exactPrintM rhs+  emitFollowingComments (followingComments comments)++--------------------------------------------------------------------------------++renderPrefixAppM :: AnnPrefixNode -> Text -> Expr -> EPM ()+renderPrefixAppM ann tok expr = do+  let comments = annComments ann+  emitPriorCommentsTo (expectTokenSpan "prefix operator" (apfxToken ann)) (priorComments comments)+  emitTokenText (apfxToken ann) tok+  exactPrintM expr+  emitFollowingComments (followingComments comments)++--------------------------------------------------------------------------------++renderAttrPathM :: AnnAttrPath -> [LAttrKey] -> EPM ()+renderAttrPathM ann keys =+  case keys of+    [] -> throwError EmptyAttrPath+    first : rest ->+      case aapDots ann of+        dotTok : moreDots | length (aapDots ann) == length keys -> do+          emitToken "attr path dot" dotTok+          emitGapToSpan (getLoc first)+          exactPrintM (unLoc first)+          emitAttrPathTail moreDots rest+        dots -> do+          emitGapToSpan (getLoc first)+          exactPrintM (unLoc first)+          emitAttrPathTail dots rest+  where+    emitAttrPathTail [] [] = pure ()+    emitAttrPathTail (dotTok : moreDots) (key : moreKeys) = do+      emitToken "attr path dot" dotTok+      emitGapToSpan (getLoc key)+      exactPrintM (unLoc key)+      emitAttrPathTail moreDots moreKeys+    emitAttrPathTail dots moreKeys = throwError $ MismatchedAttrPathDots (length dots) (length moreKeys)++--------------------------------------------------------------------------------++renderNormalBindingM :: AnnNormalBinding -> AttrPath -> Expr -> EPM ()+renderNormalBindingM ann path expr = do+  let comments = annComments ann+  emitPriorCommentsTo (attrPathRenderSpan path) (priorComments comments)+  exactPrintM path+  emitToken "binding equals" (anbEqual ann)+  exactPrintM expr+  emitToken "binding semicolon" (anbSemicolon ann)+  emitFollowingComments (followingComments comments)++renderInheritBindingM :: AnnInheritBinding -> Maybe LExpr -> [LAttrKey] -> EPM ()+renderInheritBindingM ann mScope names = do+  let comments = annComments ann+      inheritSpan = expectTokenSpan "inherit keyword" (aibInherit ann)+  emitPriorCommentsTo inheritSpan (priorComments comments)+  emitToken "inherit keyword" (aibInherit ann)+  maybe (pure ()) exactPrintLocated mScope+  mapM_ emitName names+  emitToken "inherit semicolon" (aibSemicolon ann)+  emitFollowingComments (followingComments comments)+  where+    emitName name = emitGapToSpan (getLoc name) >> exactPrintM (unLoc name)++--------------------------------------------------------------------------------++renderParM :: AnnParNode -> Expr -> EPM ()+renderParM ann expr = do+  let comments = annComments ann+  emitPriorCommentsTo (expectTokenSpan "paren open" (apnOpenP ann)) (priorComments comments)+  emitToken "paren open" (apnOpenP ann)+  exactPrintM expr+  emitFollowingComments (followingComments comments)+  emitToken "paren close" (apnCloseP ann)++--------------------------------------------------------------------------------++renderLetM :: AnnLetNode -> [LBinding] -> Expr -> EPM ()+renderLetM ann bindings expr = do+  let comments = annComments ann+      letSpan = expectTokenSpan "let keyword" (alLet ann)+  emitPriorCommentsTo letSpan (priorComments comments)+  emitToken "let keyword" (alLet ann)+  mapM_ exactPrintLocated bindings+  emitToken "in keyword" (alIn ann)+  exactPrintM expr+  emitFollowingComments (followingComments comments)++--------------------------------------------------------------------------------++renderIfM :: AnnIfNode -> Expr -> Expr -> Expr -> EPM ()+renderIfM ann cond thenExpr elseExpr = do+  let comments = annComments ann+      ifSpan = expectTokenSpan "if keyword" (aifIf ann)+  emitPriorCommentsTo ifSpan (priorComments comments)+  emitToken "if keyword" (aifIf ann)+  exactPrintM cond+  emitToken "then keyword" (aifThen ann)+  exactPrintM thenExpr+  emitToken "else keyword" (aifElse ann)+  exactPrintM elseExpr+  emitFollowingComments (followingComments comments)++--------------------------------------------------------------------------------++renderWithM :: AnnWithNode -> Expr -> Expr -> EPM ()+renderWithM ann scope expr = do+  let comments = annComments ann+      withSpan = expectTokenSpan "with keyword" (awWith ann)+  emitPriorCommentsTo withSpan (priorComments comments)+  emitToken "with keyword" (awWith ann)+  exactPrintM scope+  emitToken "with semicolon" (awSemicolon ann)+  exactPrintM expr+  emitFollowingComments (followingComments comments)++--------------------------------------------------------------------------------++renderAssertM :: AnnAssertNode -> Expr -> Expr -> EPM ()+renderAssertM ann assertion expr = do+  let comments = annComments ann+      assertSpan = expectTokenSpan "assert keyword" (aaAssert ann)+  emitPriorCommentsTo assertSpan (priorComments comments)+  emitToken "assert keyword" (aaAssert ann)+  exactPrintM assertion+  emitToken "assert semicolon" (aaSemicolon ann)+  exactPrintM expr+  emitFollowingComments (followingComments comments)++--------------------------------------------------------------------------------++renderHasAttrM :: AnnHasAttr -> Expr -> AttrPath -> EPM ()+renderHasAttrM ann expr path = do+  let comments = annComments ann+  emitPriorCommentsTo (exprSpan expr) (priorComments comments)+  exactPrintM expr+  emitToken "has-attr question" (ahaQuestion ann)+  exactPrintM path+  emitFollowingComments (followingComments comments)++--------------------------------------------------------------------------------++renderSelectM :: AnnSelect -> Expr -> AttrPath -> Maybe LExpr -> EPM ()+renderSelectM ann expr path def = do+  let comments = annComments ann+  emitPriorCommentsTo (exprSpan expr) (priorComments comments)+  exactPrintM expr+  exactPrintM path+  case def of+    Nothing -> pure ()+    Just defExpr -> case aslOr ann of+      Just orTok -> emitTokenText orTok "or" >> exactPrintLocated defExpr+      -- fall back to plain text+      Nothing -> emitText " or " >> exactPrintLocated defExpr+  emitFollowingComments (followingComments comments)++--------------------------------------------------------------------------------++renderSetPatM :: AnnSetPatNode -> NixSetPatEllipses -> Maybe LSetPatAs -> [LSetPatBinding] -> EPM ()+renderSetPatM ann ellipses mAs params = do+  case mAs of+    Just (L _ asPat@NixSetPatAs {nspaLocation = NixSetPatAsLeading}) -> exactPrintM asPat+    _ -> pure ()+  emitToken "set pattern open" (aspOpenC ann)+  renderSetPatEntriesM params (aspCommas ann) ellipses (aspEllipsis ann)+  emitToken "set pattern close" (aspCloseC ann)+  case mAs of+    Just (L _ asPat@NixSetPatAs {nspaLocation = NixSetPatAsTrailing}) -> exactPrintM asPat+    _ -> pure ()++renderSetPatEntriesM :: [LSetPatBinding] -> [AnnToken] -> NixSetPatEllipses -> Maybe AnnToken -> EPM ()+renderSetPatEntriesM params commas ellipses ellipsisTok = do+  let (separatorCommas, trailingComma) = splitAt (max 0 (length params - 1)) commas+  renderBindings params separatorCommas+  case (ellipses, ellipsisTok, trailingComma) of+    (NixSetPatIsEllipses, Just tok, commaTok : _) -> emitToken "set pattern comma" commaTok >> emitToken "set pattern ellipsis" tok+    (NixSetPatIsEllipses, Just tok, []) -> emitToken "set pattern ellipsis" tok+    (NixSetPatIsEllipses, Nothing, _) -> pure ()+    (_, _, commaTok : _) -> emitToken "set pattern trailing comma" commaTok+    _ -> pure ()+  where+    renderBindings [] [] = pure ()+    renderBindings (param : rest) commaToks = do+      exactPrintM (unLoc param)+      case (rest, commaToks) of+        (next : more, commaTok : remaining) -> emitToken "set pattern comma" commaTok >> renderBindings (next : more) remaining+        ([], []) -> pure ()+        _ -> throwError $ MismatchedSetPatCommas (length commaToks) (length rest)+    renderBindings [] remaining = throwError $ MismatchedSetPatCommas (length remaining) 0++--------------------------------------------------------------------------------++renderCommentText :: Comment -> Text+renderCommentText = \case+  LineComment txt -> "#" <> txt+  BlockComment txt -> "/*" <> txt <> "*/"
+ src/Nix/Lang/ExactPrint/Operations.hs view
@@ -0,0 +1,358 @@+-- | 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))
+ src/Nix/Lang/ExactPrint/Prepare/Parse.hs view
@@ -0,0 +1,46 @@+-- | Parse replacement fragments for the exact-print repair pipeline.+module Nix.Lang.ExactPrint.Prepare.Parse+  ( parseExpr,+    parseBinding,+    parseAttrKey,+  )+where++import Data.Text (Text)+import qualified Data.Text as T+import Nix.Lang.ExactPrint.Prepare.Types+import Nix.Lang.Parser (Parser, attrKey, located, nixBinding, nixExpr, runNixParser)+import Nix.Lang.Span+import Nix.Lang.Types.Ps+import Text.Megaparsec (eof, errorBundlePretty)++-- | Parse a standalone expression fragment.+parseExpr :: Text -> EPResult LExpr+parseExpr = parseFragment "<expr>" locatedExprParser ParseExprError+  where+    locatedExprParser = do+      L l expr <- located nixExpr+      pure (L l expr)++-- | Parse a standalone binding fragment.+parseBinding :: Text -> EPResult LBinding+parseBinding = parseFragment "<binding>" locatedBindingParser ParseBindingError+  where+    locatedBindingParser = do+      L l binding <- located nixBinding+      pure (L l binding)++-- | Parse a standalone attribute-key fragment.+parseAttrKey :: Text -> EPResult LAttrKey+parseAttrKey = parseFragment "<attr-key>" locatedKeyParser ParseAttrKeyError+  where+    locatedKeyParser = do+      L l key <- located attrKey+      pure (L l key)++-- | Parse a fragment and convert parser failures into 'EPError'.+parseFragment :: String -> Parser a -> (Text -> EPError) -> Text -> EPResult a+parseFragment label parser mkErr src =+  case runNixParser (parser <* eof) label src of+    (Right value, _) -> Right value+    (Left err, _) -> Left (mkErr (T.pack (errorBundlePretty err)))
+ src/Nix/Lang/ExactPrint/Prepare/Rebuild.hs view
@@ -0,0 +1,442 @@+-- | 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
+ src/Nix/Lang/ExactPrint/Prepare/Reflow.hs view
@@ -0,0 +1,42 @@+-- | Sequence reflow primitives used while rebuilding lists and binding groups.+module Nix.Lang.ExactPrint.Prepare.Reflow where++import Data.Text (Text)+import Nix.Lang.ExactPrint.Operations+import Nix.Lang.Span+import Nix.Lang.Utils++-- | Direction in which a rebuilt sequence should flow.+data Flow+  = FlowInline+  | FlowMultiline++-- | Reanchor and advance a sequence of items from left to right.+reflow :: Flow -> (a -> Text) -> (RenderCursor -> Located a -> Located a) -> RenderCursor -> [Located a] -> [Located a]+reflow flow renderItem moveItem startCursor = reverse . snd . foldl' step (startCursor, [])+  where+    step (!cursor, acc) item =+      let moved = moveItem cursor item+          next = advanceItem flow renderItem cursor moved+       in (next, moved : acc)++-- | Advance the cursor past one rebuilt item according to the chosen flow.+advanceItem :: Flow -> (a -> Text) -> RenderCursor -> Located a -> RenderCursor+advanceItem flow renderItem startCursor item =+  case flow of+    FlowInline -> advanceCursor endCursor " "+    FlowMultiline -> RenderCursor (rcLine endCursor + 1) (rcColumn startCursor)+  where+    endCursor = advanceCursor startCursor (renderItem (unLoc item))++-- | Compute where a closing delimiter should be placed after a rebuilt sequence.+closeAfter :: Flow -> (a -> Text) -> SrcSpan -> [Located a] -> RenderCursor+closeAfter flow renderItem oldClose items =+  case reverse items of+    [] -> cursorAtSpanStart oldClose+    lastItem : _ ->+      case flow of+        FlowInline -> advanceCursor (cursorAtSpanStart (getLoc lastItem)) (renderItem (unLoc lastItem) <> " ")+        FlowMultiline ->+          let endCursor = advanceCursor (cursorAtSpanStart (getLoc lastItem)) (renderItem (unLoc lastItem))+           in RenderCursor (rcLine endCursor + 1) (srcSpanStartColumn oldClose)
+ src/Nix/Lang/ExactPrint/Prepare/Repair.hs view
@@ -0,0 +1,543 @@+-- | Recursive repair engine for exact-print normalization.+--+-- After an edit changes tree shape, this module recalculates spans, token+-- deltas, and related annotations so the tree can be rendered again.+module Nix.Lang.ExactPrint.Prepare.Repair+  ( repairExprLayout,+    repairBindingLayout,+    repairAttrPathLayout,+    repairFuncPatLayout,+  )+where++import Control.Monad (foldM)+import Control.Monad.Reader (ReaderT (..), ask, local, runReaderT)+import Data.Text (Text)+import qualified Data.Text as T+import Nix.Lang.Annotation+import Nix.Lang.ExactPrint.Prepare.Rebuild+import Nix.Lang.ExactPrint.Prepare.Types+import Nix.Lang.ExactPrint.Prepare.Utils+import Nix.Lang.ExactPrint.Operations+import Nix.Lang.Outputable (renderToText)+import Nix.Lang.Span+import Nix.Lang.Types+import Nix.Lang.Types.Ps+import Nix.Lang.Utils++--------------------------------------------------------------------------------++newtype RepairContext = RepairContext+  { repairCursor :: RenderCursor+  }++type RepairM = ReaderT RepairContext EPResult++--------------------------------------------------------------------------------++runRepairAt :: RenderCursor -> RepairM a -> EPResult a+runRepairAt cursor = flip runReaderT (RepairContext cursor)++withRepairCursor :: RenderCursor -> RepairM a -> RepairM a+withRepairCursor cursor = local (\ctx -> ctx {repairCursor = cursor})++currentRepairCursor :: RepairM RenderCursor+currentRepairCursor = repairCursor <$> ask++--------------------------------------------------------------------------------++repairExprLayout :: Expr -> EPResult Expr+repairExprLayout = repairLocatedLayout exprSpan repairExpr++repairBindingLayout :: Binding -> EPResult Binding+repairBindingLayout = repairLocatedLayout bindingSpan repairBinding++repairAttrPathLayout :: AttrPath -> EPResult AttrPath+repairAttrPathLayout = repairLocatedLayout attrPathSpan repairAttrPath++repairFuncPatLayout :: FuncPat -> EPResult FuncPat+repairFuncPatLayout = repairLocatedLayout funcPatBodySpan repairFuncPat++--------------------------------------------------------------------------------++repairLocatedLayout :: (a -> SrcSpan) -> (Located a -> RepairM (Located a)) -> a -> EPResult a+repairLocatedLayout spanOf repair value =+  unLoc <$> runRepairAt (spanStartCursor span') (repair (L span' value))+  where+    span' = spanOf value++--------------------------------------------------------------------------------++-- | Repair an expression subtree starting from the current repair cursor.+repairExpr :: LExpr -> RepairM LExpr+repairExpr (L originalSpan node) =+  case node of+    NixVar ann ident -> pure (repairVarExpr originalSpan ann ident)+    NixLit ann lit -> pure (repairLit originalSpan ann lit)+    NixPar ann inner -> repairPar originalSpan ann inner+    NixString ann str -> pure (repairStringExpr originalSpan ann str)+    NixPath ann path -> pure (repairPathExpr originalSpan ann path)+    NixEnvPath ann path -> pure (repairEnvPath originalSpan ann path)+    NixLam ann pat body -> repairLam originalSpan ann pat body+    NixApp ann lhs rhs -> repairApp originalSpan ann lhs rhs+    NixBinApp ann op lhs rhs -> repairBinApp originalSpan ann op lhs rhs+    NixNotApp ann inner -> repairPrefix originalSpan ann NixNotApp inner+    NixNegApp ann inner -> repairPrefix originalSpan ann NixNegApp inner+    NixList ann xs -> repairList ann xs+    NixSet ann kind (L _ bindings) -> repairSet ann kind bindings+    NixLet ann (L _ bindings) body -> repairLet ann bindings body+    NixHasAttr ann lhs path -> repairHasAttr originalSpan ann lhs path+    NixSelect ann lhs path def -> repairSelect originalSpan ann lhs path def+    NixIf ann cond thenExpr elseExpr -> repairIf originalSpan ann cond thenExpr elseExpr+    NixWith ann scope body -> repairWith originalSpan ann scope body+    NixAssert ann assertion body -> repairAssert originalSpan ann assertion body++repairExprAt :: RenderCursor -> LExpr -> RepairM LExpr+repairExprAt cursor expr = withRepairCursor cursor (repairExpr translated)+  where+    translated = translateFromTo (getLoc expr) (cursorSpan cursor (srcSpanFilename (getLoc expr))) expr++repairChildAfter :: SrcSpan -> SrcSpan -> SrcSpan -> LExpr -> RepairM LExpr+repairChildAfter oldAnchor oldTarget newAnchor child =+  repairExprAt (preserveGapTarget oldAnchor oldTarget newAnchor) child++--------------------------------------------------------------------------------++repairVarExpr :: SrcSpan -> AnnCommon -> LVarName -> LExpr+repairVarExpr originalSpan ann ident =+  let ident' = repairLocatedTextAt (spanStartCursor originalSpan) ident+      span' = getLoc ident'+      ann' = setAnnSpan span' ann+   in L span' (NixVar ann' ident')++--------------------------------------------------------------------------------++repairLit :: SrcSpan -> AnnCommon -> LLit -> LExpr+repairLit originalSpan ann lit =+  let lit' = repairLocatedLitAt (spanStartCursor originalSpan) lit+      span' = getLoc lit'+      ann' = setAnnSpan span' ann+   in L span' (NixLit ann' lit')++--------------------------------------------------------------------------------++repairStringExpr :: SrcSpan -> AnnStringNode -> LNString -> LExpr+repairStringExpr originalSpan ann str =+  let str' = repairLocatedStringAt (spanStartCursor originalSpan) str+      span' = getLoc str'+      ann' = setAnnSpan span' ann+   in L span' (NixString ann' str')++--------------------------------------------------------------------------------++repairPathExpr :: SrcSpan -> AnnPathNode -> LPath -> LExpr+repairPathExpr originalSpan ann path =+  let path' = repairLocatedPathAt (spanStartCursor originalSpan) path+      span' = getLoc path'+      ann' = setAnnSpan span' ann+   in L span' (NixPath ann' path')++--------------------------------------------------------------------------------++repairPar :: SrcSpan -> AnnParNode -> LExpr -> RepairM LExpr+repairPar originalSpan ann inner = do+  let openSpan = tokenSpanAt (spanStartCursor originalSpan) (apnOpenP ann)+  inner' <- repairChildAfter (expectTokenSpan "paren open" (apnOpenP ann)) (getLoc inner) openSpan inner+  let closeSpan = preserveGapSpan (getLoc inner) (tokenSpanFromAnchor "paren close" (getLoc inner') (apnCloseP ann)) inner'+      ann0 = ann {apnOpenP = (apnOpenP ann) {annTokenPos = AnnSpan openSpan}, apnCloseP = (apnCloseP ann) {annTokenPos = AnnSpan closeSpan}}+      ann' = setAnnSpan (openSpan `combineSrcSpans` closeSpan) (prepareParLayout ann0 (unLoc inner'))+      expr' = NixPar ann' inner'+  pure (L (exprSpan expr') expr')++--------------------------------------------------------------------------------++repairEnvPath :: SrcSpan -> AnnEnvPathNode -> Located Text -> LExpr+repairEnvPath _ ann path =+  let openSpan = expectTokenSpan "env path open" (aenvOpen ann)+      path' = repairLocatedTextAt (spanStartCursor (preserveGapSpan openSpan (getLoc path) (L openSpan ()))) path+      closeSpan = preserveGapSpan (getLoc path) (expectTokenSpan "env path close" (aenvClose ann)) path'+      ann' = setAnnSpan (openSpan `combineSrcSpans` closeSpan) ann {aenvClose = (aenvClose ann) {annTokenPos = AnnSpan closeSpan}}+      span' = openSpan `combineSrcSpans` closeSpan+   in L span' (NixEnvPath ann' path')++--------------------------------------------------------------------------------++repairLam :: SrcSpan -> AnnLamNode -> LFuncPat -> LExpr -> RepairM LExpr+repairLam originalSpan ann pat body = do+  pat' <- repairFuncPatAt (spanStartCursor originalSpan) pat+  let colonSpan = preserveGapSpan (funcPatRenderSpan (unLoc pat)) (expectTokenSpan "lambda colon" (alamColon ann)) pat'+  body' <- repairChildAfter (expectTokenSpan "lambda colon" (alamColon ann)) (getLoc body) colonSpan body+  let span' = funcPatRenderSpan (unLoc pat') `combineSrcSpans` getLoc body'+      ann' = setAnnSpan span' ann {alamColon = (alamColon ann) {annTokenPos = AnnSpan colonSpan}}+  pure (L span' (NixLam ann' pat' body'))++--------------------------------------------------------------------------------++repairApp :: SrcSpan -> AnnAppNode -> LExpr -> LExpr -> RepairM LExpr+repairApp originalSpan ann lhs rhs = do+  lhs' <- repairExprAt (spanStartCursor originalSpan) lhs+  rhs' <- repairChildAfter (getLoc lhs) (getLoc rhs) (getLoc lhs') rhs+  let span' = getLoc lhs' `combineSrcSpans` getLoc rhs'+      ann' = setAnnSpan span' ann+  pure (L span' (NixApp ann' lhs' rhs'))++--------------------------------------------------------------------------------++repairBinApp :: SrcSpan -> AnnBinAppNode -> BinaryOp -> LExpr -> LExpr -> RepairM LExpr+repairBinApp originalSpan ann op lhs rhs = do+  lhs' <- repairExprAt (spanStartCursor originalSpan) lhs+  let opSpan = preserveGapSpan (getLoc lhs) (expectTokenSpan "binary operator" (abinOperator ann)) lhs'+  rhs' <- repairChildAfter (expectTokenSpan "binary operator" (abinOperator ann)) (getLoc rhs) opSpan rhs+  let span' = getLoc lhs' `combineSrcSpans` getLoc rhs'+      ann' = setAnnSpan span' ann {abinOperator = (abinOperator ann) {annTokenPos = AnnSpan opSpan}}+  pure (L span' (NixBinApp ann' op lhs' rhs'))++--------------------------------------------------------------------------------++repairPrefix :: SrcSpan -> AnnPrefixNode -> (AnnPrefixNode -> LExpr -> Expr) -> LExpr -> RepairM LExpr+repairPrefix originalSpan ann mkNode inner = do+  let tokSpan = tokenSpanAt (spanStartCursor originalSpan) (apfxToken ann)+  inner' <- repairChildAfter (expectTokenSpan "prefix operator" (apfxToken ann)) (getLoc inner) tokSpan inner+  let span' = tokSpan `combineSrcSpans` getLoc inner'+      ann' = setAnnSpan span' ann {apfxToken = (apfxToken ann) {annTokenPos = AnnSpan tokSpan}}+  pure (L span' (mkNode ann' inner'))++--------------------------------------------------------------------------------++repairList :: AnnListNode -> [LExpr] -> RepairM LExpr+repairList ann xs = do+  xs' <- mapM repairExpr xs+  repaired <- liftEPResult (rebuildListLayout ann xs')+  pure (L (exprSpan repaired) repaired)++--------------------------------------------------------------------------------++repairSet :: AnnSet -> NixSetIsRecursive -> [LBinding] -> RepairM LExpr+repairSet ann kind bindings = do+  bindings' <- mapM repairBinding bindings+  repaired <- liftEPResult (rebuildSetLayout ann kind bindings')+  pure (L (exprSpan repaired) repaired)++--------------------------------------------------------------------------------++repairLet :: AnnLetNode -> [LBinding] -> LExpr -> RepairM LExpr+repairLet ann bindings body = do+  -- repair bindings+  bindings' <- mapM repairBinding bindings+  -- rebuild let to discover new in+  provisional <- liftEPResult (rebuildLetLayout ann bindings' body)+  let newInSpan = case provisional of+        NixLet ann' _ _ -> expectTokenSpan "in keyword" (alIn ann')+        _ -> error "impossible: rebuildLetLayout did not return let"+      oldInSpan = expectTokenSpan "in keyword" (alIn ann)+      bodyCursor+        | srcSpanStartLine (getLoc body) > srcSpanEndLine oldInSpan = RenderCursor (srcSpanEndLine newInSpan + 1) (srcSpanStartColumn (getLoc body))+        | otherwise = preserveGapTarget oldInSpan (getLoc body) newInSpan+  -- repair body there+  body' <- repairExprAt bodyCursor body+  -- rebuild let again with the repaired body+  repaired <- liftEPResult (rebuildLetLayout ann bindings' body')+  pure (L (exprSpan repaired) repaired)++--------------------------------------------------------------------------------++repairHasAttr :: SrcSpan -> AnnHasAttr -> LExpr -> LAttrPath -> RepairM LExpr+repairHasAttr originalSpan ann lhs path = do+  lhs' <- repairExprAt (spanStartCursor originalSpan) lhs+  let qSpan = preserveGapSpan (getLoc lhs) (expectTokenSpan "has-attr question" (ahaQuestion ann)) lhs'+  path' <- repairAttrPathAt (preserveGapTarget (expectTokenSpan "has-attr question" (ahaQuestion ann)) (getLoc path) qSpan) path+  let span' = getLoc lhs' `combineSrcSpans` attrPathSpan (unLoc path')+      ann' = setAnnSpan span' (prepareHasAttrLayout (ann {ahaQuestion = (ahaQuestion ann) {annTokenPos = AnnSpan qSpan}}) (unLoc lhs') (unLoc path'))+  pure (L span' (NixHasAttr ann' lhs' path'))++--------------------------------------------------------------------------------++repairSelect :: SrcSpan -> AnnSelect -> LExpr -> LAttrPath -> Maybe LExpr -> RepairM LExpr+repairSelect originalSpan ann lhs path def = do+  -- repair lhs from the original root+  lhs' <- repairExprAt (spanStartCursor originalSpan) lhs+  -- move path relative to the repaired lhs+  path' <- repairAttrPathAt (preserveGapTarget (getLoc lhs) (getLoc path) (getLoc lhs')) path+  -- if def exists, repair it relative to the repaired or+  def' <- traverse (repairExprAtSelect ann path path') def+  -- compute new or span from repaired path+  let annWithOr = case (aslOr ann, def, def') of+        (Just orTok, Just _, Just _) ->+          let orSpan = preserveGapSpan (attrPathRenderSpan (unLoc path)) (expectTokenSpan "select or" orTok) path'+           in ann {aslOr = Just (orTok {annTokenPos = AnnSpan orSpan})}+        _ -> ann+      ann' = setAnnSpan finalSpan (prepareSelectLayout annWithOr (unLoc lhs') (unLoc path') def')+      finalSpan = maybe baseSpan (combineSrcSpans baseSpan . getLoc) def'+      baseSpan = getLoc lhs' `combineSrcSpans` attrPathSpan (unLoc path')+  pure (L finalSpan (NixSelect ann' lhs' path' def'))++--------------------------------------------------------------------------------++repairIf :: SrcSpan -> AnnIfNode -> LExpr -> LExpr -> LExpr -> RepairM LExpr+repairIf originalSpan ann cond thenExpr elseExpr = do+  let ifSpan = tokenSpanAt (spanStartCursor originalSpan) (aifIf ann)+  cond' <- repairExprAt (preserveGapTarget (expectTokenSpan "if keyword" (aifIf ann)) (getLoc cond) ifSpan) cond+  let thenSpan = preserveGapSpan (getLoc cond) (expectTokenSpan "then keyword" (aifThen ann)) cond'+  then' <- repairExprAt (preserveGapTarget (expectTokenSpan "then keyword" (aifThen ann)) (getLoc thenExpr) thenSpan) thenExpr+  let elseSpan = preserveGapSpan (getLoc thenExpr) (expectTokenSpan "else keyword" (aifElse ann)) then'+  else' <- repairExprAt (preserveGapTarget (expectTokenSpan "else keyword" (aifElse ann)) (getLoc elseExpr) elseSpan) elseExpr+  let ann0 = ann {aifIf = (aifIf ann) {annTokenPos = AnnSpan ifSpan}, aifThen = (aifThen ann) {annTokenPos = AnnSpan thenSpan}, aifElse = (aifElse ann) {annTokenPos = AnnSpan elseSpan}}+      span' = ifSpan `combineSrcSpans` getLoc else'+      ann' = setAnnSpan span' (prepareIfLayout ann0 (unLoc cond') (unLoc then') (unLoc else'))+  pure (L span' (NixIf ann' cond' then' else'))++--------------------------------------------------------------------------------++repairWith :: SrcSpan -> AnnWithNode -> LExpr -> LExpr -> RepairM LExpr+repairWith originalSpan ann scope body = do+  let withSpan = tokenSpanAt (spanStartCursor originalSpan) (awWith ann)+  scope' <- repairExprAt (preserveGapTarget (expectTokenSpan "with keyword" (awWith ann)) (getLoc scope) withSpan) scope+  let semiSpan = preserveGapSpan (getLoc scope) (expectTokenSpan "with semicolon" (awSemicolon ann)) scope'+  body' <- repairExprAt (preserveGapTarget (expectTokenSpan "with semicolon" (awSemicolon ann)) (getLoc body) semiSpan) body+  let ann0 = ann {awWith = (awWith ann) {annTokenPos = AnnSpan withSpan}, awSemicolon = (awSemicolon ann) {annTokenPos = AnnSpan semiSpan}}+      span' = withSpan `combineSrcSpans` getLoc body'+      ann' = setAnnSpan span' (prepareWithLayout ann0 (unLoc scope') (unLoc body'))+  pure (L span' (NixWith ann' scope' body'))++--------------------------------------------------------------------------------++repairAssert :: SrcSpan -> AnnAssertNode -> LExpr -> LExpr -> RepairM LExpr+repairAssert originalSpan ann assertion body = do+  let assertSpan = tokenSpanAt (spanStartCursor originalSpan) (aaAssert ann)+  assertion' <- repairExprAt (preserveGapTarget (expectTokenSpan "assert keyword" (aaAssert ann)) (getLoc assertion) assertSpan) assertion+  let semiSpan = preserveGapSpan (getLoc assertion) (expectTokenSpan "assert semicolon" (aaSemicolon ann)) assertion'+  body' <- repairExprAt (preserveGapTarget (expectTokenSpan "assert semicolon" (aaSemicolon ann)) (getLoc body) semiSpan) body+  let ann0 = ann {aaAssert = (aaAssert ann) {annTokenPos = AnnSpan assertSpan}, aaSemicolon = (aaSemicolon ann) {annTokenPos = AnnSpan semiSpan}}+      span' = assertSpan `combineSrcSpans` getLoc body'+      ann' = setAnnSpan span' (prepareAssertLayout ann0 (unLoc assertion') (unLoc body'))+  pure (L span' (NixAssert ann' assertion' body'))++--------------------------------------------------------------------------------++repairBinding :: LBinding -> RepairM LBinding+repairBinding (L originalSpan node) =+  case node of+    NixNormalBinding ann path expr -> repairNormalBinding originalSpan ann path expr+    NixInheritBinding ann mScope names -> repairInheritBinding originalSpan ann mScope names++repairNormalBinding :: SrcSpan -> AnnNormalBinding -> LAttrPath -> LExpr -> RepairM LBinding+repairNormalBinding originalSpan ann path expr = do+  path' <- repairAttrPathAt (spanStartCursor originalSpan) path+  let eqSpan = preserveGapSpan (attrPathSpan (unLoc path)) (tokenSpanFromAnchor "binding equals" (attrPathSpan (unLoc path')) (anbEqual ann)) path'+  expr' <- repairExprAt (preserveGapTarget (tokenSpanFromAnchor "binding equals" (attrPathSpan (unLoc path')) (anbEqual ann)) (getLoc expr) eqSpan) expr+  let semiSpan = preserveGapSpan (getLoc expr) (tokenSpanFromAnchor "binding semicolon" (getLoc expr') (anbSemicolon ann)) expr'+      ann' = setAnnSpan span' ann {anbEqual = (anbEqual ann) {annTokenPos = AnnSpan eqSpan}, anbSemicolon = (anbSemicolon ann) {annTokenPos = AnnSpan semiSpan}}+      span' = attrPathSpan (unLoc path') `combineSrcSpans` semiSpan+  pure (L span' (NixNormalBinding ann' path' expr'))++repairInheritBinding :: SrcSpan -> AnnInheritBinding -> Maybe LExpr -> [LAttrKey] -> RepairM LBinding+repairInheritBinding originalSpan ann mScope names = do+  let inheritSpan = tokenSpanAt (spanStartCursor originalSpan) (aibInherit ann)+  scope' <- case mScope of+    Nothing -> pure Nothing+    Just scopeExpr ->+      Just+        <$> repairExprAt+          (preserveGapTarget (expectTokenSpan "inherit keyword" (aibInherit ann)) (getLoc scopeExpr) inheritSpan)+          scopeExpr+  names' <- repairInheritNames inheritSpan mScope scope' names+  let anchorOld = maybe (maybe (expectTokenSpan "inherit keyword" (aibInherit ann)) getLoc (lastMay names)) getLoc mScope+      anchorNew = maybe (maybe inheritSpan getLoc (lastMay names')) getLoc scope'+      semiSpan = preserveGapSpan anchorOld (expectTokenSpan "inherit semicolon" (aibSemicolon ann)) (L anchorNew ())+      span' = foldr combineSrcSpans (inheritSpan `combineSrcSpans` semiSpan) (maybe [] ((: []) . getLoc) scope' <> fmap getLoc names')+      ann' = setAnnSpan span' ann {aibInherit = (aibInherit ann) {annTokenPos = AnnSpan inheritSpan}, aibSemicolon = (aibSemicolon ann) {annTokenPos = AnnSpan semiSpan}}+  pure (L span' (NixInheritBinding ann' scope' names'))++--------------------------------------------------------------------------------++repairAttrPath :: LAttrPath -> RepairM LAttrPath+repairAttrPath path = repairAttrPathAt (spanStartCursor (getLoc path)) path++repairAttrPathAt :: RenderCursor -> LAttrPath -> RepairM LAttrPath+repairAttrPathAt cursor (L _ (NixAttrPath ann keys)) =+  withRepairCursor cursor $+    case keys of+      [] -> liftEPResult (Left EmptyAttrPathEdit)+      firstKey : restKeys -> repairAttrPathHead ann keys firstKey restKeys++repairAttrPathHead :: AnnAttrPath -> [LAttrKey] -> LAttrKey -> [LAttrKey] -> RepairM LAttrPath+repairAttrPathHead ann keys firstKey restKeys = do+  cursor <- currentRepairCursor+  case (leadingDot, dotTokens) of+    (True, firstDot : restDots) -> do+      let firstDotSpan = tokenSpanAt cursor firstDot+      firstKey' <- repairAttrKeyAt (preserveGapTarget (expectTokenSpan "attr path dot" firstDot) (getLoc firstKey) firstDotSpan) firstKey+      (dots', keys') <- repairAttrTail restDots restKeys (getLoc firstKey) firstKey'+      let ann' = setAnnSpan span' ann {aapDots = firstDot {annTokenPos = AnnSpan firstDotSpan} : dots'}+          span' = firstDotSpan `combineSrcSpans` foldr1 combineSrcSpans (getLoc firstKey' : fmap getLoc keys')+      pure (L span' (NixAttrPath ann' (firstKey' : keys')))+    _ -> do+      firstKey' <- repairAttrKeyAt cursor firstKey+      (dots', keys') <- repairAttrTail dotTokens restKeys (getLoc firstKey) firstKey'+      let span' = foldr1 combineSrcSpans (getLoc firstKey' : fmap getLoc keys')+          ann' = setAnnSpan span' ann {aapDots = dots'}+      pure (L span' (NixAttrPath ann' (firstKey' : keys')))+  where+    leadingDot = length (aapDots ann) == length keys && not (null (aapDots ann))+    dotTokens = aapDots ann++--------------------------------------------------------------------------------+repairFuncPat :: LFuncPat -> RepairM LFuncPat+repairFuncPat pat = repairFuncPatAt (spanStartCursor (getLoc pat)) pat++repairFuncPatAt :: RenderCursor -> LFuncPat -> RepairM LFuncPat+repairFuncPatAt cursor (L _ node) =+  withRepairCursor cursor $+    case node of+      NixVarPat ann ident ->+        let ident' = repairLocatedTextAt cursor ident+            span' = getLoc ident'+            ann' = setAnnSpan span' ann {avpId = span'}+         in pure (L span' (NixVarPat ann' ident'))+      NixSetPat ann ellipses mAs bindings -> repairSetPat ann ellipses mAs bindings++repairSetPat :: AnnSetPatNode -> NixSetPatEllipses -> Maybe LSetPatAs -> [LSetPatBinding] -> RepairM LFuncPat+repairSetPat ann ellipses mAs bindings = do+  cursor <- currentRepairCursor+  let openSpan = tokenSpanAt cursor (aspOpenC ann)+  mAs' <- traverse (repairSetPatAsAt cursor) mAs+  let entryCursor = setPatEntryCursor openSpan mAs'+  bindings' <- repairSetPatBindingsAt ann entryCursor bindings+  let closeSpan = repairSetPatCloseSpan ann bindings' ellipses+      ann' = setAnnSpan patSpan ann {aspOpenC = (aspOpenC ann) {annTokenPos = AnnSpan openSpan}, aspCloseC = (aspCloseC ann) {annTokenPos = AnnSpan closeSpan}}+      patNode = NixSetPat ann' ellipses mAs' bindings'+      patSpan = funcPatBodySpan patNode+  pure (L patSpan patNode)++setPatEntryCursor :: SrcSpan -> Maybe LSetPatAs -> RenderCursor+setPatEntryCursor openSpan = \case+  Just asPat@(L _ NixSetPatAs {nspaLocation = NixSetPatAsLeading}) -> spanStartCursor (getLoc asPat)+  _ -> RenderCursor (srcSpanStartLine openSpan) (srcSpanEndColumn openSpan)++--------------------------------------------------------------------------------++repairAttrKeyAt :: RenderCursor -> LAttrKey -> RepairM LAttrKey+repairAttrKeyAt cursor (L _ key) =+  let span' = case key of+        NixStaticAttrKey _ ident -> textSpanAt (cursorFile ident) cursor (unLoc ident)+        NixDynamicStringAttrKey _ _ -> textSpanAt cursorFallbackFile cursor (renderToText key)+        NixDynamicInterpolAttrKey _ _ -> textSpanAt cursorFallbackFile cursor (renderToText key)+      key' = case key of+        NixStaticAttrKey ann ident -> NixStaticAttrKey ann (repairLocatedTextAt cursor ident)+        other -> other+   in pure (L span' key')++--------------------------------------------------------------------------------++repairLocatedLitAt :: RenderCursor -> LLit -> LLit+repairLocatedLitAt cursor (L _ lit) = L (textSpanAt cursorFallbackFile cursor (renderToText lit)) lit++repairLocatedStringAt :: RenderCursor -> LNString -> LNString+repairLocatedStringAt cursor (L _ str) = L (textSpanAt cursorFallbackFile cursor (renderStringText str)) str+  where+    renderStringText = \case+      NixDoubleQuotesString src _ -> renderDoubleQuotedSourceText src+      NixDoubleSingleQuotesString src _ -> renderIndentedStringSourceText src++repairLocatedPathAt :: RenderCursor -> LPath -> LPath+repairLocatedPathAt cursor (L _ path) = L (textSpanAt cursorFallbackFile cursor (renderToText path)) path++repairLocatedTextAt :: RenderCursor -> Located Text -> Located Text+repairLocatedTextAt cursor (L _ txt) = L (textSpanAt cursorFallbackFile cursor txt) txt++--------------------------------------------------------------------------------++repairSetPatAsAt :: RenderCursor -> LSetPatAs -> RepairM LSetPatAs+repairSetPatAsAt cursor (L _ asPat@NixSetPatAs {..}) =+  let var' = repairLocatedTextAt cursor nspaVar+      atSpan = case nspaLocation of+        NixSetPatAsLeading -> tokenSpanAt (spanEndCursor (getLoc var')) (aspaAt nspaAnn)+        NixSetPatAsTrailing -> tokenSpanAt cursor (aspaAt nspaAnn)+      span' = setPatAsRenderSpan (nspaAnn {aspaAt = (aspaAt nspaAnn) {annTokenPos = AnnSpan atSpan}}) var' nspaLocation+      ann' = setAnnSpan span' nspaAnn {aspaAt = (aspaAt nspaAnn) {annTokenPos = AnnSpan atSpan}}+   in pure (L span' asPat {nspaAnn = ann', nspaVar = var'})++repairSetPatBindingsAt :: AnnSetPatNode -> RenderCursor -> [LSetPatBinding] -> RepairM [LSetPatBinding]+repairSetPatBindingsAt ann startCursor bindings = reverse . snd <$> foldM step (startCursor, []) (zip [0 ..] bindings)+  where+    step (!cursor, repaired) (idx, binding) = do+      binding' <- repairSetPatBindingAt ann idx cursor binding+      let next = advanceCursor (spanStartCursor (getLoc binding')) (renderToText (unLoc binding'))+      pure (next, binding' : repaired)++repairSetPatBindingAt :: AnnSetPatNode -> Int -> RenderCursor -> LSetPatBinding -> RepairM LSetPatBinding+repairSetPatBindingAt _ _ cursor (L _ binding@NixSetPatBinding {..}) = do+  let var' = repairLocatedTextAt cursor nspbVar+  def' <- case nspbDefault of+    Nothing -> pure Nothing+    Just defExpr ->+      Just+        <$> repairExprAt+          (preserveGapTargetForSetPat binding var' defExpr)+          defExpr+  let qTok' = case (aspbQuestion nspbAnn, nspbDefault, def') of+        (Just qTok, Just _, Just _) ->+          let qSpan = preserveGapSpan (getLoc nspbVar) (expectTokenSpan "set pattern question" qTok) var'+           in Just (qTok {annTokenPos = AnnSpan qSpan})+        _ -> aspbQuestion nspbAnn+      binding' = NixSetPatBinding (setAnnSpan span' nspbAnn {aspbQuestion = qTok'}) var' def'+      span' = maybe (getLoc var') (combineSrcSpans (getLoc var') . getLoc) def'+  pure (L span' binding')++--------------------------------------------------------------------------------++repairAttrTail :: [AnnToken] -> [LAttrKey] -> SrcSpan -> LAttrKey -> RepairM ([AnnToken], [LAttrKey])+repairAttrTail [] [] _ _ = pure ([], [])+repairAttrTail (dotTok : dotToks) (key : keys) oldPrev newPrev = do+  let dotSpan = preserveGapSpan oldPrev (expectTokenSpan "attr path dot" dotTok) (L (getLoc newPrev) ())+  key' <- repairAttrKeyAt (preserveGapTarget dotSpan (getLoc key) dotSpan) key+  (dots', keys') <- repairAttrTail dotToks keys (getLoc key) key'+  pure (dotTok {annTokenPos = AnnSpan dotSpan} : dots', key' : keys')+repairAttrTail dots keys _ _ = liftEPResult (Left (IndexOutOfRange (length dots) (length keys)))++--------------------------------------------------------------------------------++repairInheritNames :: SrcSpan -> Maybe LExpr -> Maybe LExpr -> [LAttrKey] -> RepairM [LAttrKey]+repairInheritNames inheritSpan oldScope newScope names = third <$> foldM step (oldStartAnchor, startAnchor, []) names+  where+    third (_, _, repaired) = repaired+    startAnchor = maybe inheritSpan getLoc newScope+    oldStartAnchor = maybe inheritSpan getLoc oldScope+    step (prevOld, prevNew, repaired) name = do+      name' <- repairAttrKeyAt (preserveGapTarget prevOld (getLoc name) prevNew) name+      pure (getLoc name, getLoc name', repaired <> [name'])++--------------------------------------------------------------------------------++tokenSpanFromAnchor :: Text -> SrcSpan -> AnnToken -> SrcSpan+tokenSpanFromAnchor label anchor tok =+  case annTokenSrcSpan tok of+    Just span' -> span'+    Nothing -> case annTokenDelta tok of+      Just delta -> applyDeltaToAnchor anchor delta+      Nothing -> error (T.unpack label <> ": missing token span")++--------------------------------------------------------------------------------++repairExprAtSelect :: AnnSelect -> LAttrPath -> LAttrPath -> LExpr -> RepairM LExpr+repairExprAtSelect ann oldPath newPath oldDef =+  case aslOr ann of+    Just orTok ->+      let orSpan = preserveGapSpan (attrPathRenderSpan (unLoc oldPath)) (expectTokenSpan "select or" orTok) newPath+       in repairExprAt (preserveGapTarget (expectTokenSpan "select or" orTok) (getLoc oldDef) orSpan) oldDef+    Nothing -> repairExpr oldDef++--------------------------------------------------------------------------------++liftEPResult :: EPResult a -> RepairM a+liftEPResult = ReaderT . const++--------------------------------------------------------------------------------++repairSetPatCloseSpan :: AnnSetPatNode -> [LSetPatBinding] -> NixSetPatEllipses -> SrcSpan+repairSetPatCloseSpan ann bindings _ =+  case reverse bindings of+    lastBinding : _ ->+      let endCursor = advanceCursor (spanStartCursor (getLoc lastBinding)) (renderToText (unLoc lastBinding))+       in tokenSpanAt endCursor (aspCloseC ann)+    [] -> expectTokenSpan "set pattern close" (aspCloseC ann)
+ src/Nix/Lang/ExactPrint/Prepare/Types.hs view
@@ -0,0 +1,49 @@+-- | Result and error types used while preparing edited trees for exact printing.+module Nix.Lang.ExactPrint.Prepare.Types+  ( EPResult,+    EPError (..),+    BindingInsertPosition (..),+    ListInsertPosition (..),+  )+where++import Data.Text (Text)++--------------------------------------------------------------------------------++-- | Result type used by pure exact-print preparation operations.+--+-- The exact-print preparation pipeline is intentionally modeled as a pure @Either@ pipeline:+-- operations either produce a repaired tree or an 'EPError'.+type EPResult = Either EPError++-- | Errors raised by internal exact-print preparation operations.+data EPError+  = NotASet+  | NotALet+  | NotAList+  | NotABindingContainer+  | EmptyAttrPathEdit+  | NotANormalBinding Int+  | NegativeIndex Int+  | IndexOutOfRange Int Int+  | ParseBindingError Text+  | ParseExprError Text+  | ParseAttrKeyError Text+  deriving (Show, Eq)++--------------------------------------------------------------------------------++-- | Where to insert a binding in a set or @let@ binding list.+data BindingInsertPosition+  = InsertBindingAt Int+  | AppendBinding+  deriving (Show, Eq)++-- | Where to insert an element in a list expression.+data ListInsertPosition+  = InsertListElementAt Int+  | AppendListElement+  deriving (Show, Eq)++--------------------------------------------------------------------------------
+ src/Nix/Lang/ExactPrint/Prepare/Utils.hs view
@@ -0,0 +1,245 @@+{-# 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))
+ src/Nix/Lang/Lexer/Text.hs view
@@ -0,0 +1,52 @@+-- | Shared lexical character classes and string escaping helpers.+--+-- Parser and printer code both rely on this module so that identifier, path, and+-- string rules stay in sync.+module Nix.Lang.Lexer.Text+  ( escapedChars,+    escapeDoubleQuotedText,+    escapeIndentedText,+    reservedNames,+    isIdentChar,+    isPathChar,+    isSchemeChar,+    isUriChar,+  ) where++import Data.Char (isAlpha, isDigit)+import Data.Text (Text)+import qualified Data.Text as T++escapedChars :: [(Char, Char)]+escapedChars =+  [ ('n', '\n'),+    ('t', '\t'),+    ('r', '\r'),+    ('\\', '\\'),+    ('$', '$'),+    ('"', '"'),+    ('\'', '\''),+    ('.', '.'),+    ('{', '{')+  ]++escapeDoubleQuotedText :: Text -> Text+escapeDoubleQuotedText = foldr (.) id [T.replace (T.singleton char) (T.cons '\\' (T.singleton code)) | (code, char) <- escapedChars]++escapeIndentedText :: Text -> Text+escapeIndentedText = foldr (.) id [T.replace "${" "''${", T.replace "''" "'''"]++reservedNames :: [Text]+reservedNames = ["rec", "let", "in", "with", "inherit", "assert", "if", "then", "else"]++isIdentChar :: Char -> Bool+isIdentChar x = isAlpha x || isDigit x || x `elem` ['-', '_', '\'']++isPathChar :: Char -> Bool+isPathChar x = isAlpha x || isDigit x || x `elem` ['.', '_', '-', '+', '~']++isSchemeChar :: Char -> Bool+isSchemeChar x = isAlpha x || isDigit x || x `elem` ['+', '-', '.']++isUriChar :: Char -> Bool+isUriChar x = isAlpha x || isDigit x || x `elem` ['~', '!', '@', '$', '%', '&', '*', '-', '=', '_', '+', ':', ',', '.', '/', '?']
+ src/Nix/Lang/Outputable.hs view
@@ -0,0 +1,252 @@+-- | Basic pretty-printing for Nix AST values.+--+-- This is the simple renderer shared across the AST. It does not try to preserve+-- parsed layout, and it is less opinionated than 'Nix.Lang.RFCPrint'.+module Nix.Lang.Outputable where++import Data.Data (Proxy (..))+import Data.Text (Text)+import Nix.Lang.Lexer.Text+import Nix.Lang.Types+import Nix.Lang.Types.Ps+import Nix.Lang.Types.Syn (Syn)+import Nix.Lang.Utils+import Prettyprinter+import Prettyprinter.Render.Text (renderStrict)++class Outputable a where+  output :: a -> Doc ann++class OutputableNames p where+  outputVarName :: Proxy p -> NixVarName p -> Doc ann+  outputBinderName :: Proxy p -> NixBinderName p -> Doc ann+  outputAttrName :: Proxy p -> NixAttrName p -> Doc ann++instance OutputableNames Ps where+  outputVarName _ = pretty+  outputBinderName _ = pretty+  outputAttrName _ = pretty++instance OutputableNames Syn where+  outputVarName _ = pretty+  outputBinderName _ = pretty+  outputAttrName _ = pretty++renderToText :: (Outputable a) => a -> Text+renderToText = renderStrict . layoutPretty defaultLayoutOptions . output++extErr :: a+extErr = error "unimplemented for extension"++unwrapText :: UnXRec p => Proxy p -> XRec p Text -> Text+unwrapText = unXRec++unwrapVarName :: UnXRec p => Proxy p -> LNixVarName p -> NixVarName p+unwrapVarName = unXRec++unwrapBinderName :: UnXRec p => Proxy p -> LNixBinderName p -> NixBinderName p+unwrapBinderName = unXRec++unwrapAttrName :: UnXRec p => Proxy p -> LNixAttrName p -> NixAttrName p+unwrapAttrName = unXRec++unwrapLit :: UnXRec p => Proxy p -> LNixLit p -> NixLit p+unwrapLit = unXRec++unwrapExpr :: UnXRec p => Proxy p -> LNixExpr p -> NixExpr p+unwrapExpr = unXRec++unwrapString :: UnXRec p => Proxy p -> LNixString p -> NixString p+unwrapString = unXRec++unwrapPath :: UnXRec p => Proxy p -> LNixPath p -> NixPath p+unwrapPath = unXRec++unwrapAttrKey :: UnXRec p => Proxy p -> LNixAttrKey p -> NixAttrKey p+unwrapAttrKey = unXRec++unwrapAttrPath :: UnXRec p => Proxy p -> LNixAttrPath p -> NixAttrPath p+unwrapAttrPath = unXRec++unwrapBinding :: UnXRec p => Proxy p -> LNixBinding p -> NixBinding p+unwrapBinding = unXRec++unwrapFuncPat :: UnXRec p => Proxy p -> LNixFuncPat p -> NixFuncPat p+unwrapFuncPat = unXRec++unwrapBindings :: UnXRec p => Proxy p -> LNixBindings p -> [LNixBinding p]+unwrapBindings = unXRec++unwrapStringPart :: UnXRec p => Proxy p -> LNixStringPart p -> NixStringPart p+unwrapStringPart = unXRec++unwrapSetPatAs :: UnXRec p => Proxy p -> LNixSetPatAs p -> NixSetPatAs p+unwrapSetPatAs = unXRec++unwrapSetPatBinding :: UnXRec p => Proxy p -> LNixSetPatBinding p -> NixSetPatBinding p+unwrapSetPatBinding = unXRec++outputLitX :: UnXRec p => Proxy p -> LNixLit p -> Doc ann+outputLitX proxy lit = output (unwrapLit proxy lit)++outputExprX :: (OutputableNames p, UnXRec p) => Proxy p -> LNixExpr p -> Doc ann+outputExprX proxy expr = output (unwrapExpr proxy expr)++outputStringX :: (OutputableNames p, UnXRec p) => Proxy p -> LNixString p -> Doc ann+outputStringX proxy str = output (unwrapString proxy str)++outputPathX :: (OutputableNames p, UnXRec p) => Proxy p -> LNixPath p -> Doc ann+outputPathX proxy path = output (unwrapPath proxy path)++outputAttrKeyX :: (OutputableNames p, UnXRec p) => Proxy p -> LNixAttrKey p -> Doc ann+outputAttrKeyX proxy key = output (unwrapAttrKey proxy key)++outputAttrPathX :: (OutputableNames p, UnXRec p) => Proxy p -> LNixAttrPath p -> Doc ann+outputAttrPathX proxy path = output (unwrapAttrPath proxy path)++outputBindingX :: (OutputableNames p, UnXRec p) => Proxy p -> LNixBinding p -> Doc ann+outputBindingX proxy binding = output (unwrapBinding proxy binding)++outputFuncPatX :: (OutputableNames p, UnXRec p) => Proxy p -> LNixFuncPat p -> Doc ann+outputFuncPatX proxy pat = output (unwrapFuncPat proxy pat)++outputSetPatBindingX :: (OutputableNames p, UnXRec p) => Proxy p -> LNixSetPatBinding p -> Doc ann+outputSetPatBindingX proxy binding = output (unwrapSetPatBinding proxy binding)++outputSetPatAsX :: (OutputableNames p, UnXRec p) => Proxy p -> LNixSetPatAs p -> Doc ann+outputSetPatAsX proxy asPat = output (unwrapSetPatAs proxy asPat)++instance Outputable (NixLit p) where+  output (NixUri _ uri) = pretty uri+  output (NixInteger _ int) = pretty int+  output (NixFloat _ float) = pretty float+  output (NixBoolean _ bool) = if bool then "true" else "false"+  output (NixNull _) = "null"+  output (XNixLit _) = extErr++instance (OutputableNames p, UnXRec p) => Outputable (NixExpr p) where+  output = go+    where+      proxy = Proxy @p++      go (NixLit _ lit) = outputLitX proxy lit+      go (NixString _ str) = outputStringX proxy str+      go (NixVar _ x) = outputVarName proxy (unwrapVarName proxy x)+      go (NixPar _ x) = parens $ outputExprX proxy x+      go (NixNegApp _ x) = "-" <> outputExprX proxy x+      go (NixNotApp _ x) = "!" <> outputExprX proxy x+      go (NixApp _ f x) = hsep [outputExprX proxy f, outputExprX proxy x]+      go (NixBinApp _ op x y) =+        hsep+          [ outputExprX proxy x,+            pretty $ showBinOP op,+            outputExprX proxy y+          ]+      go (NixHasAttr _ x p) = hsep [outputExprX proxy x, "?", outputAttrPathX proxy p]+      go (NixPath _ p) = outputPathX proxy p+      go (NixEnvPath _ p) = "<" <> pretty (unwrapText proxy p) <> ">"+      go (NixLam _ pat x) = nestedSep [outputFuncPatX proxy pat <> ":", outputExprX proxy x]+      go (NixList _ xs) = case xs of+        [] -> "[]"+        _ -> nestedSep $ "[" : (outputExprX proxy <$> xs) <> ["]"]+      go (NixSet _ NixSetRecursive bindings) = case unwrapBindings proxy bindings of+        [] -> "rec {}"+        xs -> nestedSep $ "rec {" : (outputBindingX proxy <$> xs) <> ["}"]+      go (NixSet _ NixSetNonRecursive bindings) = case unwrapBindings proxy bindings of+        [] -> "{}"+        xs -> nestedSep $ "{" : (outputBindingX proxy <$> xs) <> ["}"]+      go (NixSelect _ x p mx) = outputExprX proxy x <> "." <> outputAttrPathX proxy p <> alt+        where+          alt = maybe mempty ((" or " <>) . outputExprX proxy) mx+      go (NixLet _ bindings x) =+        group $+          vsep ["let", indent 2 (vsep $ outputBindingX proxy <$> unwrapBindings proxy bindings), "in " <> outputExprX proxy x]+      go (NixIf _ cond t f) = nestedSep ["if " <> outputExprX proxy cond, "then " <> outputExprX proxy t, "else " <> outputExprX proxy f]+      go (NixWith _ s x) = nestedSep ["with " <> outputExprX proxy s <> ";", outputExprX proxy x]+      go (NixAssert _ s x) = nestedSep ["assert " <> outputExprX proxy s <> ";", outputExprX proxy x]+      go (XNixExpr _) = extErr++nestedSep :: [Doc ann] -> Doc ann+nestedSep = group . nest 2 . vsep++outputStringPart :: forall p ann. (OutputableNames p, UnXRec p) => Proxy p -> (Text -> Text) -> NixStringPart p -> Doc ann+outputStringPart _ escape (NixStringLiteral _ text) = pretty $ escape text+outputStringPart proxy _ (NixStringInterpol _ expr) = "${" <> outputExprX proxy expr <> "}"+outputStringPart _ _ (XNixStringPart _) = extErr++instance (OutputableNames p, UnXRec p) => Outputable (NixString p) where+  output = go+    where+      proxy = Proxy @p++      go (NixDoubleQuotesString _ parts) = dquotes $ outputParts $ fmap (unwrapStringPart proxy) parts+        where+          outputParts = mconcat . fmap (outputStringPart proxy escapeDoubleQuotedText)+      go (NixDoubleSingleQuotesString _ parts) = vcat ["''", nest 2 $ outputParts $ fmap (unwrapStringPart proxy) parts, "''"]+        where+          outputParts = mconcat . fmap (outputStringPart proxy escapeIndentedText)+      go (XNixString _) = extErr++instance (OutputableNames p, UnXRec p) => Outputable (NixAttrKey p) where+  output = go+    where+      proxy = Proxy @p++      go (NixStaticAttrKey _ x) = outputAttrName proxy (unwrapAttrName proxy x)+      go (NixDynamicStringAttrKey _ parts) = dquotes $ mconcat $ outputStringPart proxy escapeDoubleQuotedText . unwrapStringPart proxy <$> parts+      go (NixDynamicInterpolAttrKey _ expr) = "${" <> outputExprX proxy expr <> "}"+      go (XNixNixAttrKey _) = extErr++instance (OutputableNames p, UnXRec p) => Outputable (NixAttrPath p) where+  output (NixAttrPath _ keys) = hcat . punctuate dot $ outputAttrKeyX (Proxy @p) <$> keys++instance (OutputableNames p, UnXRec p) => Outputable (NixPath p) where+  output = go+    where+      proxy = Proxy @p++      go (NixLiteralPath _ p) = pretty p+      go (NixInterpolPath _ parts) = hcat $ outputStringPart proxy id . unwrapStringPart proxy <$> parts+      go (XNixPath _) = extErr++instance (OutputableNames p, UnXRec p) => Outputable (NixBinding p) where+  output = go+    where+      proxy = Proxy @p++      go (NixNormalBinding _ p x) = hsep [outputAttrPathX proxy p, "=", outputExprX proxy x] <> ";"+      go (NixInheritBinding _ mScope names) = "inherit" <> outputScope <> outputNames names <> ";"+        where+          outputScope = maybe mempty ((" " <>) . outputExprX proxy) mScope+          outputNames [] = mempty+          outputNames ns = " " <> (align . fillSep $ outputAttrKeyX proxy <$> ns)+      go (XNixBinding _) = extErr++instance (OutputableNames p, UnXRec p) => Outputable (NixSetPatBinding p) where+  output NixSetPatBinding {..} = outputBinderName proxy (unwrapBinderName proxy nspbVar) <> outputDefault+    where+      proxy = Proxy @p+      outputDefault = maybe mempty ((" ? " <>) . outputExprX proxy) nspbDefault++instance (OutputableNames p, UnXRec p) => Outputable (NixSetPatAs p) where+  output NixSetPatAs {..} = case nspaLocation of+    NixSetPatAsLeading -> outputNm <> "@"+    NixSetPatAsTrailing -> "@" <> outputNm+    where+      proxy = Proxy @p+      outputNm = outputBinderName proxy $ unwrapBinderName proxy nspaVar++instance (OutputableNames p, UnXRec p) => Outputable (NixFuncPat p) where+  output = go+    where+      proxy = Proxy @p++      go (NixVarPat _ x) = outputBinderName proxy (unwrapBinderName proxy x)+      go (NixSetPat _ ellipses mAs params) = case fmap (nspaLocation . unwrapSetPatAs proxy) mAs of+        Just NixSetPatAsLeading -> outputAs <> outputParams+        Just NixSetPatAsTrailing -> outputParams <> outputAs+        Nothing -> outputParams+        where+          outputAs = maybe mempty (outputSetPatAsX proxy) mAs+          outputParams = encloseSep "{ " " }" ", " ((outputSetPatBindingX proxy <$> params) <> ["..." | ellipses == NixSetPatIsEllipses])+      go (XNixFuncPat _) = extErr
+ src/Nix/Lang/Parser.hs view
@@ -0,0 +1,904 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Parser for Nix source text.+--+-- This is where raw Nix source first becomes an annotated tree.+--+-- The parser does more than produce syntax: it also records source spans,+-- comments, and token ownership, which is what makes later exact printing and+-- layout-preserving edits possible.+--+-- The usual entry points are 'nixFile' for a full file and 'nixExpr' for a+-- standalone expression.+--+-- Example:+--+-- @+-- import Text.Megaparsec (eof)+-- import Nix.Lang.Parser (nixExpr, runNixParser)+--+-- parsed = runNixParser (nixExpr <* eof) "<expr>" "x: x + 1"+-- @+module Nix.Lang.Parser where++import Control.Monad (forM, guard, msum, unless, void, when)+import Control.Monad.Combinators.Expr+import Control.Monad.State.Strict+import Data.Char (isAlpha, isDigit, isSpace)+import Data.Data (Data)+import Data.Maybe (fromJust, maybeToList)+import Data.Text (Text)+import qualified Data.Text as T+import Data.Void (Void)+import Nix.Lang.Annotation+import Nix.Lang.Lexer.Text+import Nix.Lang.Span+import Nix.Lang.Types+import Nix.Lang.Types.Ps+import Nix.Lang.Utils+import Text.Megaparsec hiding (State, Token, token)+import Text.Megaparsec.Char+import qualified Text.Megaparsec.Char.Lexer as L++--------------------------------------------------------------------------------++data PState = PState+  { psCommentQueue :: [Located Comment],+    psLoc :: (Int, Int)+  }+  deriving (Show, Eq, Data)++type Parser = ParsecT Void Text (State PState)++mkPState :: PState+mkPState = PState [] (1, 1)++runNixParser :: Parser a -> String -> Text -> (Either (ParseErrorBundle Text Void) a, PState)+runNixParser p f t = runState (runParserT p f t) mkPState++--------------------------------------------------------------------------------++takeCommentsBefore :: SrcSpan -> Parser [Located Comment]+takeCommentsBefore s = state $ \ps@PState {..} ->+  let (inside, outside) = foldr step ([], []) psCommentQueue+      step c@(L l _) (ins, outs)+        | commentEndsBefore l s = (c : ins, outs)+        | otherwise = (ins, c : outs)+   in (reverse inside, ps {psCommentQueue = outside})++takeRemainingComments :: Parser [Located Comment]+takeRemainingComments = state $ \ps@PState {..} ->+  (reverse psCommentQueue, ps {psCommentQueue = []})++mkAnnCommon :: SrcSpan -> Parser AnnCommon+mkAnnCommon l = do+  comments <- takeCommentsBefore l+  pure $ AnnCommon (NodeComments comments []) (AnnSpan l)++mkEmptyAnnCommon :: SrcSpan -> AnnCommon+mkEmptyAnnCommon l = AnnCommon emptyComments (AnnSpan l)++addPendingComment ::+  Located Comment ->+  Parser ()+addPendingComment c = modify' $ \ps ->+  ps+    { psCommentQueue = c : psCommentQueue ps+    }++commentEndsBefore :: SrcSpan -> SrcSpan -> Bool+commentEndsBefore comment anchor+  | srcSpanFilename comment /= srcSpanFilename anchor = False+  | otherwise =+      (srcSpanEndLine comment, srcSpanEndColumn comment)+        <= (srcSpanStartLine anchor, srcSpanStartColumn anchor)++attachFollowingComments :: (HasAnnCommon a) => [Located Comment] -> a -> a+attachFollowingComments comments ann+  | null comments = ann+  | otherwise =+      let common = getAnnCommon ann+          nodeComments = acComments common+          common' =+            common+              { acComments =+                  nodeComments+                    { followingComments = followingComments nodeComments <> comments+                    }+              }+       in setAnnCommon common' ann++attachCommentsBeforeAnchor :: (HasAnnCommon a) => SrcSpan -> a -> Parser a+attachCommentsBeforeAnchor anchor ann = do+  comments <- takeCommentsBefore anchor+  pure $ attachFollowingComments comments ann++{-# INLINE sourcePosToLoc #-}+sourcePosToLoc :: SourcePos -> (Int, Int)+sourcePosToLoc pos = (unPos (sourceLine pos), unPos (sourceColumn pos))++putLoc :: (Int, Int) -> Parser ()+putLoc loc = modify' $ \ps -> ps {psLoc = loc}++-- | Invariant: the use of located without consuming whitespace must manually updateLoc+updateLoc :: Parser ()+updateLoc = getSourcePos >>= putLoc . sourcePosToLoc++--------------------------------------------------------------------------------++located' :: Bool -> Bool -> Parser a -> Parser (Located a)+located' includeWhiteSpace allocateComments p = do+  start <- getSourcePos+  x <- p+  end <-+    if not includeWhiteSpace+      then gets psLoc+      else sourcePosToLoc <$> getSourcePos+  let s =+        mkSrcSpan+          (sourceName start)+          (unPos $ sourceLine start, unPos $ sourceColumn start)+          end+  when allocateComments $ pure ()+  pure $ L s x++located :: Parser a -> Parser (Located a)+located = located' False False++locatedC :: Parser a -> Parser (Located a)+locatedC = located++eatLineComment :: Parser ()+eatLineComment = do+  c <- located'+    True+    False+    $ do+      _ <- string "#"+      takeWhileP (Just "chars") (\x -> x /= '\n' && x /= '\r')+  addPendingComment $ LineComment <$> c++eatBlockComment :: Parser ()+eatBlockComment = do+  c <- located' True False $ do+    _ <- string "/*"+    content <- manyTill anySingle $ string "*/"+    pure $ T.pack content+  addPendingComment $ BlockComment <$> c++ws :: Parser ()+ws = updateLoc >> L.space space1 eatLineComment eatBlockComment++lexeme :: Parser a -> Parser a+lexeme = L.lexeme ws++symbol :: Text -> Parser Text+symbol = L.symbol ws++-- | Not including whitespace+symbol' :: Text -> Parser Text+symbol' = string++token :: Ann -> Bool -> Parser Text+token tk includeWhitespace = (if includeWhitespace then symbol else symbol') $ fromJust $ showToken tk++--------------------------------------------------------------------------------++betweenToken :: Ann -> Ann -> Bool -> Bool -> Parser a -> Parser (Located a)+betweenToken t1 t2 includeWhitespaceOpen includeWhitespaceClose p = do+  (L l (_, x, _)) <-+    locatedC $+      (,,)+        <$> located (token t1 includeWhitespaceOpen <* unless includeWhitespaceOpen updateLoc)+        <*> p+        <*> located (token t2 includeWhitespaceClose <* unless includeWhitespaceClose updateLoc)+  pure $ L l x++antiquote :: Bool -> Bool -> Parser a -> Parser (Located a)+antiquote = betweenToken AnnInterpolOpen AnnInterpolClose++legalReserved :: Parser ()+legalReserved =+  lookAhead $+    void eof+      <|> void+        ( satisfy $+            \x -> not $ isIdentChar x || isPathChar x+        )++reservedKw :: Text -> Parser (Located Text)+reservedKw x = try $ located $ lexeme $ symbol' x <* legalReserved++--------------------------------------------------------------------------------++litBoolean :: Parser (NixExpr Ps)+litBoolean = litTrue <|> litFalse+  where+    litTrue = do+      (L l _) <- reservedKw "true"+      common <- mkAnnCommon l+      pure $ NixLit common $ L l $ NixBoolean NoExtF True+    litFalse = do+      (L l _) <- reservedKw "false"+      common <- mkAnnCommon l+      pure $ NixLit common $ L l $ NixBoolean NoExtF False++litNull :: Parser (NixExpr Ps)+litNull = do+  (L l _) <- reservedKw "null"+  common <- mkAnnCommon l+  pure $ NixLit common $ L l $ NixNull NoExtF++litFloat :: Parser (NixExpr Ps)+litFloat = do+  (L l f) <- located $ lexeme L.float+  common <- mkAnnCommon l+  pure $ NixLit common $ L l $ NixFloat NoExtF f++litInteger :: Parser (NixExpr Ps)+litInteger = do+  (L l f) <- located $ lexeme L.decimal+  common <- mkAnnCommon l+  pure $ NixLit common $ L l $ NixInteger NoExtF f++litUri :: Parser (NixExpr Ps)+litUri = do+  (L l uri) <- located $ lexeme $ do+    h <- letterChar+    scheme <- takeWhileP (Just "scheme") isSchemeChar+    colon <- char ':'+    rest <- takeWhile1P (Just "uri") isUriChar+    pure $ T.cons h scheme <> T.cons colon rest+  common <- mkAnnCommon l+  pure $ NixLit common $ L l $ NixUri NoExtF uri++--------------------------------------------------------------------------------+slash :: Parser Char+slash = char '/' <* notFollowedBy (satisfy $ \x -> x == '/' || isSpace x || x == '>')++nixEnvPath :: Parser (NixExpr Ps)+nixEnvPath =+  lexeme $+    betweenToken AnnEnvPathOpen AnnEnvPathClose False False (lookAhead (satisfy (/= '/')) >> T.pack <$> many (satisfy isPathChar <|> slash)) >>= \(L l p) -> do+      common <- mkAnnCommon l+      let ann = AnnEnvPathNode common (parsedAnnToken AnnEnvPathOpen l) (parsedAnnToken AnnEnvPathClose l)+      pure $ NixEnvPath ann (L l p)++literalPath :: Parser (NixExpr Ps)+literalPath =+  locatedC+    ( NixLiteralPath NoExtF+        <$> ( do+                u <- takeWhileP (Just "path") isPathChar+                r <- some (T.cons <$> slash <*> takeWhile1P (Just "path") isPathChar)+                pure $ T.concat $ u : r+            )+        <* updateLoc+    )+    >>= \(L l p) -> do+      common <- mkAnnCommon l+      pure $ NixPath (AnnPathNode common) (L l p)++interpolPath :: Parser (NixExpr Ps)+interpolPath =+  located path >>= \(L l p) -> do+    let parts = mergeStringPartLiteral p+    guard $ slashInFirstPart parts+    common <- mkAnnCommon l+    pure $ NixPath (AnnPathNode common) $ L l $ NixInterpolPath NoExtF parts+  where+    -- at least one slash before interpolation+    slashInFirstPart (L _ (NixStringLiteral _ s) : _) = T.elem '/' s+    slashInFirstPart _ = False+    lit = located $ NixStringLiteral NoExtF . T.pack <$> some (notFollowedBy (char '$') >> satisfy isPathChar) <* updateLoc+    interpol = located nixStringPartInterpol+    slash' = located $ NixStringLiteral NoExtF . T.singleton <$> slash <* updateLoc+    path = (<>) <$> (maybeToList <$> optional (lit <|> interpol)) <*> (some (lit <|> interpol <|> slash') <* notFollowedBy slash')++nixPath :: Parser (NixExpr Ps)+nixPath = lexeme $ try (literalPath <* notFollowedBy "$") <|> (interpolPath <* notFollowedBy "*")++pathStartsHere :: Parser Bool+pathStartsHere =+  option False . try . lookAhead $ do+    _ <- takeWhile1P (Just "path prefix") isPathChar+    void slash+    pure True++uriStartsHere :: Parser Bool+uriStartsHere =+  option False . try . lookAhead $ do+    _ <- letterChar+    _ <- takeWhileP (Just "scheme") isSchemeChar+    _ <- char ':'+    _ <- takeWhile1P (Just "uri") isUriChar+    pure True++--------------------------------------------------------------------------------+ident :: Parser Text+ident = try $+  lexeme $ do+    _ <- lookAhead (satisfy (\x -> isAlpha x || x == '_'))+    x <- takeWhile1P (Just "ident") isIdentChar+    when (x `elem` reservedNames) $+      fail $+        "'" <> T.unpack x <> "' is a reserved name"+    pure x++nixVar :: Parser (NixExpr Ps)+nixVar =+  located ident >>= \(L l x) -> do+    common <- mkAnnCommon l+    pure $ NixVar common (L l x)++--------------------------------------------------------------------------------++escapedChar :: Parser Char+escapedChar = choice [char code >> pure r | (code, r) <- escapedChars] <|> anySingle++mergeStringPartLiteral :: [LNixStringPart Ps] -> [LNixStringPart Ps]+mergeStringPartLiteral [] = []+mergeStringPartLiteral (L l1 (NixStringLiteral _ t1) : L l2 (NixStringLiteral _ t2) : xs) =+  mergeStringPartLiteral $ L (l1 `combineSrcSpans` l2) (NixStringLiteral NoExtF $ t1 <> t2) : xs+mergeStringPartLiteral (x : xs) = x : mergeStringPartLiteral xs++nixStringPartLiteral :: Parser Text -> Parser Text -> Parser (NixStringPart Ps) -> Parser (NixStringPart Ps)+nixStringPartLiteral end escapeStart escape =+  (NixStringLiteral NoExtF . T.singleton <$> char '$')+    <|> escape+    <|> (NixStringLiteral NoExtF . T.pack <$> some (notFollowedBy (void end <|> void (char '$') <|> void escapeStart) >> anySingle))++nixStringPartInterpol :: Parser (NixStringPart Ps)+nixStringPartInterpol = NixStringInterpol NoExtF <$> antiquote True False nixExpr++-- | Get source text without consuming it+stringSourceText :: Parser Text -> Parser Text -> Parser Text -> Parser SourceText+stringSourceText start end escapeStart =+  lookAhead $+    between start end $+      fmap (SourceText . T.concat) $+        many $+          try+            ( (\a b -> a <> T.singleton b)+                <$> escapeStart+                <*> anySingle+            )+            <|> T.singleton+            <$> (notFollowedBy (end <|> escapeStart) >> anySingle)++doubleQuotesString :: Parser (NixExpr Ps)+doubleQuotesString =+  betweenToken AnnDoubleQuote AnnDoubleQuote False False lit >>= \(L l s) -> do+    common <- mkAnnCommon l+    pure $ NixString (AnnStringNode common) (L l s)+  where+    lit = do+      (src, parsedParts) <- match doubleQuotedStringParts+      pure $ NixDoubleQuotesString (SourceText src) (mergeStringPartLiteral parsedParts)++doubleQuotedStringParts :: Parser [LNixStringPart Ps]+doubleQuotedStringParts = many (located $ ((NixStringLiteral NoExtF <$> string "$$") <|> nixStringPartInterpol <|> nixStringPartLiteral "\"" "\\" doubleQuotedStringEscape) <* updateLoc)++doubleQuotedStringEscape :: Parser (NixStringPart Ps)+doubleQuotedStringEscape = NixStringLiteral NoExtF . T.singleton <$> (char '\\' >> escapedChar)++doubleSingleQuotesString :: Parser (NixExpr Ps)+doubleSingleQuotesString = s >>= expr+  where+    -- we can't use stringSourceText here since '' can escape ''+    s =+      lookAhead $+        between (string "''") (string "''") $+          fmap (SourceText . T.concat) $+            many $+              msum+                [ try $ T.snoc <$> string "''" <*> (char '$' <|> char '\''),+                  notFollowedBy ("''" <* notFollowedBy "\\") >> T.singleton <$> anySingle+                ]+    escape =+      try $+        NixStringLiteral NoExtF+          <$> ( string "''"+                  >> ( ((char '\'' >> pure "''") <|> (char '$' >> pure "$"))+                         <|> (char '\\' >> (T.singleton <$> escapedChar))+                     )+              )+    -- @$${@ does not indicate an interpolation, so we try to consume $$ first+    parts = many (located $ ((NixStringLiteral NoExtF <$> string "$$") <|> nixStringPartInterpol <|> nixStringPartLiteral "''" "''" escape) <* updateLoc)+    lit src = fmap (NixDoubleSingleQuotesString src . mergeStringPartLiteral) parts+    expr src =+      betweenToken AnnDoubleSingleQuotes AnnDoubleSingleQuotes False False (lit src) >>= \(L l str) -> do+        common <- mkAnnCommon l+        pure $ NixString (AnnStringNode common) (L l str)++nixString :: Parser (NixExpr Ps)+nixString = lexeme $ doubleQuotesString <|> doubleSingleQuotesString++--------------------------------------------------------------------------------++nixPar :: Parser (NixExpr Ps)+nixPar = do+  lp <- open+  comments <- takeCommentsBefore (getLoc lp)+  x <- located nixExpr+  rp <- close+  let l = getLoc lp `combineSrcSpans` getLoc rp+      common = AnnCommon (NodeComments comments []) (AnnSpan l)+  ann <- attachCommentsBeforeAnchor (getLoc rp) $ AnnParNode common (parsedAnnToken AnnOpenP (getLoc lp)) (parsedAnnToken AnnCloseP (getLoc rp))+  pure $ NixPar ann x+  where+    open = located (symbol' "(" <* updateLoc) <* ws+    close = located (symbol' ")" <* updateLoc) <* ws++--------------------------------------------------------------------------------++dot :: Parser Text+dot = try $ lexeme $ token AnnDot False <* notFollowedBy ("/" <|> ".")++attrKey :: Parser (NixAttrKey Ps)+attrKey = dynamicString <|> dynamicInterpol <|> static+  where+    static = NixStaticAttrKey NoExtF <$> located ident+    dynamicString =+      lexeme (betweenToken AnnDoubleQuote AnnDoubleQuote False False (mergeStringPartLiteral <$> doubleQuotedStringParts)) >>= \(L _ x) ->+        pure $ NixDynamicStringAttrKey NoExtF x+    dynamicInterpol =+      NixDynamicInterpolAttrKey NoExtF+        <$> antiquote True True nixExpr++attrPath :: Bool -> Parser (NixAttrPath Ps)+attrPath dotFirst =+  locatedC+    ( do+        mdot <- if dotFirst then pure <$> located dot else pure []+        h <- located attrKey+        (rd, ra) <- fmap unzip $ many $ (,) <$> located dot <*> located attrKey+        pure (fmap getLoc $ mdot <> rd, h : ra)+    )+    >>= \(L l (ld, a)) -> do+      common <- mkAnnCommon l+      pure $ NixAttrPath (AnnAttrPath common (parsedAnnToken AnnDot <$> ld)) a++--------------------------------------------------------------------------------++inherit :: Parser (NixBinding Ps)+inherit =+  locatedC ((,,,) <$> kw <*> set <*> keys <*> end) >>= \(L l (a, b, c, d)) -> do+    common <- mkAnnCommon l+    let ann = AnnInheritBinding common (parsedAnnToken AnnInherit (getLoc a)) (parsedAnnToken AnnSemicolon (getLoc d))+    pure $ NixInheritBinding ann b c+  where+    kw = try $ located $ symbol' "inherit" <* (legalReserved >> ws)+    set = optional $ located nixPar+    keys = many $ located attrKey+    end = located $ symbol ";"++normal :: Parser (NixBinding Ps)+normal =+  locatedC ((,,,) <$> path <*> eq <*> expr <*> end) >>= \(L l (a, b, c, d)) -> do+    common <- mkAnnCommon l+    let ann = AnnNormalBinding common (parsedAnnToken AnnAssign (getLoc b)) (parsedAnnToken AnnSemicolon (getLoc d))+    pure $ NixNormalBinding ann a c+  where+    path = located $ attrPath False+    eq = located $ symbol "="+    expr = located nixExpr+    end = located $ symbol ";"++nixBinding :: Parser (NixBinding Ps)+nixBinding = inherit <|> normal++--------------------------------------------------------------------------------++nixSet :: Parser (NixExpr Ps)+nixSet = do+  a <- kw+  b <- open+  comments <- takeCommentsBefore (maybe (getLoc b) getLoc a)+  c <- bindings+  d <- close+  let l = maybe (getLoc b) getLoc a `combineSrcSpans` getLoc d+      common = AnnCommon (NodeComments comments []) (AnnSpan l)+  ann <-+    attachCommentsBeforeAnchor (getLoc d) $+      AnnSet common (parsedAnnToken AnnRec . getLoc <$> a) (parsedAnnToken AnnOpenC (getLoc b)) (parsedAnnToken AnnCloseC (getLoc d))+  pure $ NixSet ann (maybe NixSetNonRecursive (const NixSetRecursive) a) c+  where+    kw = optional $ reservedKw "rec"+    open = located (symbol' "{" <* updateLoc) <* ws+    close = located (symbol' "}" <* updateLoc) <* ws+    bindings = located' True False $ many $ located nixBinding <* updateLoc++--------------------------------------------------------------------------------++varPat :: Parser (NixFuncPat Ps)+varPat =+  (try litUri >> fail "unexpected uri")+    <|> ( located ident >>= \li -> do+            let l = getLoc li+            common <- mkAnnCommon l+            let ann = AnnVarPat common (getLoc li)+            pure $ NixVarPat ann li+        )++-- Note: this is not identical to @pat $ try bodyWithLeading <|> bodyWithTrailing@+setPat :: Parser (NixFuncPat Ps)+setPat = try (pat bodyWithLeading) <|> pat bodyWithTrailing+  where+    leadingAs :: Parser (Located (Located (), NixSetPatAs Ps))+    leadingAs = do+      L l (i, a) <- located $ (,) <$> located ident <*> located (void $ symbol "@")+      common <- mkAnnCommon l+      pure $ L l (a, NixSetPatAs (AnnSetPatAs common (parsedAnnToken AnnAt (getLoc a))) NixSetPatAsLeading i)+    trailingAs :: Parser (Located (Located (), NixSetPatAs Ps))+    trailingAs = do+      L l (a, i) <- located $ (,) <$> located (void $ symbol "@") <*> located ident+      common <- mkAnnCommon l+      pure $ L l (a, NixSetPatAs (AnnSetPatAs common (parsedAnnToken AnnAt (getLoc a))) NixSetPatAsTrailing i)+    bind = located $ (,) <$> located ident <*> optional ((,) <$> located (void $ symbol "?") <*> located nixExpr)+    ellipsis = located $ void $ symbol "..."+    openBrace = located (symbol' "{" <* updateLoc) <* ws+    closeBrace = located (symbol' "}" <* updateLoc) <* ws+    -- ([bind], [comma])+    go ::+      ([Located (Located Text, Maybe (Located (), LNixExpr Ps))], [Located ()]) ->+      -- ([bind], [comma], ellipsis)+      Parser ([Located (Located Text, Maybe (Located (), LNixExpr Ps))], [Located ()], Maybe (Located ()))+    go (bs, cs) = ((reverse bs, reverse cs,) . pure <$> ellipsis) <|> go1+      where+        go1 = option (reverse bs, reverse cs, Nothing) $ do+          b <- bind+          let bs1 = b : bs+          option (reverse bs1, reverse cs, Nothing) $ do+            c <- located $ void $ symbol ","+            go (bs1, c : cs)+    body ::+      Parser+        ( Located+            ( Located Text,+              ( [Located (Located Text, Maybe (Located (), LNixExpr Ps))],+                [Located ()],+                Maybe (Located ())+              ),+              Located Text+            )+        )+    body = locatedC $ (,,) <$> openBrace <*> go mempty <*> closeBrace+    bodyWithLeading = (,) <$> (Just <$> leadingAs) <*> body+    bodyWithTrailing = flip (,) <$> body <*> optional trailingAs+    pat ::+      -- (asPattern, [bind], [comma], ellipsis)+      Parser+        ( Maybe (Located (Located (), NixSetPatAs Ps)),+          Located+            ( Located Text,+              ( [Located (Located Text, Maybe (Located (), LNixExpr Ps))],+                [Located ()],+                Maybe (Located ())+              ),+              Located Text+            )+        ) ->+      Parser (NixFuncPat Ps)+    pat x = do+      (L l (mas, L _lBody (lo, (bs, cs, me), lcBody))) <- located x++      -- as pattern+      ras <- case mas of+        Just (L al (_, as)) -> do+          pure $ Just $ L al as+        Nothing -> pure Nothing++      -- ellipsis+      re <- case me of+        Just _ -> pure NixSetPatIsEllipses+        _ -> pure NixSetPatNotEllipses++      -- bindings+      rb <- forM bs $ \(L lb (i, mDefault)) -> do+        (rQuestion, rDef) <- case mDefault of+          Just (q, def) -> pure (Just (parsedAnnToken AnnQuestion (getLoc q)), Just def)+          _ -> pure (Nothing, Nothing)+        commonBinding <- mkAnnCommon lb+        pure $ L lb $ NixSetPatBinding (AnnSetPatBinding commonBinding rQuestion) i rDef++      common <- mkAnnCommon l+      let ann =+            AnnSetPatNode+              common+              (parsedAnnToken AnnOpenC (getLoc lo))+              (parsedAnnToken AnnCloseC (getLoc lcBody))+              (parsedAnnToken AnnEllipsis . getLoc <$> me)+              (parsedAnnToken AnnComma . getLoc <$> cs)+      pure $ NixSetPat ann re ras rb++nixFuncPat :: Parser (NixFuncPat Ps)+nixFuncPat = try setPat <|> varPat++nixLam :: Parser (NixExpr Ps)+nixLam = try $ do+  pat <- located nixFuncPat+  colon <- located (symbol ":")+  body <- located nixExpr+  let l = getLoc pat `combineSrcSpans` getLoc body+  common <- mkAnnCommon l+  pure $ NixLam (AnnLamNode common (parsedAnnToken AnnColon (getLoc colon))) pat body++--------------------------------------------------------------------------------++nixList :: Parser (NixExpr Ps)+nixList = do+  ls <- open+  comments <- takeCommentsBefore (getLoc ls)+  xs <- many (located nixTerm)+  rs <- close+  let l = getLoc ls `combineSrcSpans` getLoc rs+      common = AnnCommon (NodeComments comments []) (AnnSpan l)+  ann <- attachCommentsBeforeAnchor (getLoc rs) $ AnnListNode common (parsedAnnToken AnnOpenS (getLoc ls)) (parsedAnnToken AnnCloseS (getLoc rs))+  pure $ NixList ann xs+  where+    open = located (symbol' "[" <* updateLoc) <* ws+    close = located (symbol' "]" <* updateLoc) <* ws++--------------------------------------------------------------------------------++nixIf :: Parser (NixExpr Ps)+nixIf = do+  kif <- kwIf+  comments <- takeCommentsBefore (getLoc kif)+  op <- located nixExpr+  kth <- kwThen+  e1 <- located nixExpr+  kel <- kwElse+  e2 <- located nixExpr+  let l = getLoc kif `combineSrcSpans` getLoc e2+      common = AnnCommon (NodeComments comments []) (AnnSpan l)+      ann = AnnIfNode common (parsedAnnToken AnnIf (getLoc kif)) (parsedAnnToken AnnThen (getLoc kth)) (parsedAnnToken AnnElse (getLoc kel))+  pure $ NixIf ann op e1 e2+  where+    kwIf = reservedKw "if"+    kwThen = reservedKw "then"+    kwElse = reservedKw "else"++--------------------------------------------------------------------------------++nixWith :: Parser (NixExpr Ps)+nixWith = do+  kwi <- kw+  comments <- takeCommentsBefore (getLoc kwi)+  e1 <- located nixExpr+  semicolon <- sem+  e2 <- located nixExpr+  let l = getLoc kwi `combineSrcSpans` getLoc e2+      common = AnnCommon (NodeComments comments []) (AnnSpan l)+      ann = AnnWithNode common (parsedAnnToken AnnWith (getLoc kwi)) (parsedAnnToken AnnSemicolon (getLoc semicolon))+  pure $ NixWith ann e1 e2+  where+    kw = reservedKw "with"+    sem = located $ symbol ";"++--------------------------------------------------------------------------------++nixAssert :: Parser (NixExpr Ps)+nixAssert = do+  kas <- ka+  comments <- takeCommentsBefore (getLoc kas)+  e1 <- located nixExpr+  semicolon <- sem+  e2 <- located nixExpr+  let l = getLoc kas `combineSrcSpans` getLoc e2+      common = AnnCommon (NodeComments comments []) (AnnSpan l)+      ann = AnnAssertNode common (parsedAnnToken AnnAssert (getLoc kas)) (parsedAnnToken AnnSemicolon (getLoc semicolon))+  pure $ NixAssert ann e1 e2+  where+    ka = reservedKw "assert"+    sem = located $ symbol ";"++--------------------------------------------------------------------------------++nixLet :: Parser (NixExpr Ps)+nixLet = do+  kl <- kLet+  comments <- takeCommentsBefore (getLoc kl)+  bs <- bindings+  ki <- kIn+  e <- located nixExpr+  let l = getLoc kl `combineSrcSpans` getLoc e+      common = AnnCommon (NodeComments comments []) (AnnSpan l)+      ann = AnnLetNode common (parsedAnnToken AnnLet (getLoc kl)) (parsedAnnToken AnnIn (getLoc ki))+  pure $ NixLet ann bs e+  where+    kLet = reservedKw "let"+    kIn = reservedKw "in"+    bindings = located $ many $ located nixBinding++--------------------------------------------------------------------------------++-- | Term' is expr without operators (including selection)+nixTerm' :: Parser (NixExpr Ps)+nixTerm' =+  lookAhead anySingle >>= \case+    '(' -> nixPar+    '{' -> nixSet+    '[' -> nixList+    '/' -> try nixPath+    '<' -> try nixEnvPath+    '\"' -> nixString+    '\'' -> nixString+    '.' -> nixPath+    '~' -> nixPath+    '_' -> do+      isPath <- pathStartsHere+      if isPath then try nixPath <|> nixVar else nixVar+    x+      | isDigit x -> do+          isPath <- pathStartsHere+          if isPath+            then nixPath+            else try litFloat <|> litInteger+      | isAlpha x -> do+          isPath <- pathStartsHere+          if isPath+            then if x == 'r' then try nixSet <|> try nixPath <|> alphaLike x else try nixPath <|> alphaLike x+            else alphaLike x+    _ -> fail "unexpected token"+  where+    alphaLike x+      | x == 'r' = try nixSet <|> keywordOrUriOrVar x+      | otherwise = keywordOrUriOrVar x++    keywordOrUriOrVar x+      | x == 'n' = litNull <|> uriOrVar+      | x == 't' || x == 'f' = litBoolean <|> uriOrVar+      | otherwise = uriOrVar++    uriOrVar = do+      isUri <- uriStartsHere+      if isUri then litUri else nixVar++-- | Term is term' with selection+nixTerm :: Parser (NixExpr Ps)+nixTerm = do+  t <- located nixTerm'+  ms <- optional $ try $ located $ attrPath True+  case ms of+    (Just s) ->+      optional+        (located $ (,) <$> kwOr <*> def)+        >>= \case+          Just (L bl (o, d)) -> do+            common <- mkAnnCommon (getLoc s `combineSrcSpans` bl)+            let ann = AnnSelect common (Just (parsedAnnToken AnnOr (getLoc o)))+            pure $ NixSelect ann t s $ Just d+          _ -> do+            common <- mkAnnCommon (getLoc t `combineSrcSpans` getLoc s)+            let ann = AnnSelect common Nothing+            pure $ NixSelect ann t s Nothing+    Nothing -> pure $ unLoc t+  where+    kwOr = reservedKw "or"+    def = located nixExpr++--------------------------------------------------------------------------------++operator :: Ann -> Parser (Located Ann)+operator t = try $ located $ lexeme $ t <$ symbol' (fromJust $ showToken t) <* notFollowedBy (oneOf opString)++type OpParser = Operator Parser (LNixExpr Ps)++appOp :: OpParser+appOp = InfixL $ pure $ \e1 e2 ->+  let l = getLoc e1 `combineSrcSpans` getLoc e2+   in L l $ NixApp (AnnAppNode (mkEmptyAnnCommon l)) e1 e2++notAppOp :: OpParser+notAppOp =+  Prefix $+    ( \(L lt _) e ->+        let l = lt `combineSrcSpans` getLoc e+            ann = AnnPrefixNode (mkEmptyAnnCommon l) (parsedAnnToken AnnEx lt)+         in L l $ NixNotApp ann e+    )+      <$> operator AnnEx++negAppOp :: OpParser+negAppOp =+  Prefix $+    ( \(L lt _) e ->+        let l = lt `combineSrcSpans` getLoc e+            ann = AnnPrefixNode (mkEmptyAnnCommon l) (parsedAnnToken AnnNeg lt)+         in L l $ NixNegApp ann e+    )+      <$> operator AnnNeg++hasAttrOp :: OpParser+hasAttrOp =+  Postfix $+    ( \(L lt _) p e ->+        let l = lt `combineSrcSpans` getLoc p `combineSrcSpans` getLoc e+            ann = AnnHasAttr (mkEmptyAnnCommon l) (parsedAnnToken AnnQuestion lt)+         in L l $ NixHasAttr ann e p+    )+      <$> operator AnnQuestion+      <*> located (attrPath False)++infixOp ::+  ( Parser+      ( Located (NixExpr Ps) ->+        Located (NixExpr Ps) ->+        Located (NixExpr Ps)+      ) ->+    OpParser+  ) ->+  BinaryOp ->+  OpParser+infixOp f op =+  f $+    ( \(L lt _) e1 e2 ->+        let l = getLoc e1 `combineSrcSpans` lt `combineSrcSpans` getLoc e2+            ann = AnnBinAppNode (mkEmptyAnnCommon l) (parsedAnnToken (opToToken op) lt)+         in L l $ NixBinApp ann op e1 e2+    )+      <$> operator (opToToken op)++opTable :: [[OpParser]]+opTable =+  [ [appOp],+    [negAppOp],+    [hasAttrOp],+    [infixR OpConcat],+    [infixL OpMul, infixL OpDiv],+    [infixL OpAdd, infixL OpSub],+    [notAppOp],+    [infixR OpUpdate],+    [infixL OpLT, infixL OpLE, infixL OpGT, infixL OpGE],+    [infixN OpEq, infixN OpNEq],+    [infixL OpAnd],+    [infixL OpOr],+    [infixR OpImpl]+  ]+  where+    infixL = infixOp InfixL+    infixN = infixOp InfixN+    infixR = infixOp InfixR++nixOp :: Parser (NixExpr Ps)+nixOp = unLoc <$> makeExprParser (located nixTerm) opTable++--------------------------------------------------------------------------------++nixExpr :: Parser (NixExpr Ps)+nixExpr = nixLet <|> nixIf <|> nixAssert <|> nixWith <|> nixLam <|> nixOp++--------------------------------------------------------------------------------++nixFile :: Parser (NixExpr Ps)+nixFile = do+  ws+  expr <- nixExpr+  trailing <- takeRemainingComments+  eof+  pure $ attachRootTrailingComments trailing expr++attachRootTrailingComments :: [Located Comment] -> NixExpr Ps -> NixExpr Ps+attachRootTrailingComments comments = \case+  NixVar ann name -> NixVar (attachFollowingComments comments ann) name+  NixLit ann lit -> NixLit (attachFollowingComments comments ann) lit+  NixPar ann expr -> NixPar (attachFollowingComments comments ann) expr+  NixString ann str -> NixString (attachFollowingComments comments ann) str+  NixPath ann path -> NixPath (attachFollowingComments comments ann) path+  NixEnvPath ann path -> NixEnvPath (attachFollowingComments comments ann) path+  NixLam ann pat body -> NixLam (attachFollowingComments comments ann) pat body+  NixApp ann fun arg -> NixApp (attachFollowingComments comments ann) fun arg+  NixBinApp ann op lhs rhs -> NixBinApp (attachFollowingComments comments ann) op lhs rhs+  NixNotApp ann expr -> NixNotApp (attachFollowingComments comments ann) expr+  NixNegApp ann expr -> NixNegApp (attachFollowingComments comments ann) expr+  NixList ann xs -> NixList (attachFollowingComments comments ann) xs+  NixSet ann kind bindings -> NixSet (attachFollowingComments comments ann) kind bindings+  NixLet ann bindings expr -> NixLet (attachFollowingComments comments ann) bindings expr+  NixHasAttr ann expr path -> NixHasAttr (attachFollowingComments comments ann) expr path+  NixSelect ann expr path def -> NixSelect (attachFollowingComments comments ann) expr path def+  NixIf ann cond thenExpr elseExpr -> NixIf (attachFollowingComments comments ann) cond thenExpr elseExpr+  NixWith ann scope expr -> NixWith (attachFollowingComments comments ann) scope expr+  NixAssert ann assertion expr -> NixAssert (attachFollowingComments comments ann) assertion expr
+ src/Nix/Lang/QQ/Parsed.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE TemplateHaskell #-}++module Nix.Lang.QQ.Parsed (nixParsedQQ) where++import qualified Data.Text as T+import Language.Haskell.TH+import Language.Haskell.TH.Quote+import Nix.Lang.Parser (located, nixFile, runNixParser)+import Nix.Lang.Utils (stripCommonPrefix)+import Text.Megaparsec (errorBundlePretty)++nixParsedQQ :: QuasiQuoter+nixParsedQQ =+  QuasiQuoter+    { quoteExp = nixParsedQuot,+      quotePat = const $ error "nixParsedQQ: unsupported quotePat",+      quoteType = const $ error "nixParsedQQ: unsupported quoteType",+      quoteDec = const $ error "nixParsedQQ: unsupported quoteDec"+    }++nixParsedQuot :: String -> ExpQ+nixParsedQuot src =+  case runNixParser (located nixFile) "<qq>" (T.pack (stripCommonPrefix src)) of+    (Left err, _) -> fail (errorBundlePretty err)+    (Right expr, _) -> dataToExpQ (const Nothing) expr
+ src/Nix/Lang/RFCPrint.hs view
@@ -0,0 +1,552 @@+{-# LANGUAGE EmptyCase #-}++-- | RFC 166-oriented formatter for fresh syntax trees.+--+-- This formatter is for fresh syntax trees: values you constructed yourself,+-- lowered from another representation, or otherwise do not want to exact-print.+--+-- In other words, this is the “choose a clean layout” renderer, not the+-- “preserve the original file exactly” renderer. For the latter, use+-- 'Nix.Lang.ExactPrint'. If you want this formatter to inherit some layout intent+-- from parsed source, use 'Nix.Lang.RFCPrint.LayoutHints'.+--+-- Example:+--+-- @+-- import Nix.Lang.RFCPrint (formatExpr)+-- import Nix.Lang.Types.Syn (mkApp, mkVar)+--+-- doc = formatExpr (mkApp (mkVar "f") (mkVar "x"))+-- @+module Nix.Lang.RFCPrint+  ( formatExpr,+    formatExprWithHints,+    formatDoc,+    formatDocWithHints,+  )+where++import Data.Text (Text)+import qualified Data.Text as T+import Nix.Lang.Lexer.Text (escapeIndentedText, escapedChars)+import Nix.Lang.RFCPrint.LayoutHints+import Nix.Lang.Types+import Nix.Lang.Types.Syn+import Nix.Lang.Utils (showBinOP)+import Prettyprinter+import Prettyprinter.Render.Text (renderStrict)++newtype FormatEnv = FormatEnv+  { feLayoutHints :: LayoutHints+  }++rootPath :: NodePath+rootPath = []++childPath :: NodePath -> Int -> NodePath+childPath = childNodePath++chooseLayout :: FormatEnv -> NodePath -> ContainerLayout -> ContainerLayout+chooseLayout env path defaultLayout =+  maybe defaultLayout resolve (lookupLayoutHint path (feLayoutHints env))+  where+    resolve layoutHint = maybe defaultLayout id (lhContainerLayout layoutHint)++lookupTrivia :: FormatEnv -> NodePath -> ([CanonicalTrivia], [CanonicalTrivia])+lookupTrivia env path =+  case lookupLayoutHint path (feLayoutHints env) of+    Just hint -> (lhPriorTrivia hint, lhFollowingTrivia hint)+    Nothing -> ([], [])++formatInterpolation :: FormatEnv -> NodePath -> Expr -> Doc ann+formatInterpolation env path expr = "${" <> formatTop env path expr <> "}"++forceMultilineEnv :: FormatEnv -> NodePath -> FormatEnv+forceMultilineEnv env path = env {feLayoutHints = forceMultilineHint path (feLayoutHints env)}++formatExpr :: Expr -> Text+formatExpr = formatExprWithHints emptyLayoutHints++formatExprWithHints :: LayoutHints -> Expr -> Text+formatExprWithHints hints = renderStrict . layoutPretty defaultLayoutOptions . formatDocWithHints hints++formatDoc :: Expr -> Doc ann+formatDoc = formatDocWithHints emptyLayoutHints++formatDocWithHints :: LayoutHints -> Expr -> Doc ann+formatDocWithHints hints expr =+  let env = FormatEnv hints+   in formatTop env rootPath expr++formatTop :: FormatEnv -> NodePath -> Expr -> Doc ann+formatTop env path expr =+  let (priorTrivia, followingTrivia) = lookupTrivia env path+   in applyTrivia priorTrivia (formatNode env path expr) followingTrivia++data Assoc+  = AssocLeft+  | AssocRight+  | AssocNone+  deriving (Eq)++precLowest, precImpl, precOr, precAnd, precEq, precCmp, precUpdate, precNot, precAdd, precMul, precConcat, precHasAttr, precNeg, precApp, precSelect, precAtom :: Int+precLowest = 0+precImpl = 1+precOr = 2+precAnd = 3+precEq = 4+precCmp = 5+precUpdate = 6+precNot = 7+precAdd = 8+precMul = 9+precConcat = 10+precHasAttr = 11+precNeg = 12+precApp = 13+precSelect = 14+precAtom = 15++exprPrec :: Expr -> Int+exprPrec = \case+  NixVar {} -> precAtom+  NixLit {} -> precAtom+  NixPar {} -> precAtom+  NixString {} -> precAtom+  NixPath {} -> precAtom+  NixEnvPath {} -> precAtom+  NixSelect _ _ _ (Just _) -> precLowest+  NixSelect {} -> precSelect+  NixApp {} -> precApp+  NixNegApp {} -> precNeg+  NixHasAttr {} -> precHasAttr+  NixBinApp _ op _ _ -> opPrec op+  NixNotApp {} -> precNot+  NixList {} -> precAtom+  NixSet {} -> precAtom+  NixLet {} -> precLowest+  NixIf {} -> precLowest+  NixWith {} -> precLowest+  NixAssert {} -> precLowest+  NixLam {} -> precLowest++opPrec :: BinaryOp -> Int+opPrec = \case+  OpConcat -> precConcat+  OpMul -> precMul+  OpDiv -> precMul+  OpAdd -> precAdd+  OpSub -> precAdd+  OpUpdate -> precUpdate+  OpLT -> precCmp+  OpLE -> precCmp+  OpGT -> precCmp+  OpGE -> precCmp+  OpEq -> precEq+  OpNEq -> precEq+  OpAnd -> precAnd+  OpOr -> precOr+  OpImpl -> precImpl++opAssoc :: BinaryOp -> Assoc+opAssoc = \case+  OpConcat -> AssocRight+  OpUpdate -> AssocRight+  OpImpl -> AssocRight+  OpEq -> AssocNone+  OpNEq -> AssocNone+  _ -> AssocLeft++formatChild :: FormatEnv -> NodePath -> Int -> Bool -> Expr -> Doc ann+formatChild env path parentPrec allowSamePrecedence expr =+  parenthesize (childPrecedence < parentPrec || (childPrecedence == parentPrec && not allowSamePrecedence)) (formatNode env path expr)+  where+    childPrecedence = exprPrec expr++formatPrefixChild :: FormatEnv -> NodePath -> Int -> Expr -> Doc ann+formatPrefixChild env path parentPrec expr =+  parenthesize (childPrec <= parentPrec) (formatNode env path expr)+  where+    childPrec = exprPrec expr++formatBinaryLeftChild :: FormatEnv -> NodePath -> BinaryOp -> Expr -> Doc ann+formatBinaryLeftChild env path op =+  formatChild env path (opPrec op) (opAssoc op == AssocLeft)++formatBinaryRightChild :: FormatEnv -> NodePath -> BinaryOp -> Expr -> Doc ann+formatBinaryRightChild env path op =+  formatChild env path (opPrec op) (opAssoc op == AssocRight)++formatNode :: FormatEnv -> NodePath -> Expr -> Doc ann+formatNode env path = \case+  NixVar _ name -> pretty name+  NixLit _ lit -> formatLit lit+  NixPar _ expr -> parens (formatTop env (childPath path 0) expr)+  NixString _ str -> formatString env path str+  NixPath _ p -> formatPath env path p+  NixEnvPath _ p -> "<" <> pretty p <> ">"+  NixLam _ pat@(NixVarPat _ _) expr -> group $ hang 2 (formatFuncPat env (childPath path 0) pat <> ":" <+> formatTop env (childPath path 1) expr)+  NixLam _ pat expr ->+    case chooseLayout env path (defaultLambdaLayout pat) of+      PreferInline -> group $ hang 2 (formatFuncPat env (childPath path 0) pat <> ":" <+> formatTop env (childPath path 1) expr)+      PreferMultiline -> formatFuncPat env (childPath path 0) pat <> ":" <> hardline <> formatTop env (childPath path 1) expr+  app@NixApp {} -> formatApp env path app+  NixBinApp _ op lhs rhs ->+    group $ hang 2 (formatBinaryLeftChild env (childPath path 0) op lhs <+> pretty (showBinOP op) <+> formatBinaryRightChild env (childPath path 1) op rhs)+  NixNotApp _ expr -> "!" <> formatPrefixChild env (childPath path 0) precNot expr+  NixNegApp _ expr -> "-" <> formatPrefixChild env (childPath path 0) precNeg expr+  NixList _ xs -> formatBracketed env path "[" "]" (zipWith (formatTop env . childPath path) [0 ..] xs)+  NixSet _ recFlag bindings -> formatSet env path recFlag bindings+  NixLet _ bindings expr -> formatLet env path bindings expr+  NixHasAttr _ expr attrPath -> formatPrefixChild env (childPath path 0) precHasAttr expr <+> "?" <+> formatAttrPath env (childPath path 1) attrPath+  NixSelect _ expr attrPath mDefault ->+    formatPrefixChild env (childPath path 0) precSelect expr <> "." <> formatAttrPath env (childPath path 1) attrPath <> maybe mempty (formatSelectDefault env (childPath path 2)) mDefault+  NixIf _ cond t f -> formatIf env path cond t f+  NixWith _ scope expr ->+    case wantsForcedMultiline expr of+      True ->+        vsep+          [ "with" <+> formatTop env (childPath path 0) scope <> ";",+            formatForcedMultiline env (childPath path 1) expr+          ]+      False -> group $ "with" <+> formatTop env (childPath path 0) scope <> ";" <+> formatTop env (childPath path 1) expr+  NixAssert _ assertion expr ->+    "assert "+      <> formatTop env (childPath path 0) assertion+      <> ";"+      <> hardline+      <> formatTop env (childPath path 1) expr++formatLit :: Lit -> Doc ann+formatLit = \case+  NixUri _ uri -> pretty uri+  NixInteger _ int -> pretty int+  NixFloat _ float -> pretty float+  NixBoolean _ bool -> if bool then "true" else "false"+  NixNull _ -> "null"++formatString :: FormatEnv -> NodePath -> NString -> Doc ann+formatString env path = \case+  NixDoubleQuotesString _ parts -> dquotes $ mconcat (zipWith (formatDoubleQuotedPart env . childPath path) [0 ..] parts)+  NixDoubleSingleQuotesString _ parts -> "''" <> mconcat (zipWith (formatIndentedPart env . childPath path) [0 ..] parts) <> "''"++formatDoubleQuotedPart :: FormatEnv -> NodePath -> StringPart -> Doc ann+formatDoubleQuotedPart env path = \case+  NixStringLiteral _ text -> pretty (escapeDoubleQuoted text)+  NixStringInterpol _ expr -> formatInterpolation env path expr++formatIndentedPart :: FormatEnv -> NodePath -> StringPart -> Doc ann+formatIndentedPart env path = \case+  NixStringLiteral _ text -> pretty (escapeIndentedText text)+  NixStringInterpol _ expr -> formatInterpolation env path expr++formatPath :: FormatEnv -> NodePath -> Path -> Doc ann+formatPath env path = \case+  NixLiteralPath _ literalPath -> pretty literalPath+  NixInterpolPath _ parts -> mconcat (zipWith (formatPathPart env . childPath path) [0 ..] parts)++formatPathPart :: FormatEnv -> NodePath -> StringPart -> Doc ann+formatPathPart env path = \case+  NixStringLiteral _ text -> pretty text+  NixStringInterpol _ expr -> formatInterpolation env path expr++formatAttrKey :: FormatEnv -> NodePath -> AttrKey -> Doc ann+formatAttrKey env path = \case+  NixStaticAttrKey _ name -> pretty name+  NixDynamicStringAttrKey _ parts -> dquotes $ mconcat (zipWith (formatDoubleQuotedPart env . childPath path) [0 ..] parts)+  NixDynamicInterpolAttrKey _ expr -> formatInterpolation env path expr++formatAttrPath :: FormatEnv -> NodePath -> AttrPath -> Doc ann+formatAttrPath env path (NixAttrPath _ keys) =+  let (prior, following) = lookupTrivia env path+      (_, prior') = splitLeadingBlankLine prior+      body = hcat $ punctuate "." (zipWith (formatAttrKey env . childPath path) [0 ..] keys)+   in applyTrivia prior' body following++formatBinding :: FormatEnv -> NodePath -> Binding -> Doc ann+formatBinding env path binding =+  case binding of+    NixNormalBinding _ attrPath expr ->+      case wantsBindingValueMultiline expr of+        True ->+          formatAttrPath env (childPath path 0) attrPath+            <+> "="+            <> hardline+            <> indent 2 (formatForcedMultiline env (childPath path 1) expr)+            <> ";"+        False -> formatAttrPath env (childPath path 0) attrPath <+> "=" <+> formatTop env (childPath path 1) expr <> ";"+    NixInheritBinding _ mScope names ->+      let (prior, following) = lookupTrivia env path+          scopeDoc = maybe mempty (\scope -> " (" <> formatInheritScope env (childPath path 0) scope <> ")") mScope+          keyDocs = zipWith (formatAttrKey env . childPath path) [0 ..] names+          namesDoc = case names of+            [] -> mempty+            _ -> " " <> fillSep keyDocs+          multiline =+            vsep+              [ "inherit" <> scopeDoc,+                indent 2 (vsep keyDocs),+                indent 2 ";"+              ]+          body = if length names >= 4 then multiline else group ("inherit" <> scopeDoc <> namesDoc <> ";")+       in applyTrivia prior body following++formatFuncPat :: FormatEnv -> NodePath -> FuncPat -> Doc ann+formatFuncPat env path = \case+  NixVarPat _ name -> pretty name+  NixSetPat _ ellipses mAs bindings ->+    case fmap nspaLocation mAs of+      Just NixSetPatAsLeading -> formatSetPatAsLeading <> formatSetPatParams+      Just NixSetPatAsTrailing -> formatSetPatParams <> formatSetPatAsTrailing+      Nothing -> formatSetPatParams+    where+      formatSetPatAsLeading = maybe mempty formatSetPatAs mAs+      formatSetPatAsTrailing = maybe mempty formatSetPatAs mAs+      entries = zipWith (formatSetPatBinding env . childPath path) [0 ..] bindings <> ["..." | ellipses == NixSetPatIsEllipses]+      formatSetPatParams =+        case chooseLayout env path (defaultSetPatLayout ellipses bindings) of+          PreferInline ->+            case (ellipses, bindings) of+              (NixSetPatNotEllipses, []) -> "{ }"+              (NixSetPatIsEllipses, []) -> "{ ... }"+              _ -> encloseSep "{ " " }" ", " entries+          PreferMultiline -> case entries of+            [] -> "{ }"+            _ ->+              let bodyEntries = fmap (<> ",") (zipWith const entries bindings)+                  ellipsisEntry = ["..." | ellipses == NixSetPatIsEllipses]+               in vsep (["{"] <> fmap indentEntry (bodyEntries <> ellipsisEntry) <> ["}"])+      indentEntry = indent 2++formatSetPatAs :: SetPatAs -> Doc ann+formatSetPatAs NixSetPatAs {..} = case nspaLocation of+  NixSetPatAsLeading -> pretty nspaVar <> "@"+  NixSetPatAsTrailing -> "@" <> pretty nspaVar++formatSetPatBinding :: FormatEnv -> NodePath -> SetPatBinding -> Doc ann+formatSetPatBinding env path NixSetPatBinding {..} = pretty nspbVar <> maybe mempty ((" ? " <>) . formatTop env (childPath path 0)) nspbDefault++defaultLambdaLayout :: FuncPat -> ContainerLayout+defaultLambdaLayout = \case+  NixVarPat {} -> PreferInline+  NixSetPat _ ellipses _ bindings -> defaultSetPatLayout ellipses bindings++defaultSetPatLayout :: NixSetPatEllipses -> [SetPatBinding] -> ContainerLayout+defaultSetPatLayout ellipses bindings =+  case (ellipses, bindings) of+    (NixSetPatNotEllipses, []) -> PreferInline+    (NixSetPatIsEllipses, []) -> PreferInline+    _ -> PreferMultiline++formatSet :: FormatEnv -> NodePath -> NixSetIsRecursive -> [Binding] -> Doc ann+formatSet env path recFlag bindings =+  case bindings of+    [] -> case recFlag of+      NixSetRecursive -> "rec { }"+      NixSetNonRecursive -> "{ }"+    _ -> case chooseLayout env path PreferMultiline of+      PreferInline -> single+      PreferMultiline -> multi+  where+    prefix = case recFlag of+      NixSetRecursive -> "rec "+      NixSetNonRecursive -> mempty+    single = prefix <> "{ " <> hsep (zipWith (formatBinding env . childPath path) [0 ..] bindings) <+> "}"+    multi =+      let bindingDocs = zipWith (formatBinding env . childPath path) [0 ..] bindings+          renderedBindings = intercalateBindingDocs bindingDocs+          openGap = if firstBindingHasLeadingBlankLine then [mempty] else []+       in vsep ([prefix <> "{"] <> openGap <> renderedBindings <> ["}"])++    firstBindingHasLeadingBlankLine =+      case bindings of+        firstBinding : _ -> bindingLeadingBlankLine env (childPath path 0) firstBinding+        [] -> False++    intercalateBindingDocs [] = []+    intercalateBindingDocs [doc] = [indent 2 doc]+    intercalateBindingDocs (doc : rest) = indent 2 doc : concatMap addBindingGap (zip [1 :: Int ..] rest)++    addBindingGap (i, doc)+      | bindingLeadingBlankLine env (childPath path i) (bindings !! i) = [mempty, indent 2 doc]+      | otherwise = [indent 2 doc]++formatLet :: FormatEnv -> NodePath -> [Binding] -> Expr -> Doc ann+formatLet env path bindings expr =+  case bindings of+    [] -> vsep ["let", "in", formatTop env (childPath path 0) expr]+    _ -> case chooseLayout env path PreferMultiline of+      PreferInline -> single+      PreferMultiline -> multi+  where+    single = "let" <+> hsep (zipWith (formatBinding env . childPath path) [0 ..] bindings) <+> "in" <+> formatTop env (childPath path (length bindings)) expr+    multi = vsep ["let", indent 2 (vsep (zipWith (formatBinding env . childPath path) [0 ..] bindings)), "in", formatTop env (childPath path (length bindings)) expr]++formatInheritScope :: FormatEnv -> NodePath -> Expr -> Doc ann+formatInheritScope env path = formatTop env path++formatApp :: FormatEnv -> NodePath -> Expr -> Doc ann+formatApp env path expr =+  case collectAppChain expr of+    [] -> mempty+    f : args ->+      let headDoc = formatAppHead env (childPath path 0) f+          argDocs = zipWith (formatAppArg env . childPath path) [1 ..] args+       in if any wantsForcedMultiline args+            then foldl (<+>) headDoc argDocs+            else group $ hang 2 (headDoc <+> hsep argDocs)++collectAppChain :: Expr -> [Expr]+collectAppChain = go []+  where+    go acc = \case+      NixApp _ f x -> go (x : acc) f+      other -> other : acc++formatAppHead :: FormatEnv -> NodePath -> Expr -> Doc ann+formatAppHead env path expr = formatChild env path precApp True expr++formatAppArg :: FormatEnv -> NodePath -> Expr -> Doc ann+formatAppArg env path expr =+  case wantsForcedMultiline expr of+    True -> parenthesizeForcedAppArg expr (formatForcedMultiline env path expr)+    False -> formatChild env path precApp False expr++parenthesizeForcedAppArg :: Expr -> Doc ann -> Doc ann+parenthesizeForcedAppArg expr doc+  | exprPrec expr <= precApp = vsep ["(", indent 2 doc, ")"]+  | otherwise = doc++formatForcedMultiline :: FormatEnv -> NodePath -> Expr -> Doc ann+formatForcedMultiline env path expr =+  case expr of+    NixSet _ recFlag bindings -> formatSet (forceMultilineEnv env path) path recFlag bindings+    NixLet _ bindings body -> formatLet (forceMultilineEnv env path) path bindings body+    _ -> formatTop env path expr++wantsForcedMultiline :: Expr -> Bool+wantsForcedMultiline = \case+  NixSet _ _ bindings -> not (null bindings)+  NixLet _ bindings _ -> not (null bindings)+  _ -> False++wantsBindingValueMultiline :: Expr -> Bool+wantsBindingValueMultiline = \case+  NixLet _ bindings _ -> not (null bindings)+  _ -> False++formatBracketed :: FormatEnv -> NodePath -> Doc ann -> Doc ann -> [Doc ann] -> Doc ann+formatBracketed env path open close docs =+  case docs of+    [] -> open <+> close+    _ -> case chooseLayout env path PreferMultiline of+      PreferInline -> single+      PreferMultiline -> multi+  where+    single = open <+> hsep docs <+> close+    multi = vsep [open, indent 2 (vsep docs), close]++formatSelectDefault :: FormatEnv -> NodePath -> Expr -> Doc ann+formatSelectDefault env path expr = " or " <> parenthesize (needsKeywordParens expr) (formatTop env path expr)++formatIf :: FormatEnv -> NodePath -> Expr -> Expr -> Expr -> Doc ann+formatIf env path cond t f =+  case f of+    NixIf _ nestedCond nestedThen nestedElse ->+      vsep+        [ "if" <+> formatTop env (childPath path 0) cond <+> "then",+          indent 2 (formatTop env (childPath path 1) t),+          formatElseIf env (childPath path 2) nestedCond nestedThen nestedElse+        ]+    _ -> group $ "if" <+> formatTop env (childPath path 0) cond <+> "then" <+> formatTop env (childPath path 1) t <+> "else" <+> formatTop env (childPath path 2) f++formatElseIf :: FormatEnv -> NodePath -> Expr -> Expr -> Expr -> Doc ann+formatElseIf env path cond t f =+  case f of+    NixIf _ nestedCond nestedThen nestedElse ->+      vsep+        [ "else if" <+> formatTop env (childPath path 0) cond <+> "then",+          indent 2 (formatTop env (childPath path 1) t),+          formatElseIf env (childPath path 2) nestedCond nestedThen nestedElse+        ]+    _ ->+      vsep+        [ "else if" <+> formatTop env (childPath path 0) cond <+> "then",+          indent 2 (formatTop env (childPath path 1) t),+          "else",+          indent 2 (formatTop env (childPath path 2) f)+        ]++needsKeywordParens :: Expr -> Bool+needsKeywordParens = \case+  NixIf {} -> True+  NixWith {} -> True+  NixAssert {} -> True+  NixLet {} -> True+  NixLam {} -> True+  NixSelect _ _ _ (Just _) -> True+  _ -> False++parenthesize :: Bool -> Doc ann -> Doc ann+parenthesize needs doc+  | needs = parens doc+  | otherwise = doc++formatCanonicalTrivia :: CanonicalTrivia -> Doc ann+formatCanonicalTrivia = \case+  CanonicalLineComment txt -> "#" <> pretty txt+  CanonicalBlockComment txt -> "/*" <> pretty txt <> "*/"+  CanonicalBlankLine -> mempty++applyTrivia :: [CanonicalTrivia] -> Doc ann -> [CanonicalTrivia] -> Doc ann+applyTrivia prior body following = prepend prior (append following body)+  where+    prepend [] doc = doc+    prepend (CanonicalBlankLine : ts) doc = hardline <> prepend ts doc+    prepend (trivia : ts) doc = formatCanonicalTrivia trivia <> hardline <> prepend ts doc++    append [] doc = doc+    append (CanonicalBlankLine : ts) doc = doc <> hardline <> append ts mempty+    append (trivia : ts) doc = doc <> hardline <> formatCanonicalTrivia trivia <> append ts mempty++splitLeadingBlankLine :: [CanonicalTrivia] -> (Bool, [CanonicalTrivia])+splitLeadingBlankLine (CanonicalBlankLine : xs) = (True, xs)+splitLeadingBlankLine xs = (False, xs)++bindingLeadingBlankLine :: FormatEnv -> NodePath -> Binding -> Bool+bindingLeadingBlankLine env path = \case+  NixNormalBinding _ _ _ ->+    case lookupTrivia env (childPath path 0) of+      (prior, _) -> fst (splitLeadingBlankLine prior)+  NixInheritBinding _ _ _ ->+    case lookupTrivia env path of+      (prior, _) -> fst (splitLeadingBlankLine prior)++forceMultilineHint :: NodePath -> LayoutHints -> LayoutHints+forceMultilineHint path hints =+  case lookupLayoutHint path hints of+    Just hint ->+      hints <> singletonLayoutHint path (LayoutHint (Just PreferMultiline) (lhPriorTrivia hint) (lhFollowingTrivia hint))+    Nothing -> hints <> singletonLayoutHint path (LayoutHint (Just PreferMultiline) [] [])++escapeDoubleQuoted :: Text -> Text+escapeDoubleQuoted = go+  where+    go text+      | T.null text = mempty+      | "${" `T.isPrefixOf` text = "\\${" <> go (T.drop 2 text)+      | otherwise =+          let (char, rest) = (T.head text, T.tail text)+           in escapeChar char <> go rest++    escapeChar char = case lookupEscape char of+      Just code -> "\\" <> T.singleton code+      Nothing -> T.singleton char++    lookupEscape char = lookup char escapes++    escapes =+      [ (decoded, encoded)+      | (encoded, decoded) <- escapedChars,+        decoded `notElem` ['{', '.']+      ]
+ src/Nix/Lang/RFCPrint/LayoutHints.hs view
@@ -0,0 +1,402 @@+-- | 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
+ src/Nix/Lang/Span.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveTraversable #-}++-- | Source span and located-value types.+--+-- These are the location wrappers used throughout the parsed and exact-printing+-- parts of the library.+module Nix.Lang.Span where++import Data.Data (Data)++data SrcSpan = SrcSpan+  { srcSpanFilename :: String,+    srcSpanStartLine :: Int,+    srcSpanStartColumn :: Int,+    srcSpanEndLine :: Int,+    srcSpanEndColumn :: Int+  }+  deriving (Eq, Data)++instance Show SrcSpan where+  show SrcSpan {..} =+    srcSpanFilename+      <> ":"+      <> if srcSpanStartLine == srcSpanEndLine+        then+          show srcSpanEndLine+            <> ":"+            <> if srcSpanStartColumn == srcSpanEndColumn+              then show srcSpanEndColumn+              else show srcSpanStartColumn <> "-" <> show srcSpanEndColumn+        else show (srcSpanStartLine, srcSpanStartColumn) <> "-" <> show (srcSpanEndLine, srcSpanEndColumn)++instance Ord SrcSpan where+  a `compare` b = case (srcSpanStartLine a, srcSpanStartColumn a)+    `compare` (srcSpanStartLine b, srcSpanStartColumn b) of+    EQ -> (srcSpanEndLine a, srcSpanEndColumn a) `compare` (srcSpanEndLine b, srcSpanEndColumn b)+    x -> x++data Located a = L SrcSpan a+  deriving (Eq, Show, Ord, Data, Functor, Foldable, Traversable)
+ src/Nix/Lang/Types.hs view
@@ -0,0 +1,601 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE UndecidableInstances #-}++-- | Core Trees-that-Grow Nix AST definitions.+--+-- This is the shared TTG definition that both syntax passes instantiate:+-- parser-produced trees with annotations, and fresh trees built for formatting.+module Nix.Lang.Types where++import Data.Data (Data)+import Data.Proxy (Proxy)+import Data.Text (Text)++--------------------------------------------------------------------------------+data NoExtF = NoExtF+  deriving (Eq, Show, Data)++data NoExtC+  deriving (Data)++instance Show NoExtC where+  show _ = undefined++type family XRec p a++class UnXRec p where+  unXRec :: Proxy p -> XRec p a -> a++--------------------------------------------------------------------------------+data BinaryOp+  = -- | @++@+    OpConcat+  | -- | @*@+    OpMul+  | -- | @/@+    OpDiv+  | -- | @+@+    OpAdd+  | -- | @-@+    OpSub+  | -- | @//@+    OpUpdate+  | -- | @<@+    OpLT+  | -- | @<=@+    OpLE+  | -- | @>@+    OpGT+  | -- | @>=@+    OpGE+  | -- | @==@+    OpEq+  | -- | @!=@+    OpNEq+  | -- | @&&@+    OpAnd+  | -- | @||@+    OpOr+  | -- | @->@+    OpImpl+  deriving (Show, Eq, Enum, Data)++--------------------------------------------------------------------------------+data NixLit p+  = -- | @https://nixos.org/@+    NixUri (XNixUri p) Text+  | -- | @233@+    NixInteger (XNixInteger p) Integer+  | -- | @233.3@+    -- Note: Currently scientific and number started with decimal dot are not supported+    NixFloat (XNixFloat p) Float+  | -- | @true@ or @false@+    NixBoolean (XNixBoolean p) Bool+  | -- |  @null@+    NixNull (XNixNull p)+  | XNixLit !(XXNixLit p)++instance Eq (NixLit p) where+  (NixUri _ x) == (NixUri _ y) = x == y+  (NixInteger _ x) == (NixInteger _ y) = x == y+  (NixFloat _ x) == (NixFloat _ y) = x == y+  (NixBoolean _ x) == (NixBoolean _ y) = x == y+  (NixNull _) == (NixNull _) = True+  _ == _ = False++deriving instance+  ( Data p,+    Data (XNixUri p),+    Data (XNixInteger p),+    Data (XNixFloat p),+    Data (XNixNull p),+    Data (XNixBoolean p),+    Data (XXNixLit p)+  ) =>+  Data (NixLit p)++deriving instance+  ( Show (XNixUri p),+    Show (XNixInteger p),+    Show (XNixFloat p),+    Show (XNixNull p),+    Show (XNixBoolean p),+    Show (XXNixLit p)+  ) =>+  Show (NixLit p)++type LNixLit p = XRec p (NixLit p)++type family XNixUri p++type family XNixInteger p++type family XNixFloat p++type family XNixBoolean p++type family XNixNull p++type family XXNixLit p++--------------------------------------------------------------------------------++data NixStringPart p+  = -- | @x@+    -- As there is no annotation attached to this node, the location of 'Text'+    -- should be the same as the parent node; thus 'Text' doesn't need to have location+    -- information+    NixStringLiteral (XNixStringLiteral p) Text+  | -- | @${e}@, where e is a string+    NixStringInterpol (XNixStringInterpol p) (LNixExpr p)+  | XNixStringPart !(XXNixStringPart p)++deriving instance+  ( Data p,+    Data (XNixStringLiteral p),+    Data (XNixStringInterpol p),+    Data (XXNixStringPart p),+    Data (LNixExpr p)+  ) =>+  Data (NixStringPart p)++deriving instance+  ( Show (XNixStringLiteral p),+    Show (XNixStringInterpol p),+    Show (XXNixStringPart p),+    Show (LNixExpr p)+  ) =>+  Show (NixStringPart p)++type LNixStringPart p = XRec p (NixStringPart p)++type family XNixStringLiteral p++type family XNixStringInterpol p++type family XXNixStringPart p++--------------------------------------------------------------------------------++data NixString p+  = -- | @"x${e}x"@+    NixDoubleQuotesString (XNixDoubleQuotesString p) [LNixStringPart p]+  | -- | @+    -- '' A+    --    B ${c}+    --    d+    -- ''+    -- @+    -- In order to keep original source location correct, the parser won't strip+    -- common minimum indentation and do further processing.+    NixDoubleSingleQuotesString (XNixDoubleSingleQuotesString p) [LNixStringPart p]+  | XNixString !(XXNixString p)++deriving instance+  ( Data p,+    Data (XNixDoubleQuotesString p),+    Data (XNixDoubleSingleQuotesString p),+    Data (XXNixString p),+    Data (LNixStringPart p)+  ) =>+  Data (NixString p)++deriving instance+  ( Show (XNixDoubleQuotesString p),+    Show (XNixDoubleSingleQuotesString p),+    Show (XXNixString p),+    Show (LNixStringPart p)+  ) =>+  Show (NixString p)++type LNixString p = XRec p (NixString p)++type family XNixDoubleQuotesString p++type family XNixDoubleSingleQuotesString p++type family XXNixString p++--------------------------------------------------------------------------------+data NixPath p+  = -- | @./a/b/c@, @/a/b/c@, @~/a/b/c@+    --+    -- No consecutive slashes are allowed.+    NixLiteralPath (XNixLiteralPath p) Text+  | -- | @./${e}/b/c@, @./${a}-${b}/c/d${e}@+    --+    -- Since nix 2.13, slashes are no longer required between parts, but the behavior seems to be a bit strange:+    -- no more than two consecutive slashes can appear before the interpolation,+    -- while arbitrary number of slashes can appear after the interpolation.+    -- The parser won't enforce this.+    NixInterpolPath (XNixInterpolPath p) [LNixStringPart p]+  | XNixPath !(XXNixPath p)++deriving instance+  ( Data p,+    Data (XNixLiteralPath p),+    Data (XNixInterpolPath p),+    Data (LNixStringPart p),+    Data (XXNixPath p)+  ) =>+  Data (NixPath p)++deriving instance+  ( Show (XNixLiteralPath p),+    Show (XNixInterpolPath p),+    Show (LNixStringPart p),+    Show (XXNixPath p)+  ) =>+  Show (NixPath p)++type LNixPath p = XRec p (NixPath p)++type family XNixLiteralPath p++type family XNixInterpolPath p++type family XXNixPath p++--------------------------------------------------------------------------------+data NixAttrKey p+  = -- | @{ x = 123; }.x@+    NixStaticAttrKey (XNixStaticAttrKey p) (LNixAttrName p)+  | -- | @{ x = 123; }."x"@, @{ x = 123; }."${"x"}"@+    -- Only double quoted strings are allowed+    NixDynamicStringAttrKey (XNixDynamicStringAttrKey p) [LNixStringPart p]+  | -- | @{ x = 123; }.${"x"}@+    NixDynamicInterpolAttrKey (XNixDynamicInterpolAttrKey p) (LNixExpr p)+  | XNixNixAttrKey !(XXNixAttrKey p)++deriving instance+  ( Data p,+    Data (XNixStaticAttrKey p),+    Data (XNixDynamicStringAttrKey p),+    Data (XNixDynamicInterpolAttrKey p),+    Data (LNixStringPart p),+    Data (XXNixAttrKey p),+    Data (LNixAttrName p),+    Data (LNixExpr p)+  ) =>+  Data (NixAttrKey p)++deriving instance+  ( Show (XNixStaticAttrKey p),+    Show (XNixDynamicStringAttrKey p),+    Show (XNixDynamicInterpolAttrKey p),+    Show (LNixStringPart p),+    Show (XXNixAttrKey p),+    Show (LNixAttrName p),+    Show (LNixExpr p)+  ) =>+  Show (NixAttrKey p)++type LNixAttrKey p = XRec p (NixAttrKey p)++type family XNixStaticAttrKey p++type family XNixDynamicStringAttrKey p++type family XNixDynamicInterpolAttrKey p++type family XXNixAttrKey p++--------------------------------------------------------------------------------++-- | @a.b.${c}."d"."${"e"}"@+data NixAttrPath p = NixAttrPath (XNixAttrPath p) [LNixAttrKey p]++deriving instance (Data p, Data (XNixAttrPath p), Data (LNixAttrKey p)) => Data (NixAttrPath p)++deriving instance (Show (XNixAttrPath p), Show (LNixAttrKey p)) => Show (NixAttrPath p)++type LNixAttrPath p = XRec p (NixAttrPath p)++type family XNixAttrPath p++--------------------------------------------------------------------------------++data NixBinding p+  = -- | @x = e;@+    NixNormalBinding (XNixNormalBinding p) (LNixAttrPath p) (LNixExpr p)+  | -- | @inherit a@, @inherit (a) b c@, @inherit (a) "b" ${"c"}@+    -- Note: Dynamic keys like @${"a" + "b"}@ are allowed since nix 2.13+    NixInheritBinding (XNixInheritBinding p) (Maybe (LNixExpr p)) [LNixAttrKey p]+  | XNixBinding !(XXNixBinding p)++deriving instance+  ( Data p,+    Data (XNixNormalBinding p),+    Data (LNixAttrPath p),+    Data (LNixExpr p),+    Data (XNixInheritBinding p),+    Data (XXNixBinding p),+    Data (LNixAttrKey p)+  ) =>+  Data (NixBinding p)++deriving instance+  ( Show (XNixNormalBinding p),+    Show (LNixAttrPath p),+    Show (LNixExpr p),+    Show (XNixInheritBinding p),+    Show (XXNixBinding p),+    Show (LNixAttrKey p)+  ) =>+  Show (NixBinding p)++type LNixBinding p = XRec p (NixBinding p)++type NixBindings p = [LNixBinding p]++type LNixBindings p = XRec p (NixBindings p)++type family XNixNormalBinding p++type family XNixInheritBinding p++type family XXNixBinding p++--------------------------------------------------------------------------------+data NixSetPatAsLocation+  = -- | @x@{...}@+    NixSetPatAsLeading+  | -- | @{...}@x@+    NixSetPatAsTrailing+  deriving (Eq, Show, Ord, Data)++--------------------------------------------------------------------------------++data NixSetPatAs p = NixSetPatAs+  { nspaAnn :: XNixSetPatAs p,+    nspaLocation :: NixSetPatAsLocation,+    -- | @x@{...}@+    nspaVar :: LNixBinderName p+  }++deriving instance (Data p, Data (XNixSetPatAs p), Data (LNixBinderName p)) => Data (NixSetPatAs p)++deriving instance (Show (XNixSetPatAs p), Show (LNixBinderName p)) => Show (NixSetPatAs p)++type LNixSetPatAs p = XRec p (NixSetPatAs p)++--------------------------------------------------------------------------------++data NixSetPatBinding p = NixSetPatBinding+  { nspbAnn :: XNixSetPatBinding p,+    -- | @{a}@+    nspbVar :: LNixBinderName p,+    -- | @{a ? b}@+    nspbDefault :: Maybe (LNixExpr p)+  }++deriving instance (Data p, Data (XNixSetPatBinding p), Data (LNixBinderName p), Data (LNixExpr p)) => Data (NixSetPatBinding p)++deriving instance (Show (XNixSetPatBinding p), Show (LNixBinderName p), Show (LNixExpr p)) => Show (NixSetPatBinding p)++type LNixSetPatBinding p = XRec p (NixSetPatBinding p)++--------------------------------------------------------------------------------++-- | Whether the pattern accepts unknown arguments+data NixSetPatEllipses+  = NixSetPatIsEllipses+  | NixSetPatNotEllipses+  deriving (Show, Eq, Ord, Data)++data NixFuncPat p+  = -- | @x: ...@+    NixVarPat (XNixVarPat p) (LNixBinderName p)+  | -- | @x@{a, b ? c, d, ...}: ...@+    NixSetPat (XNixSetPat p) NixSetPatEllipses (Maybe (LNixSetPatAs p)) [LNixSetPatBinding p]+  | XNixFuncPat !(XXNixFuncPat p)++deriving instance+  ( Data p,+    Data (LNixBinderName p),+    Data (XNixVarPat p),+    Data (XNixSetPat p),+    Data (XXNixFuncPat p),+    Data (LNixSetPatAs p),+    Data (LNixSetPatBinding p)+  ) =>+  Data (NixFuncPat p)++deriving instance+  ( Show (LNixBinderName p),+    Show (XNixVarPat p),+    Show (XNixSetPat p),+    Show (XXNixFuncPat p),+    Show (LNixSetPatAs p),+    Show (LNixSetPatBinding p)+  ) =>+  Show (NixFuncPat p)++type LNixFuncPat p = XRec p (NixFuncPat p)++type family XNixVarPat p++type family XNixSetPat p++type family XNixSetPatAs p++type family XNixSetPatBinding p++type family XXNixFuncPat p++--------------------------------------------------------------------------------+data NixSetIsRecursive+  = -- | @rec {...}@+    NixSetRecursive+  | -- | @{...}@+    NixSetNonRecursive+  deriving (Show, Eq, Ord, Data)++--------------------------------------------------------------------------------++data NixExpr p+  = -- | @x@+    NixVar (XNixVar p) (LNixVarName p)+  | -- | See 'NixLit'+    NixLit (XNixLit p) (LNixLit p)+  | -- | Parenthesized expr - the pretty printer won't add parens+    NixPar (XNixPar p) (LNixExpr p)+  | -- | See 'NixString'+    NixString (XNixString p) (LNixString p)+  | -- | See 'NixPath'+    NixPath (XNixPath p) (LNixPath p)+  | -- | @<nixpkgs>@+    NixEnvPath (XNixEnvPath p) (XRec p Text)+  | -- | See 'LNixFuncPat'+    NixLam (XNixLam p) (LNixFuncPat p) (LNixExpr p)+  | -- | @f x@+    NixApp (XNixApp p) (LNixExpr p) (LNixExpr p)+  | -- | @a + b@, @a >= b@, @a // b@...+    NixBinApp (XNixBinApp p) BinaryOp (LNixExpr p) (LNixExpr p)+  | -- | @!a@+    NixNotApp (XNixNotApp p) (LNixExpr p)+  | -- | @-a@+    NixNegApp (XNixNegApp p) (LNixExpr p)+  | -- | @[ a b c ]@+    NixList (XNixList p) [LNixExpr p]+  | -- | See 'NixSetRecursive' and 'NixBinding'+    NixSet (XNixSet p) NixSetIsRecursive (LNixBindings p)+  | -- | @let a = 1; in b@, @let inherit (a) b; in c@+    -- Note: legacy let body syntax is not supported+    NixLet (XNixLet p) (LNixBindings p) (LNixExpr p)+  | -- | @a ? b@+    NixHasAttr (XNixHasAttr p) (LNixExpr p) (LNixAttrPath p)+  | -- | @a.b or c@+    NixSelect (XNixSelect p) (LNixExpr p) (LNixAttrPath p) (Maybe (LNixExpr p))+  | -- | @if a then b else c@+    NixIf (XNixIf p) (LNixExpr p) (LNixExpr p) (LNixExpr p)+  | -- | @with a; b@+    NixWith (XNixWith p) (LNixExpr p) (LNixExpr p)+  | -- | @assert a; b@+    NixAssert (XNixAssert p) (LNixExpr p) (LNixExpr p)+  | XNixExpr !(XXNixExpr p)++deriving instance+  ( Data p,+    Data (LNixVarName p),+    Data (XNixVar p),+    Data (XNixLit p),+    Data (LNixLit p),+    Data (XNixPar p),+    Data (LNixExpr p),+    Data (XNixString p),+    Data (LNixString p),+    Data (LNixPath p),+    Data (XNixPath p),+    Data (XNixEnvPath p),+    Data (XRec p Text),+    Data (XNixLam p),+    Data (LNixFuncPat p),+    Data (XNixApp p),+    Data (XNixBinApp p),+    Data (XNixNotApp p),+    Data (XNixNegApp p),+    Data (XNixList p),+    Data (XNixSet p),+    Data (LNixBindings p),+    Data (XNixLet p),+    Data (XNixHasAttr p),+    Data (LNixAttrPath p),+    Data (XNixSelect p),+    Data (XNixIf p),+    Data (XNixWith p),+    Data (XNixAssert p),+    Data (XXNixExpr p)+  ) =>+  Data (NixExpr p)++deriving instance+  ( Show (LNixVarName p),+    Show (XNixVar p),+    Show (XNixLit p),+    Show (LNixLit p),+    Show (XNixPar p),+    Show (LNixExpr p),+    Show (XNixString p),+    Show (LNixString p),+    Show (LNixPath p),+    Show (XNixPath p),+    Show (XNixEnvPath p),+    Show (XRec p Text),+    Show (XNixLam p),+    Show (LNixFuncPat p),+    Show (XNixApp p),+    Show (XNixBinApp p),+    Show (XNixNotApp p),+    Show (XNixNegApp p),+    Show (XNixList p),+    Show (XNixSet p),+    Show (LNixBindings p),+    Show (XNixLet p),+    Show (XNixHasAttr p),+    Show (LNixAttrPath p),+    Show (XNixSelect p),+    Show (XNixIf p),+    Show (XNixWith p),+    Show (XNixAssert p),+    Show (XXNixExpr p)+  ) =>+  Show (NixExpr p)++type family NixVarName p++type LNixVarName p = XRec p (NixVarName p)++type family NixBinderName p++type LNixBinderName p = XRec p (NixBinderName p)++type family NixAttrName p++type LNixAttrName p = XRec p (NixAttrName p)++type LNixExpr p = XRec p (NixExpr p)++type family XXNixExpr p++type family XNixApp p++type family XNixPath p++type family XNixEnvPath p++type family XNixString p++type family XNixBinApp p++type family XNixNotApp p++type family XNixNegApp p++type family XNixList p++type family XNixHasAttr p++type family XNixIf p++type family XNixWith p++type family XNixAssert p++type family XNixSelect p++type family XNixSet p++type family XNixLet p++type family XNixLam p++type family XNixVar p++type family XNixLit p++type family XNixPar p++--------------------------------------------------------------------------------
+ src/Nix/Lang/Types/Ps.hs view
@@ -0,0 +1,180 @@+{-# LANGUAGE DeriveDataTypeable #-}++-- | Annotated AST specialization used by the parser and exact printer.+--+-- This is the parsed form of the tree: every node is wrapped in location and+-- annotation data that records where it came from and how it should be rendered+-- back out.+--+-- It is the tree shape shared by 'Nix.Lang.Parser', 'Nix.Lang.ExactPrint', and+-- 'Nix.Lang.Edit'. If you are constructing Nix syntax from scratch instead,+-- prefer 'Nix.Lang.Types.Syn'.+--+module Nix.Lang.Types.Ps where++import Data.Data (Data)+import Data.Text (Text)+import Nix.Lang.Annotation+import Nix.Lang.Span (Located (..))+import Nix.Lang.Types++-- | Parser pass+data Ps deriving (Data)++type instance XRec Ps a = Located a++instance UnXRec Ps where+  unXRec _ (L _ x) = x++newtype SourceText = SourceText Text+  deriving (Eq, Show, Ord, Data)++type instance NixVarName Ps = Text++type instance NixBinderName Ps = Text++type instance NixAttrName Ps = Text++type instance XNixUri Ps = NoExtF++type instance XNixInteger Ps = NoExtF++type instance XNixFloat Ps = NoExtF++type instance XNixBoolean Ps = NoExtF++type instance XNixNull Ps = NoExtF++type instance XNixStringLiteral Ps = NoExtF++type instance XNixStringInterpol Ps = NoExtF++type instance XXNixStringPart Ps = NoExtC++type instance XNixDoubleQuotesString Ps = SourceText++type instance XNixDoubleSingleQuotesString Ps = SourceText++type instance XXNixString Ps = NoExtC++type instance XNixLiteralPath Ps = NoExtF++type instance XNixInterpolPath Ps = NoExtF++type instance XXNixPath Ps = NoExtC++type instance XNixStaticAttrKey Ps = NoExtF++type instance XNixDynamicStringAttrKey Ps = NoExtF++type instance XNixDynamicInterpolAttrKey Ps = NoExtF++type instance XXNixAttrKey Ps = NoExtC++type instance XNixAttrPath Ps = AnnAttrPath++type instance XNixNormalBinding Ps = AnnNormalBinding++type instance XNixInheritBinding Ps = AnnInheritBinding++type instance XXNixBinding Ps = NoExtC++type instance XNixVarPat Ps = AnnVarPat++type instance XNixSetPat Ps = AnnSetPatNode++type instance XNixSetPatAs Ps = AnnSetPatAs++type instance XNixSetPatBinding Ps = AnnSetPatBinding++type instance XXNixFuncPat Ps = NoExtC++type instance XNixVar Ps = AnnCommon++type instance XNixLit Ps = AnnCommon++type instance XNixPar Ps = AnnParNode++type instance XXNixLit Ps = NoExtC++type instance XNixString Ps = AnnStringNode++type instance XNixPath Ps = AnnPathNode++type instance XNixEnvPath Ps = AnnEnvPathNode++type instance XNixLam Ps = AnnLamNode++type instance XNixApp Ps = AnnAppNode++type instance XNixBinApp Ps = AnnBinAppNode++type instance XNixNotApp Ps = AnnPrefixNode++type instance XNixNegApp Ps = AnnPrefixNode++type instance XNixList Ps = AnnListNode++type instance XNixSet Ps = AnnSet++type instance XNixLet Ps = AnnLetNode++type instance XNixHasAttr Ps = AnnHasAttr++type instance XNixSelect Ps = AnnSelect++type instance XNixIf Ps = AnnIfNode++type instance XNixWith Ps = AnnWithNode++type instance XNixAssert Ps = AnnAssertNode++type instance XXNixExpr Ps = NoExtC++type Expr = NixExpr Ps++type LExpr = LNixExpr Ps++type Binding = NixBinding Ps++type LBinding = LNixBinding Ps++type AttrPath = NixAttrPath Ps++type LAttrPath = LNixAttrPath Ps++type AttrKey = NixAttrKey Ps++type LAttrKey = LNixAttrKey Ps++type FuncPat = NixFuncPat Ps++type LFuncPat = LNixFuncPat Ps++type Lit = NixLit Ps++type LLit = LNixLit Ps++type SetPatBinding = NixSetPatBinding Ps++type LSetPatBinding = Located SetPatBinding++type SetPatAs = NixSetPatAs Ps++type LSetPatAs = Located SetPatAs++type BinderName = NixBinderName Ps++type LBinderName = Located BinderName++type VarName = NixVarName Ps++type LVarName = Located VarName++type Path = NixPath Ps++type LPath = Located Path++type NString = NixString Ps++type LNString = Located NString
+ src/Nix/Lang/Types/Syn.hs view
@@ -0,0 +1,437 @@+{-# LANGUAGE DeriveDataTypeable #-}++-- | Fresh syntax-tree specialization with smart constructors.+--+-- This is the AST you reach for when you are building Nix code yourself rather+-- than recovering it from an existing file.+--+-- It gives you the concrete type aliases for the annotation-free pass together+-- with smart constructors such as 'mkVar', 'mkApp', and 'mkSet'. These trees are+-- the natural input to 'Nix.Lang.RFCPrint'.+--+-- Example construction:+--+-- @+-- import Nix.Lang.Types.Syn (Expr, mkLet, mkNormalBinding, mkVar)+--+-- expr :: Expr+-- expr = mkLet [mkNormalBinding (mkVar "x") (mkVar "y")] (mkVar "x")+-- @+module Nix.Lang.Types.Syn+  ( Syn,+    Expr,+    Binding,+    AttrPath,+    AttrKey,+    FuncPat,+    Lit,+    StringPart,+    NString,+    Path,+    SetPatAs,+    SetPatBinding,+    mkVar,+    mkLit,+    mkPar,+    mkString,+    mkPath,+    mkEnvPath,+    mkLam,+    mkApp,+    mkBinApp,+    mkNot,+    mkNeg,+    mkList,+    mkSet,+    mkRecSet,+    mkLet,+    mkHasAttr,+    mkSelect,+    mkSelectOr,+    mkIf,+    mkWith,+    mkAssert,+    mkUriLit,+    mkIntegerLit,+    mkFloatLit,+    mkBooleanLit,+    mkNullLit,+    mkUri,+    mkInt,+    mkFloat,+    mkBool,+    mkTrue,+    mkFalse,+    mkNull,+    mkStringLiteral,+    mkInterpol,+    mkDoubleQuotedString,+    mkIndentedNString,+    mkText,+    mkIndentedText,+    mkLiteralPath,+    mkInterpolatedPath,+    mkAttr,+    mkQuotedAttr,+    mkDynamicAttr,+    mkInterpolatedAttr,+    mkAttrs,+    mkAttrPath,+    mkNormalBinding,+    mkBinding,+    mkBindingPath,+    mkInheritKeys,+    mkInherit,+    mkInheritFromKeys,+    mkInheritFrom,+    mkOptionalBinding,+    mkOptionalBindingPath,+    mkStaticSet,+    mkStaticRecSet,+    mkStaticLet,+    mkSelectAttrs,+    mkSelectOrAttrs,+    mkHasAttrs,+    mkVarPat,+    mkSetPat,+    mkSetPatBinding,+    mkSetPatAs,+  )+where++import Data.Data (Data)+import Data.Text (Text)+import Nix.Lang.Types++-- | Syntax (or synthetic) pass+data Syn deriving (Data)++type instance XRec Syn a = a++instance UnXRec Syn where+  unXRec _ = id++type instance NixVarName Syn = Text++type instance NixBinderName Syn = Text++type instance NixAttrName Syn = Text++type instance XNixUri Syn = NoExtF++type instance XNixInteger Syn = NoExtF++type instance XNixFloat Syn = NoExtF++type instance XNixBoolean Syn = NoExtF++type instance XNixNull Syn = NoExtF++type instance XXNixLit Syn = NoExtC++type instance XNixStringLiteral Syn = NoExtF++type instance XNixStringInterpol Syn = NoExtF++type instance XXNixStringPart Syn = NoExtC++type instance XNixDoubleQuotesString Syn = NoExtF++type instance XNixDoubleSingleQuotesString Syn = NoExtF++type instance XXNixString Syn = NoExtC++type instance XNixLiteralPath Syn = NoExtF++type instance XNixInterpolPath Syn = NoExtF++type instance XXNixPath Syn = NoExtC++type instance XNixStaticAttrKey Syn = NoExtF++type instance XNixDynamicStringAttrKey Syn = NoExtF++type instance XNixDynamicInterpolAttrKey Syn = NoExtF++type instance XXNixAttrKey Syn = NoExtC++type instance XNixAttrPath Syn = NoExtF++type instance XNixNormalBinding Syn = NoExtF++type instance XNixInheritBinding Syn = NoExtF++type instance XXNixBinding Syn = NoExtC++type instance XNixVarPat Syn = NoExtF++type instance XNixSetPat Syn = NoExtF++type instance XNixSetPatAs Syn = NoExtF++type instance XNixSetPatBinding Syn = NoExtF++type instance XXNixFuncPat Syn = NoExtC++type instance XNixVar Syn = NoExtF++type instance XNixLit Syn = NoExtF++type instance XNixPar Syn = NoExtF++type instance XNixString Syn = NoExtF++type instance XNixPath Syn = NoExtF++type instance XNixEnvPath Syn = NoExtF++type instance XNixLam Syn = NoExtF++type instance XNixApp Syn = NoExtF++type instance XNixBinApp Syn = NoExtF++type instance XNixNotApp Syn = NoExtF++type instance XNixNegApp Syn = NoExtF++type instance XNixList Syn = NoExtF++type instance XNixSet Syn = NoExtF++type instance XNixLet Syn = NoExtF++type instance XNixHasAttr Syn = NoExtF++type instance XNixSelect Syn = NoExtF++type instance XNixIf Syn = NoExtF++type instance XNixWith Syn = NoExtF++type instance XNixAssert Syn = NoExtF++type instance XXNixExpr Syn = NoExtC++type Expr = NixExpr Syn++type Binding = NixBinding Syn++type AttrPath = NixAttrPath Syn++type AttrKey = NixAttrKey Syn++type FuncPat = NixFuncPat Syn++type Lit = NixLit Syn++type StringPart = NixStringPart Syn++type NString = NixString Syn++type Path = NixPath Syn++type SetPatAs = NixSetPatAs Syn++type SetPatBinding = NixSetPatBinding Syn++mkVar :: Text -> Expr+mkVar = NixVar NoExtF++mkLit :: Lit -> Expr+mkLit = NixLit NoExtF++mkPar :: Expr -> Expr+mkPar = NixPar NoExtF++mkString :: NString -> Expr+mkString = NixString NoExtF++mkPath :: Path -> Expr+mkPath = NixPath NoExtF++mkEnvPath :: Text -> Expr+mkEnvPath = NixEnvPath NoExtF++mkLam :: FuncPat -> Expr -> Expr+mkLam = NixLam NoExtF++mkApp :: Expr -> Expr -> Expr+mkApp = NixApp NoExtF++mkBinApp :: BinaryOp -> Expr -> Expr -> Expr+mkBinApp = NixBinApp NoExtF++mkNot :: Expr -> Expr+mkNot = NixNotApp NoExtF++mkNeg :: Expr -> Expr+mkNeg = NixNegApp NoExtF++mkList :: [Expr] -> Expr+mkList = NixList NoExtF++mkSet :: [Binding] -> Expr+mkSet = NixSet NoExtF NixSetNonRecursive++mkRecSet :: [Binding] -> Expr+mkRecSet = NixSet NoExtF NixSetRecursive++mkLet :: [Binding] -> Expr -> Expr+mkLet = NixLet NoExtF++mkHasAttr :: Expr -> AttrPath -> Expr+mkHasAttr = NixHasAttr NoExtF++mkSelect :: Expr -> AttrPath -> Expr+mkSelect expr path' = NixSelect NoExtF expr path' Nothing++mkSelectOr :: Expr -> AttrPath -> Expr -> Expr+mkSelectOr expr path' fallback = NixSelect NoExtF expr path' (Just fallback)++mkIf :: Expr -> Expr -> Expr -> Expr+mkIf = NixIf NoExtF++mkWith :: Expr -> Expr -> Expr+mkWith = NixWith NoExtF++mkAssert :: Expr -> Expr -> Expr+mkAssert = NixAssert NoExtF++mkUriLit :: Text -> Lit+mkUriLit = NixUri NoExtF++mkIntegerLit :: Integer -> Lit+mkIntegerLit = NixInteger NoExtF++mkFloatLit :: Float -> Lit+mkFloatLit = NixFloat NoExtF++mkBooleanLit :: Bool -> Lit+mkBooleanLit = NixBoolean NoExtF++mkNullLit :: Lit+mkNullLit = NixNull NoExtF++mkUri :: Text -> Expr+mkUri = mkLit . mkUriLit++mkInt :: Integer -> Expr+mkInt = mkLit . mkIntegerLit++mkFloat :: Float -> Expr+mkFloat = mkLit . mkFloatLit++mkBool :: Bool -> Expr+mkBool = mkLit . mkBooleanLit++mkTrue :: Expr+mkTrue = mkBool True++mkFalse :: Expr+mkFalse = mkBool False++mkNull :: Expr+mkNull = mkLit mkNullLit++mkStringLiteral :: Text -> StringPart+mkStringLiteral = NixStringLiteral NoExtF++mkInterpol :: Expr -> StringPart+mkInterpol = NixStringInterpol NoExtF++mkDoubleQuotedString :: [StringPart] -> NString+mkDoubleQuotedString = NixDoubleQuotesString NoExtF++mkIndentedNString :: [StringPart] -> NString+mkIndentedNString = NixDoubleSingleQuotesString NoExtF++mkText :: Text -> Expr+mkText value = mkString (mkDoubleQuotedString [mkStringLiteral value])++mkIndentedText :: Text -> Expr+mkIndentedText value = mkString (mkIndentedNString [mkStringLiteral value])++mkLiteralPath :: Text -> Path+mkLiteralPath = NixLiteralPath NoExtF++mkInterpolatedPath :: [StringPart] -> Path+mkInterpolatedPath = NixInterpolPath NoExtF++mkAttr :: Text -> AttrKey+mkAttr = NixStaticAttrKey NoExtF++mkQuotedAttr :: Text -> AttrKey+mkQuotedAttr value = mkDynamicAttr [mkStringLiteral value]++mkDynamicAttr :: [StringPart] -> AttrKey+mkDynamicAttr = NixDynamicStringAttrKey NoExtF++mkInterpolatedAttr :: Expr -> AttrKey+mkInterpolatedAttr = NixDynamicInterpolAttrKey NoExtF++mkAttrs :: [Text] -> AttrPath+mkAttrs = mkAttrPath . fmap mkAttr++mkAttrPath :: [AttrKey] -> AttrPath+mkAttrPath = NixAttrPath NoExtF++mkNormalBinding :: AttrPath -> Expr -> Binding+mkNormalBinding = NixNormalBinding NoExtF++mkBinding :: [Text] -> Expr -> Binding+mkBinding pathParts value = mkBindingPath (fmap mkAttr pathParts) value++mkBindingPath :: [AttrKey] -> Expr -> Binding+mkBindingPath keys value = mkNormalBinding (mkAttrPath keys) value++mkInheritKeys :: [AttrKey] -> Binding+mkInheritKeys = mkInheritBinding Nothing++mkInherit :: [Text] -> Binding+mkInherit = mkInheritKeys . fmap mkAttr++mkInheritFromKeys :: Expr -> [AttrKey] -> Binding+mkInheritFromKeys scope = mkInheritBinding (Just scope)++mkInheritFrom :: Expr -> [Text] -> Binding+mkInheritFrom scope = mkInheritFromKeys scope . fmap mkAttr++mkOptionalBinding :: [Text] -> Maybe Expr -> [Binding]+mkOptionalBinding pathParts = maybe [] (pure . mkBinding pathParts)++mkOptionalBindingPath :: [AttrKey] -> Maybe Expr -> [Binding]+mkOptionalBindingPath keys = maybe [] (pure . mkBindingPath keys)++mkStaticSet :: [(Text, Expr)] -> Expr+mkStaticSet = mkSet . fmap (uncurry (mkBinding . pure))++mkStaticRecSet :: [(Text, Expr)] -> Expr+mkStaticRecSet = mkRecSet . fmap (uncurry (mkBinding . pure))++mkStaticLet :: [(Text, Expr)] -> Expr -> Expr+mkStaticLet bindings body = mkLet (fmap (uncurry (mkBinding . pure)) bindings) body++mkSelectAttrs :: Expr -> [Text] -> Expr+mkSelectAttrs expr = mkSelect expr . mkAttrs++mkSelectOrAttrs :: Expr -> [Text] -> Expr -> Expr+mkSelectOrAttrs expr pathParts fallback = mkSelectOr expr (mkAttrs pathParts) fallback++mkHasAttrs :: Expr -> [Text] -> Expr+mkHasAttrs expr = mkHasAttr expr . mkAttrs++mkVarPat :: Text -> FuncPat+mkVarPat = NixVarPat NoExtF++mkSetPat :: NixSetPatEllipses -> Maybe (NixSetPatAs Syn) -> [NixSetPatBinding Syn] -> FuncPat+mkSetPat = NixSetPat NoExtF++mkSetPatBinding :: Text -> Maybe Expr -> NixSetPatBinding Syn+mkSetPatBinding = NixSetPatBinding NoExtF++mkSetPatAs :: NixSetPatAsLocation -> Text -> NixSetPatAs Syn+mkSetPatAs location name = NixSetPatAs NoExtF location name++mkInheritBinding :: Maybe Expr -> [AttrKey] -> Binding+mkInheritBinding = NixInheritBinding NoExtF
+ src/Nix/Lang/Utils.hs view
@@ -0,0 +1,136 @@+-- | Miscellaneous shared helpers for spans, tokens, and small AST utilities.+--+-- This is package plumbing: span math, token text, and a few small helpers that+-- get reused across parsing, exact printing, and editing.+module Nix.Lang.Utils where++import Data.Char (isSpace)+import Data.Maybe (fromJust)+import Data.Text (Text)+import qualified Data.Text as T+import Nix.Lang.Annotation+import Nix.Lang.Span+import Nix.Lang.Types++mkSrcSpan :: String -> (Int, Int) -> (Int, Int) -> SrcSpan+mkSrcSpan+  srcSpanFilename+  (srcSpanStartLine, srcSpanStartColumn)+  (srcSpanEndLine, srcSpanEndColumn)+    | (srcSpanEndLine, srcSpanEndColumn) >= (srcSpanStartLine, srcSpanStartColumn) = SrcSpan {..}+    | otherwise = error $ show (srcSpanEndLine, srcSpanEndColumn) <> " is less than " <> show (srcSpanStartLine, srcSpanStartColumn)++isSubspanOf ::+  SrcSpan ->+  SrcSpan ->+  Bool+isSubspanOf src parent+  | srcSpanFilename parent /= srcSpanFilename src = False+  | otherwise =+      (srcSpanStartLine parent, srcSpanStartColumn parent) <= (srcSpanStartLine src, srcSpanStartColumn src)+        && (srcSpanEndLine parent, srcSpanEndColumn parent) >= (srcSpanEndLine src, srcSpanEndColumn src)++combineSrcSpans :: SrcSpan -> SrcSpan -> SrcSpan+combineSrcSpans span1 span2 = SrcSpan f sl sc el ec+  where+    (sl, sc) =+      min+        (srcSpanStartLine span1, srcSpanStartColumn span1)+        (srcSpanStartLine span2, srcSpanStartColumn span2)+    (el, ec) =+      max+        (srcSpanEndLine span1, srcSpanEndColumn span1)+        (srcSpanEndLine span2, srcSpanEndColumn span2)+    f = srcSpanFilename span1++unLoc :: Located a -> a+unLoc (L _ x) = x++getLoc :: Located a -> SrcSpan+getLoc (L s _) = s++showToken :: Ann -> Maybe Text+showToken = \case+  AnnDoubleSingleQuotes -> Just "''"+  AnnDoubleQuote -> Just "\""+  AnnAssert -> Just "assert"+  AnnIf -> Just "if"+  AnnElse -> Just "else"+  AnnThen -> Just "then"+  AnnLet -> Just "let"+  AnnIn -> Just "in"+  AnnInherit -> Just "inherit"+  AnnRec -> Just "rec"+  AnnWith -> Just "with"+  AnnOpenC -> Just "{"+  AnnCloseC -> Just "}"+  AnnOpenS -> Just "["+  AnnCloseS -> Just "]"+  AnnOpenP -> Just "("+  AnnCloseP -> Just ")"+  AnnAssign -> Just "="+  AnnAt -> Just "@"+  AnnColon -> Just ":"+  AnnComma -> Just ","+  AnnDot -> Just "."+  AnnEllipsis -> Just "..."+  AnnQuestion -> Just "?"+  AnnSemicolon -> Just ";"+  AnnConcat -> Just "++"+  AnnUpdate -> Just "//"+  AnnEx -> Just "!"+  AnnAdd -> Just "+"+  AnnSub -> Just "-"+  AnnMul -> Just "*"+  AnnDiv -> Just "/"+  AnnAnd -> Just "&&"+  AnnOr -> Just "||"+  AnnImpl -> Just "->"+  AnnEqual -> Just "=="+  AnnNEqual -> Just "!="+  AnnGT -> Just ">"+  AnnGE -> Just ">="+  AnnLT -> Just "<"+  AnnLE -> Just "<="+  AnnId -> Nothing+  AnnVal -> Nothing+  AnnInterpolOpen -> Just "${"+  AnnInterpolClose -> Just "}"+  AnnEnvPathOpen -> Just "<"+  AnnEnvPathClose -> Just ">"+  AnnNeg -> Just "-"+  AnnEof -> Nothing++opToToken :: BinaryOp -> Ann+opToToken = \case+  OpConcat -> AnnConcat+  OpMul -> AnnMul+  OpDiv -> AnnDiv+  OpAdd -> AnnAdd+  OpSub -> AnnSub+  OpUpdate -> AnnUpdate+  OpLT -> AnnLT+  OpLE -> AnnLE+  OpGT -> AnnGT+  OpGE -> AnnGE+  OpEq -> AnnEqual+  OpNEq -> AnnNEqual+  OpAnd -> AnnAnd+  OpOr -> AnnOr+  OpImpl -> AnnImpl++showBinOP :: BinaryOp -> Text+showBinOP = fromJust . showToken . opToToken++-- >>> opString+-- "++*/+-//<<=>>===!=&&||->"+opString :: String+opString = T.unpack . T.concat $ showBinOP <$> [OpConcat ..]++stripCommonPrefix :: String -> String+stripCommonPrefix src = unlines $ drop commonIndent <$> ls+  where+    ls = lines src+    commonIndent = case length . takeWhile isSpace <$> filter (any $ not . isSpace) ls of+      [] -> 0+      indents -> minimum indents
+ test/Edit.hs view
@@ -0,0 +1,249 @@+{-# LANGUAGE QuasiQuotes #-}++module Edit where++import Nix.Lang.Edit+import Nix.Lang.ExactPrint+import Nix.Lang.QQ.Parsed (nixParsedQQ)+import qualified Nix.Lang.Types.Ps as Ps+import Nix.Lang.Utils+import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests =+  testGroup+    "edit"+    [ testCase "editExpr supports nested selector updates" editExprNestedSelectorUpdate,+      testCase "editExpr repairs root replacements" editExprRootReplacement,+      testCase "editExpr preserves unrelated let binding layout" editExprPreservesUnrelatedLetLayout,+      testCase "editExpr repairs shape-changing replacement locally" editExprShapeChangingReplacement,+      testCase "editExpr repairs list element replacement" editExprListElementReplacement,+      testCase "editExpr supports binding lookup by key" editExprBindingLookupByKey,+      testCase "editExpr supports binding lookup by path" editExprBindingLookupByPath,+      testCase "editExpr supports let binding insertion" editExprLetBindingInsertion,+      testCase "editExpr supports set binding insertion" editExprSetBindingInsertion,+      testCase "editExpr supports inherit binding insertion" editExprInheritBindingInsertion,+      testCase "editBinding supports inherit scope updates" editBindingInheritScopeUpdate,+      testCase "editBinding supports inherit key updates" editBindingInheritKeyUpdate,+      testCase "editBinding supports inherit key insertion" editBindingInheritKeyInsertion+    ]++editExprNestedSelectorUpdate :: Assertion+editExprNestedSelectorUpdate = do+  let expr =+        [nixParsedQQ|+          let+            f = {a, b}: a + b;+          in+            f { a = 1; b = 2; }+        |] ::+          Ps.LExpr+  case editExpr (letBody // appArgument // bindingAt 0 // bindingValue) (setIntLiteral 233) expr of+    Right x -> renderExactText (unLoc x) @?= "let\n  f = {a, b}: a + b;\nin\n  f { a = 233; b = 2; }"+    Left err -> fail $ show err++editExprPreservesUnrelatedLetLayout :: Assertion+editExprPreservesUnrelatedLetLayout = do+  let expr =+        [nixParsedQQ|+          let+            f = {a, b}: a + b;+            g = [+              1+              2+            ];+          in+            f { a = 1; b = 2; }+        |] ::+          Ps.LExpr+  case editExpr (letBody // appArgument // bindingAt 0 // bindingValue) (setIntLiteral 233) expr of+    Right x ->+      renderExactText (unLoc x)+        @?= "let\n  f = {a, b}: a + b;\n  g = [\n    1\n    2\n  ];\nin\n  f { a = 233; b = 2; }"+    Left err -> fail $ show err++editExprRootReplacement :: Assertion+editExprRootReplacement = do+  let expr =+        [nixParsedQQ|+          {+            a = 1;+            b = [+              2+              3+            ];+          }+        |] ::+          Ps.LExpr+      replacement =+        [nixParsedQQ|+          {+            a = 2;+            b = [+              3+              4+            ];+          }+        |] ::+          Ps.LExpr+  case editExpr root (replace replacement) expr of+    Right x ->+      renderExactText (unLoc x)+        @?= "{\n  a = 2;\n  b = [\n    3\n    4\n  ];\n}"+    Left err -> fail $ show err++editExprShapeChangingReplacement :: Assertion+editExprShapeChangingReplacement = do+  let expr =+        [nixParsedQQ|+          let+            value = 1;+          in+            [ value 2 ]+        |] ::+          Ps.LExpr+      replacement =+        [nixParsedQQ|+          value 20+        |] ::+          Ps.LExpr+  case editExpr (letBody // listElement 0) (replace replacement) expr of+    Right x ->+      renderExactText (unLoc x)+        @?= "let\n  value = 1;\nin\n  [\n    value 20\n    2\n            ]"+    Left err -> fail $ show err++editExprListElementReplacement :: Assertion+editExprListElementReplacement = do+  let expr =+        [nixParsedQQ|+          [+            1+            2+            3+          ]+        |] ::+          Ps.LExpr+  case editExpr (listElement 1) (setIntLiteral 200) expr of+    Right x ->+      renderExactText (unLoc x)+        @?= "[\n  1\n  200\n  3\n]"+    Left err -> fail $ show err++editExprBindingLookupByKey :: Assertion+editExprBindingLookupByKey = do+  let expr =+        [nixParsedQQ|+          {+            a = 1;+            b = 2;+          }+        |] ::+          Ps.LExpr+  case editExpr (bindingByKey "b" // bindingValue) (setIntLiteral 20) expr of+    Right x ->+      renderExactText (unLoc x)+        @?= "{\n  a = 1;\n  b = 20;\n}"+    Left err -> fail $ show err++editExprBindingLookupByPath :: Assertion+editExprBindingLookupByPath = do+  let expr =+        [nixParsedQQ|+          {+            services.nginx.enable = false;+          }+        |] ::+          Ps.LExpr+  case editExpr (bindingByPath ["services", "nginx", "enable"] // bindingValue) (replaceExprText "true") expr of+    Right x ->+      renderExactText (unLoc x)+        @?= "{\n  services.nginx.enable = true;\n}"+    Left err -> fail $ show err++editExprLetBindingInsertion :: Assertion+editExprLetBindingInsertion = do+  let expr =+        [nixParsedQQ|+          let+            a = 1;+          in+            a+        |] ::+          Ps.LExpr+  case editExpr root (insertBindingText (InsertBindingAt 1) "b = 2;") expr of+    Right x ->+      renderExactText (unLoc x)+        @?= "let\n  a = 1;\n  b = 2;\nin\n  a"+    Left err -> fail $ show err++editExprSetBindingInsertion :: Assertion+editExprSetBindingInsertion = do+  let expr =+        [nixParsedQQ|+          {+            a = 1;+            c = 3;+          }+        |] ::+          Ps.LExpr+  case editExpr root (insertBindingText (InsertBindingAt 1) "b = 2;") expr of+    Right x ->+      renderExactText (unLoc x)+        @?= "{\n  a = 1;\n  b = 2;\n  c = 3;\n}"+    Left err -> fail $ show err++editExprInheritBindingInsertion :: Assertion+editExprInheritBindingInsertion = do+  let expr =+        [nixParsedQQ|+          {+            a = 1;+          }+        |] ::+          Ps.LExpr+  case editExpr root (insertBindingText (InsertBindingAt 0) "inherit (pkgs) lib;") expr of+    Right x ->+      renderExactText (unLoc x)+        @?= "{\n  inherit (pkgs) lib;\n  a = 1;\n}"+    Left err -> fail $ show err++editBindingInheritScopeUpdate :: Assertion+editBindingInheritScopeUpdate = do+  let expr =+        [nixParsedQQ|+          {+            inherit (pkgs) lib;+          }+        |] ::+          Ps.LExpr+  case editExpr (bindingByKey "lib" // inheritScope) (replaceExprText "scope") expr of+    Right x -> renderExactText (unLoc x) @?= "{\n  inherit (scope) lib;\n}"+    Left err -> fail $ show err++editBindingInheritKeyUpdate :: Assertion+editBindingInheritKeyUpdate = do+  let expr =+        [nixParsedQQ|+          {+            inherit (pkgs) old other;+          }+        |] ::+          Ps.LExpr+  case editExpr (bindingByKey "old" // inheritKey "old") (replaceAttrKeyText "new") expr of+    Right x -> renderExactText (unLoc x) @?= "{\n  inherit (pkgs) new other;\n}"+    Left err -> fail $ show err++editBindingInheritKeyInsertion :: Assertion+editBindingInheritKeyInsertion = do+  let expr =+        [nixParsedQQ|+          {+            inherit (pkgs) a c;+          }+        |] ::+          Ps.LExpr+  case editExpr (bindingByKey "a") (insertInheritKeyTextAt 1 "b") expr of+    Right x -> renderExactText (unLoc x) @?= "{\n  inherit (pkgs) a b c;\n}"+    Left err -> fail $ show err
+ test/ExactPrint.hs view
@@ -0,0 +1,536 @@+module ExactPrint where++import qualified Data.Text as T+import qualified Data.Text.IO as T+import Nix.Lang.Annotation+import Nix.Lang.ExactPrint+import qualified Nix.Lang.ExactPrint.Operations as Ops+import Nix.Lang.Parser+import Nix.Lang.Span+import Nix.Lang.Types+import Nix.Lang.Utils+import Test.Tasty+import Test.Tasty.HUnit+import Text.Megaparsec (eof, errorBundlePretty)+import Utils++tests :: TestTree+tests =+  testGroup+    "exact print"+    [ testGroup+        "state and layout"+        [ testCase "line comments are collected" lineCommentsCollected,+          testCase "block comments are collected" blockCommentsCollected,+          testCase "file-leading comments attach to root node prior comments" fileLeadingCommentsAttachToRoot,+          testCase "file-trailing comments attach to root node following comments" fileTrailingCommentsAttachToRoot,+          testCase "list carries tokenized delimiter annotations" listCarriesTokenizedDelimiters,+          testCase "list layout prep rewrites delimiters to deltas" listLayoutPrepRewritesDelimitersToDeltas,+          testCase "paren layout prep rewrites close delimiter to delta" parenLayoutPrepRewritesCloseDelimiterToDelta,+          testCase "set layout prep rewrites close delimiter to delta" setLayoutPrepRewritesCloseDelimiterToDelta,+          testCase "let layout prep rewrites in token to delta" letLayoutPrepRewritesInTokenToDelta,+          testCase "if layout prep rewrites then/else tokens to deltas" ifLayoutPrepRewritesThenElseTokensToDeltas,+          testCase "with layout prep rewrites semicolon to delta" withLayoutPrepRewritesSemicolonToDelta,+          testCase "assert layout prep rewrites semicolon to delta" assertLayoutPrepRewritesSemicolonToDelta,+          testCase "has-attr layout prep rewrites question token to delta" hasAttrLayoutPrepRewritesQuestionToDelta,+          testCase "select layout prep rewrites or token to delta" selectLayoutPrepRewritesOrTokenToDelta,+          testCase "deltaFromAnchor same line computes column delta" deltaFromAnchorSameLine,+          testCase "deltaFromAnchor multiline computes line and indentation" deltaFromAnchorMultiline,+          testCase "renderGapFromDeltaText renders whitespace" renderGapFromDeltaTextRendersWhitespace,+          testCase "advanceCursor tracks newlines" advanceCursorTracksNewlines,+          testCase "assert carries tokenized keyword and semicolon annotations" assertCarriesTokenizedDelimiters,+          testCase "set carries tokenized brace annotations" setCarriesTokenizedBraces,+          testCase "assert carries typed keyword and semicolon spans" assertAnnotationsCollected,+          testCase "set carries typed annotation comments" setCarriesTypedAnnComments,+          testCase "isSubspanOf checks end columns" isSubspanOfChecksEndColumns+        ],+      testGroup+        "roundtrip"+        [ testCase "exact print roundtrips quoted string source" exactPrintRoundtripsQuotedStringSource,+          testCase "exact print roundtrips env path" exactPrintRoundtripsEnvPath,+          testCase "exact print roundtrips multiline list" exactPrintRoundtripsMultilineList,+          testCase "exact print roundtrips multiline set" exactPrintRoundtripsMultilineSet,+          testCase "exact print roundtrips multiline let" exactPrintRoundtripsMultilineLet,+          testCase "exact print roundtrips multiline if" exactPrintRoundtripsMultilineIf,+          testCase "exact print roundtrips multiline with" exactPrintRoundtripsMultilineWith,+          testCase "exact print roundtrips multiline assert" exactPrintRoundtripsMultilineAssert,+          testCase "exact print roundtrips multiline has-attr" exactPrintRoundtripsMultilineHasAttr,+          testCase "exact print roundtrips multiline select or" exactPrintRoundtripsMultilineSelectOr,+          testCase "exact print roundtrips sample fixture" exactPrintRoundtripsSampleFixture,+          testCase "exact print preserves lambda comments from sample shape" exactPrintPreservesLambdaCommentShape,+          testCase "exact print preserves inline close comment without whitespace" exactPrintPreservesCommentNoWhitespace,+          testCase "exact print preserves trailing comma set pattern" exactPrintPreservesTrailingCommaSetPattern,+          testCase "exact print preserves interpolated select attr path" exactPrintPreservesInterpolatedSelectAttrPath,+          testCase "exact print safe API preserves trailing as-pattern" exactPrintSafeApiPreservesTrailingAsPattern,+          testCase "exact print preserves quoted string source text" exactPrintPreservesQuotedStringSource,+          testCase "exact print preserves indented string source text" exactPrintPreservesIndentedStringSource,+          testCase "exact print preserves literal path" exactPrintPreservesLiteralPath,+          testCase "exact print preserves interpolated path structurally" exactPrintPreservesInterpolatedPath,+          testCase "exact print preserves env path" exactPrintPreservesEnvPath,+          testCase "exact print preserves dynamic attr binding" exactPrintPreservesDynamicAttrBinding,+          testCase "exact print preserves dynamic string attr binding" exactPrintPreservesDynamicStringAttrBinding,+          testCase "exact print preserves structural dynamic interpol attr binding" exactPrintPreservesStructuralDynamicInterpolAttrBinding,+          testCase "exact print preserves inherit binding" exactPrintPreservesInheritBinding,+          testCase "exact print preserves lambda var pattern" exactPrintPreservesLambdaVarPattern,+          testCase "exact print preserves lambda set pattern" exactPrintPreservesLambdaSetPattern,+          testCase "exact print preserves multiline lambda chain layout" exactPrintPreservesMultilineLambdaChainLayout,+          testCase "exact print preserves application" exactPrintPreservesApplication,+          testCase "exact print preserves has-attr" exactPrintPreservesHasAttr,+          testCase "exact print preserves with expression" exactPrintPreservesWithExpr,+          testCase "exact print preserves assert expression" exactPrintPreservesAssertExpr,+          testCase "exact print emits file leading and trailing comments" exactPrintEmitsRootComments,+          testCase "exact print emits list trailing comments before close" exactPrintEmitsClosingDelimitedComments,+          testCase "exact print preserves multiline list layout" exactPrintPreservesMultilineListLayout,+          testCase "exact print preserves multiline set layout" exactPrintPreservesMultilineSetLayout,+          testCase "exact print preserves multiline let layout" exactPrintPreservesMultilineLetLayout,+          testCase "exact print preserves multiline if layout" exactPrintPreservesMultilineIfLayout+        ],+      referenceExactPrintFixtureTests+    ]++assertExactPrintRoundtripExpr :: T.Text -> Assertion+assertExactPrintRoundtripExpr src =+  case runNixParser (nixExpr <* eof) "<expr>" src of+    (Right expr, _) -> renderExactText expr @?= src+    (Left err, _) -> assertFailure $ errorBundlePretty err++assertExactPrintRoundtripFile :: FilePath -> T.Text -> Assertion+assertExactPrintRoundtripFile fp src =+  case runNixParser nixFile fp src of+    (Right expr, _) -> renderExactText expr @?= src+    (Left err, _) -> assertFailure $ errorBundlePretty err++lineCommentsCollected :: Assertion+lineCommentsCollected = do+  expr <- parseFileOrFail "<comments>" "{\n  # hello\n  x = 1;\n}"+  case expr of+    NixSet _ _ (L _ [L _ (NixNormalBinding _ (L _ (NixAttrPath ann _)) _)]) ->+      assertBool "expected collected line comment on first attr path" $ any isHelloLine (priorComments (acComments (aapCommon ann)))+    _ -> assertFailure $ "expected set expression, got: " <> show expr+  where+    isHelloLine (L _ (LineComment txt)) = txt == " hello"+    isHelloLine _ = False++blockCommentsCollected :: Assertion+blockCommentsCollected = do+  expr <- parseFileOrFail "<comments>" "[ a /* hello */ ]"+  case expr of+    NixList ann _ -> assertBool "expected collected block comment before closing bracket" $ any isHelloBlock (followingComments (acComments (alnCommon ann)))+    _ -> assertFailure $ "expected list expression, got: " <> show expr+  where+    isHelloBlock (L _ (BlockComment txt)) = txt == " hello "+    isHelloBlock _ = False++fileLeadingCommentsAttachToRoot :: Assertion+fileLeadingCommentsAttachToRoot = do+  expr <- parseFileOrFail "<comments>" "# hello\n1"+  case expr of+    NixLit ann _ -> assertBool "expected file-leading comment on root prior comments" $ any isHelloLine (priorComments (acComments ann))+    _ -> assertFailure $ "expected literal expression, got: " <> show expr+  where+    isHelloLine (L _ (LineComment txt)) = txt == " hello"+    isHelloLine _ = False++fileTrailingCommentsAttachToRoot :: Assertion+fileTrailingCommentsAttachToRoot = do+  expr <- parseFileOrFail "<comments>" "1\n# hello"+  case expr of+    NixLit ann _ -> assertBool "expected file-trailing comment on root following comments" $ any isHelloLine (followingComments (acComments ann))+    _ -> assertFailure $ "expected literal expression, got: " <> show expr+  where+    isHelloLine (L _ (LineComment txt)) = txt == " hello"+    isHelloLine _ = False++listCarriesTokenizedDelimiters :: Assertion+listCarriesTokenizedDelimiters = do+  expr <- parseExprOrFail "[1 2 3]"+  case expr of+    NixList ann _ -> do+      annToken (alnOpenS ann) @?= AnnOpenS+      annToken (alnCloseS ann) @?= AnnCloseS+      annTokenSrcSpan (alnOpenS ann) @?= Just (mkSrcSpan "<expr>" (1, 1) (1, 2))+      annTokenSrcSpan (alnCloseS ann) @?= Just (mkSrcSpan "<expr>" (1, 7) (1, 8))+    _ -> assertFailure $ "expected list expression, got: " <> show expr++listLayoutPrepRewritesDelimitersToDeltas :: Assertion+listLayoutPrepRewritesDelimitersToDeltas = do+  expr <- parseExprOrFail "[\n  a\n  b\n]"+  case expr of+    NixList ann xs -> do+      let ann' = Ops.prepareListLayout ann xs+      annTokenSrcSpan (alnOpenS ann') @?= annTokenSrcSpan (alnOpenS ann)+      annTokenDelta (alnCloseS ann') @?= Just (DeltaPos 1 0)+      annTokenSrcSpan (alnCloseS ann') @?= Nothing+    _ -> assertFailure $ "expected list expression, got: " <> show expr++parenLayoutPrepRewritesCloseDelimiterToDelta :: Assertion+parenLayoutPrepRewritesCloseDelimiterToDelta = do+  expr <- parseExprOrFail "(\n  1\n)"+  case expr of+    NixPar ann inner -> do+      let ann' = Ops.prepareParLayout ann (unLoc inner)+      annTokenSrcSpan (apnOpenP ann') @?= annTokenSrcSpan (apnOpenP ann)+      annTokenDelta (apnCloseP ann') @?= Just (DeltaPos 1 0)+      annTokenSrcSpan (apnCloseP ann') @?= Nothing+    _ -> assertFailure $ "expected parenthesized expression, got: " <> show expr++setLayoutPrepRewritesCloseDelimiterToDelta :: Assertion+setLayoutPrepRewritesCloseDelimiterToDelta = do+  expr <- parseExprOrFail "{\n  x = 1;\n}"+  case expr of+    NixSet ann _ (L _ bindings) -> do+      let ann' = Ops.prepareSetLayout ann bindings+      annTokenSrcSpan (asOpenC ann') @?= annTokenSrcSpan (asOpenC ann)+      annTokenDelta (asCloseC ann') @?= Just (DeltaPos 1 0)+      annTokenSrcSpan (asCloseC ann') @?= Nothing+    _ -> assertFailure $ "expected set expression, got: " <> show expr++letLayoutPrepRewritesInTokenToDelta :: Assertion+letLayoutPrepRewritesInTokenToDelta = do+  expr <- parseExprOrFail "let\n  x = 1;\nin\n  x"+  case expr of+    NixLet ann (L _ bindings) body -> do+      let ann' = Ops.prepareLetLayout ann bindings (unLoc body)+      annTokenDelta (alIn ann') @?= Just (DeltaPos 1 0)+      annTokenSrcSpan (alIn ann') @?= Nothing+    _ -> assertFailure $ "expected let expression, got: " <> show expr++ifLayoutPrepRewritesThenElseTokensToDeltas :: Assertion+ifLayoutPrepRewritesThenElseTokensToDeltas = do+  expr <- parseExprOrFail "if\n  a\nthen\n  b\nelse\n  c"+  case expr of+    NixIf ann cond thenExpr elseExpr -> do+      let ann' = Ops.prepareIfLayout ann (unLoc cond) (unLoc thenExpr) (unLoc elseExpr)+      annTokenDelta (aifThen ann') @?= Just (DeltaPos 1 0)+      annTokenDelta (aifElse ann') @?= Just (DeltaPos 1 0)+      annTokenSrcSpan (aifThen ann') @?= Nothing+      annTokenSrcSpan (aifElse ann') @?= Nothing+    _ -> assertFailure $ "expected if expression, got: " <> show expr++withLayoutPrepRewritesSemicolonToDelta :: Assertion+withLayoutPrepRewritesSemicolonToDelta = do+  expr <- parseExprOrFail "with\n  l\n;\n  1"+  case expr of+    NixWith ann scope body -> do+      let ann' = Ops.prepareWithLayout ann (unLoc scope) (unLoc body)+      annTokenDelta (awSemicolon ann') @?= Just (DeltaPos 1 0)+      annTokenSrcSpan (awSemicolon ann') @?= Nothing+    _ -> assertFailure $ "expected with expression, got: " <> show expr++assertLayoutPrepRewritesSemicolonToDelta :: Assertion+assertLayoutPrepRewritesSemicolonToDelta = do+  expr <- parseExprOrFail "assert\n  a == 1\n;\n  2"+  case expr of+    NixAssert ann assertion body -> do+      let ann' = Ops.prepareAssertLayout ann (unLoc assertion) (unLoc body)+      annTokenDelta (aaSemicolon ann') @?= Just (DeltaPos 1 0)+      annTokenSrcSpan (aaSemicolon ann') @?= Nothing+    _ -> assertFailure $ "expected assert expression, got: " <> show expr++hasAttrLayoutPrepRewritesQuestionToDelta :: Assertion+hasAttrLayoutPrepRewritesQuestionToDelta = do+  expr <- parseExprOrFail "a\n?\n  b"+  case expr of+    NixHasAttr ann lhs rhs -> do+      let ann' = Ops.prepareHasAttrLayout ann (unLoc lhs) (unLoc rhs)+      annTokenDelta (ahaQuestion ann') @?= Just (DeltaPos 1 0)+      annTokenSrcSpan (ahaQuestion ann') @?= Nothing+    _ -> assertFailure $ "expected has-attr expression, got: " <> show expr++selectLayoutPrepRewritesOrTokenToDelta :: Assertion+selectLayoutPrepRewritesOrTokenToDelta = do+  expr <- parseExprOrFail "a.b.c\nor\n  d.e"+  case expr of+    NixSelect ann lhs path (Just defExpr) -> do+      let ann' = Ops.prepareSelectLayout ann (unLoc lhs) (unLoc path) (Just defExpr)+      case aslOr ann' of+        Just tok -> do+          annTokenDelta tok @?= Just (DeltaPos 1 0)+          annTokenSrcSpan tok @?= Nothing+        Nothing -> assertFailure "expected or token"+    _ -> assertFailure $ "expected select expression, got: " <> show expr++deltaFromAnchorSameLine :: Assertion+deltaFromAnchorSameLine =+  Ops.deltaFromAnchor (mkSrcSpan "<test>" (1, 1) (1, 4)) (mkSrcSpan "<test>" (1, 7) (1, 8)) @?= DeltaPos 0 3++deltaFromAnchorMultiline :: Assertion+deltaFromAnchorMultiline =+  Ops.deltaFromAnchor (mkSrcSpan "<test>" (1, 3) (1, 8)) (mkSrcSpan "<test>" (3, 5) (3, 6)) @?= DeltaPos 2 4++renderGapFromDeltaTextRendersWhitespace :: Assertion+renderGapFromDeltaTextRendersWhitespace = do+  Ops.renderGapFromDeltaText (DeltaPos 0 3) @?= "   "+  Ops.renderGapFromDeltaText (DeltaPos 2 4) @?= "\n\n    "++advanceCursorTracksNewlines :: Assertion+advanceCursorTracksNewlines =+  Ops.advanceCursor (Ops.RenderCursor 1 1) "ab\n c" @?= Ops.RenderCursor 2 3++assertAnnotationsCollected :: Assertion+assertAnnotationsCollected = do+  expr <- parseExprOrFail "assert a == 1; 2"+  case expr of+    NixAssert ann _ _ -> do+      fmap srcSpanStartColumn (annTokenSrcSpan (aaAssert ann)) @?= Just 1+      fmap srcSpanStartColumn (annTokenSrcSpan (aaSemicolon ann)) @?= Just 14+    _ -> assertFailure $ "expected assert expression, got: " <> show expr++assertCarriesTokenizedDelimiters :: Assertion+assertCarriesTokenizedDelimiters = do+  expr <- parseExprOrFail "assert a == 1; 2"+  case expr of+    NixAssert ann _ _ -> do+      annToken (aaAssert ann) @?= AnnAssert+      annToken (aaSemicolon ann) @?= AnnSemicolon+      annTokenSrcSpan (aaAssert ann) @?= Just (mkSrcSpan "<expr>" (1, 1) (1, 7))+      annTokenSrcSpan (aaSemicolon ann) @?= Just (mkSrcSpan "<expr>" (1, 14) (1, 15))+    _ -> assertFailure $ "expected assert expression, got: " <> show expr++setCarriesTokenizedBraces :: Assertion+setCarriesTokenizedBraces = do+  expr <- parseExprOrFail "{ x = 1; }"+  case expr of+    NixSet ann _ _ -> do+      annToken (asOpenC ann) @?= AnnOpenC+      annToken (asCloseC ann) @?= AnnCloseC+      annTokenSrcSpan (asOpenC ann) @?= Just (mkSrcSpan "<expr>" (1, 1) (1, 2))+      annTokenSrcSpan (asCloseC ann) @?= Just (mkSrcSpan "<expr>" (1, 10) (1, 11))+    _ -> assertFailure $ "expected set expression, got: " <> show expr++setCarriesTypedAnnComments :: Assertion+setCarriesTypedAnnComments = do+  expr <- parseFileOrFail "<comments>" "{\n  # hello\n  x = 1;\n}"+  case expr of+    NixSet ann _ _ -> do+      fmap srcSpanStartColumn (annTokenSrcSpan (asOpenC ann)) @?= Just 1+      fmap srcSpanStartColumn (annTokenSrcSpan (asCloseC ann)) @?= Just 1+      acComments (asCommon ann) @?= emptyComments+    _ -> assertFailure $ "expected set expression, got: " <> show expr++isSubspanOfChecksEndColumns :: Assertion+isSubspanOfChecksEndColumns = do+  let parent = mkSrcSpan "<test>" (1, 1) (1, 5)+      inside = mkSrcSpan "<test>" (1, 2) (1, 5)+      outside = mkSrcSpan "<test>" (1, 2) (1, 6)+  assertBool "subspan ending at parent column should be inside" $ inside `isSubspanOf` parent+  assertBool "subspan ending after parent column should be outside" $ not (outside `isSubspanOf` parent)++exactPrintPreservesQuotedStringSource :: Assertion+exactPrintPreservesQuotedStringSource = do+  case runNixParser (nixExpr <* eof) "<expr>" "\"a$${b}c\"" of+    (Right expr, _) -> renderExactText expr @?= "\"a$${b}c\""+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintRoundtripsQuotedStringSource :: Assertion+exactPrintRoundtripsQuotedStringSource = assertExactPrintRoundtripExpr "\"a$${b}c\""++exactPrintRoundtripsEnvPath :: Assertion+exactPrintRoundtripsEnvPath = assertExactPrintRoundtripExpr "<nixpkgs/nixos>"++exactPrintRoundtripsMultilineList :: Assertion+exactPrintRoundtripsMultilineList = assertExactPrintRoundtripFile "<list>" "[\n  a\n  b\n]"++exactPrintRoundtripsMultilineSet :: Assertion+exactPrintRoundtripsMultilineSet = assertExactPrintRoundtripFile "<set>" "{\n  x = 1;\n  y = 2;\n}"++exactPrintRoundtripsMultilineLet :: Assertion+exactPrintRoundtripsMultilineLet = assertExactPrintRoundtripFile "<let>" "let\n  x = 1;\nin\n  x"++exactPrintRoundtripsMultilineIf :: Assertion+exactPrintRoundtripsMultilineIf = assertExactPrintRoundtripFile "<if>" "if\n  a\nthen\n  b\nelse\n  c"++exactPrintRoundtripsMultilineWith :: Assertion+exactPrintRoundtripsMultilineWith = assertExactPrintRoundtripFile "<with>" "with\n  l\n;\n  1"++exactPrintRoundtripsMultilineAssert :: Assertion+exactPrintRoundtripsMultilineAssert = assertExactPrintRoundtripFile "<assert>" "assert\n  a == 1\n;\n  2"++exactPrintRoundtripsMultilineHasAttr :: Assertion+exactPrintRoundtripsMultilineHasAttr = assertExactPrintRoundtripFile "<has-attr>" "a\n?\n  b"++exactPrintRoundtripsMultilineSelectOr :: Assertion+exactPrintRoundtripsMultilineSelectOr = assertExactPrintRoundtripFile "<select>" "a.b.c\nor\n  d.e"++exactPrintRoundtripsSampleFixture :: Assertion+exactPrintRoundtripsSampleFixture = do+  src <- T.readFile "test/sample.nix"+  assertExactPrintRoundtripFile "test/sample.nix" (maybe src id (T.stripSuffix "\n" src))++exactPrintPreservesLambdaCommentShape :: Assertion+exactPrintPreservesLambdaCommentShape =+  assertExactPrintRoundtripFile+    "<lambda-comments>"+    "let\n  # comment f\n  f =\n    x:\n    # comment y\n    y:\n    x;\nin f"++exactPrintPreservesCommentNoWhitespace :: Assertion+exactPrintPreservesCommentNoWhitespace =+  assertExactPrintRoundtripFile "<comment-no-ws>" "{ comment_no_ws = [a/**/]; }"++exactPrintPreservesTrailingCommaSetPattern :: Assertion+exactPrintPreservesTrailingCommaSetPattern =+  assertExactPrintRoundtripFile "<param-comma>" "{x,}: x"++exactPrintPreservesInterpolatedSelectAttrPath :: Assertion+exactPrintPreservesInterpolatedSelectAttrPath =+  assertExactPrintRoundtripFile "<sel-interpol>" "a.b.${c}.\"d\".\"${\"e\"}\""++exactPrintSafeApiPreservesTrailingAsPattern :: Assertion+exactPrintSafeApiPreservesTrailingAsPattern = do+  case runNixParser nixFile "<lambda>" "{x}@a: x" of+    (Right expr, _) -> renderExactTextM expr @?= Right "{x}@a: x"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesIndentedStringSource :: Assertion+exactPrintPreservesIndentedStringSource = do+  case runNixParser (nixExpr <* eof) "<expr>" "''a${b}c''" of+    (Right expr, _) -> renderExactText expr @?= "''a${b}c''"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesLiteralPath :: Assertion+exactPrintPreservesLiteralPath = do+  case runNixParser (nixExpr <* eof) "<expr>" "./nix" of+    (Right expr, _) -> renderExactText expr @?= "./nix"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesInterpolatedPath :: Assertion+exactPrintPreservesInterpolatedPath = do+  case runNixParser (nixExpr <* eof) "<expr>" "./${a}-${b}/c/d${e}" of+    (Right expr, _) -> renderExactText expr @?= "./${a}-${b}/c/d${e}"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesEnvPath :: Assertion+exactPrintPreservesEnvPath = do+  case runNixParser (nixExpr <* eof) "<expr>" "<nixpkgs/nixos>" of+    (Right expr, _) -> renderExactText expr @?= "<nixpkgs/nixos>"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesDynamicAttrBinding :: Assertion+exactPrintPreservesDynamicAttrBinding = do+  case runNixParser nixFile "<set>" "{ \"${dynamic}\".${attr} = g; }" of+    (Right expr, _) -> renderExactText expr @?= "{ \"${dynamic}\".${attr} = g; }"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesDynamicStringAttrBinding :: Assertion+exactPrintPreservesDynamicStringAttrBinding = do+  case runNixParser nixFile "<set>" "{ \"a${b}c\" = g; }" of+    (Right expr, _) -> renderExactText expr @?= "{ \"a${b}c\" = g; }"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesStructuralDynamicInterpolAttrBinding :: Assertion+exactPrintPreservesStructuralDynamicInterpolAttrBinding = do+  case runNixParser nixFile "<set>" "{ ${a + b} = g; }" of+    (Right expr, _) -> renderExactText expr @?= "{ ${a + b} = g; }"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesInheritBinding :: Assertion+exactPrintPreservesInheritBinding = do+  case runNixParser nixFile "<set>" "{ inherit (s) m g; }" of+    (Right expr, _) -> renderExactText expr @?= "{ inherit (s) m g; }"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesLambdaVarPattern :: Assertion+exactPrintPreservesLambdaVarPattern = do+  case runNixParser nixFile "<lambda>" "x: x" of+    (Right expr, _) -> renderExactText expr @?= "x: x"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesLambdaSetPattern :: Assertion+exactPrintPreservesLambdaSetPattern = do+  case runNixParser nixFile "<lambda>" "{ x ? 1, y ? {}, ... }: x" of+    (Right expr, _) -> renderExactText expr @?= "{ x ? 1, y ? {}, ... }: x"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesMultilineLambdaChainLayout :: Assertion+exactPrintPreservesMultilineLambdaChainLayout =+  assertExactPrintRoundtripFile "<lambda-multiline>" "x:\n  y:\n  x"++exactPrintPreservesApplication :: Assertion+exactPrintPreservesApplication = do+  case runNixParser nixFile "<app>" "a b c" of+    (Right expr, _) -> renderExactText expr @?= "a b c"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesHasAttr :: Assertion+exactPrintPreservesHasAttr = do+  case runNixParser nixFile "<has-attr>" "a ? b" of+    (Right expr, _) -> renderExactText expr @?= "a ? b"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesWithExpr :: Assertion+exactPrintPreservesWithExpr = do+  case runNixParser nixFile "<with>" "with l; 1" of+    (Right expr, _) -> renderExactText expr @?= "with l; 1"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesAssertExpr :: Assertion+exactPrintPreservesAssertExpr = do+  case runNixParser nixFile "<assert>" "assert a == 1; 2" of+    (Right expr, _) -> renderExactText expr @?= "assert a == 1; 2"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintEmitsRootComments :: Assertion+exactPrintEmitsRootComments = do+  case runNixParser nixFile "<comments>" "# hello\n1\n# bye" of+    (Right expr, _) -> renderExactText expr @?= "# hello\n1\n# bye"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintEmitsClosingDelimitedComments :: Assertion+exactPrintEmitsClosingDelimitedComments = do+  case runNixParser nixFile "<comments>" "[ a /* hello */ ]" of+    (Right expr, _) -> renderExactText expr @?= "[ a /* hello */ ]"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesMultilineListLayout :: Assertion+exactPrintPreservesMultilineListLayout = do+  case runNixParser nixFile "<list>" "[\n  a\n  b\n]" of+    (Right expr, _) -> renderExactText expr @?= "[\n  a\n  b\n]"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesMultilineSetLayout :: Assertion+exactPrintPreservesMultilineSetLayout = do+  case runNixParser nixFile "<set>" "{\n  x = 1;\n  y = 2;\n}" of+    (Right expr, _) -> renderExactText expr @?= "{\n  x = 1;\n  y = 2;\n}"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesMultilineLetLayout :: Assertion+exactPrintPreservesMultilineLetLayout = do+  case runNixParser nixFile "<let>" "let\n  x = 1;\nin\n  x" of+    (Right expr, _) -> renderExactText expr @?= "let\n  x = 1;\nin\n  x"+    (Left err, _) -> assertFailure $ errorBundlePretty err++exactPrintPreservesMultilineIfLayout :: Assertion+exactPrintPreservesMultilineIfLayout = do+  case runNixParser nixFile "<if>" "if\n  a\nthen\n  b\nelse\n  c" of+    (Right expr, _) -> renderExactText expr @?= "if\n  a\nthen\n  b\nelse\n  c"+    (Left err, _) -> assertFailure $ errorBundlePretty err++referenceExactPrintFixtureTests :: TestTree+referenceExactPrintFixtureTests =+  testGroup+    "reference exact-print fixtures"+    [ testGroup "roundtrip" (fixtureRoundtripCase <$> roundtripFixtures)+    ]++fixtureRoundtripCase :: FilePath -> TestTree+fixtureRoundtripCase relPath =+  testCase relPath $ do+    src <- T.readFile fullPath+    assertExactPrintRoundtripFile fullPath (dropTrailingNewline src)+  where+    fullPath = fixtureRoot <> "/" <> relPath++fixtureRoot :: FilePath+fixtureRoot = "test/fixtures/nixfmt"++roundtripFixtures :: [FilePath]+roundtripFixtures =+  [ "correct/string-with-single-quote-at-end.nix",+    "correct/paths-with-interpolations.nix",+    "correct/quotes-in-inherit.nix",+    "correct/blank-line-in-interpolation.nix",+    "correct/indented-string.nix"+  ]++dropTrailingNewline :: T.Text -> T.Text+dropTrailingNewline text = maybe text id (T.stripSuffix "\n" text)
+ test/Parser.hs view
@@ -0,0 +1,564 @@+module Parser where++import Control.Monad (filterM)+import qualified Data.List as List+import qualified Data.Text as T+import qualified Data.Text.IO as T+import Nix.Lang.Annotation+import Nix.Lang.Parser+import Nix.Lang.Span+import Nix.Lang.Types+import Nix.Lang.Types.Ps+import Nix.Lang.Utils+import System.Directory (doesDirectoryExist, doesFileExist, listDirectory)+import System.Exit (ExitCode (ExitSuccess))+import System.FilePath (makeRelative, takeExtension, (</>))+import System.Process (readProcessWithExitCode)+import Test.Tasty+import Test.Tasty.HUnit+import Text.Megaparsec (eof, errorBundlePretty)+import Utils++tests :: TestTree+tests =+  testGroup+    "parser"+    [ testCase "sample fixture parses" sampleFixtureParses,+      nixpkgsParseTest,+      referenceParserFixtureTests,+      testGroup+        "literals and atoms"+        [ testCase "integer literal" integerLiteralParses,+          testCase "float literal" floatLiteralParses,+          testCase "boolean literal" booleanLiteralParses,+          testCase "null literal" nullLiteralParses,+          testCase "uri literal" uriLiteralParses,+          testCase "variable" variableParses,+          testCase "parenthesized expression" parensParse,+          testCase "environment path" envPathParses,+          testCase "literal path" literalPathParses,+          testCase "interpolated path" interpolatedPathParses,+          testCase "underscore-starting path" underscorePathParses,+          testCase "alpha-starting path" alphaPathParses,+          testCase "rec-starting path" recPathParses+        ],+      testGroup+        "strings"+        [ testCase "double quoted string with interpolation" doubleQuotedStringParses,+          testCase "indented string with interpolation" indentedStringParses+        ],+      testGroup+        "structures and bindings"+        [ testCase "empty set" emptySetParses,+          testCase "recursive set" recursiveSetParses,+          testCase "inherit binding" inheritBindingParses,+          testCase "inherit binding carries typed annotation" inheritBindingCarriesTypedAnn,+          testCase "normal binding carries typed annotation" normalBindingCarriesTypedAnn,+          testCase "dynamic attribute binding" dynamicAttrBindingParses,+          testCase "list expression" listParses+        ],+      testGroup+        "functions and control"+        [ testCase "assert expression parses to NixAssert" assertParsesToAssert,+          testCase "assert carries typed annotation" assertCarriesTypedAnn,+          testCase "assert carries common absolute position" assertCarriesCommonAbsPosition,+          testCase "with expression" withParses,+          testCase "if expression" ifParses,+          testCase "let expression" letParses,+          testCase "lambda variable pattern" lambdaVarPatParses,+          testCase "lambda variable pattern carries typed annotation" lambdaVarPatCarriesTypedAnn,+          testCase "lambda set pattern" lambdaSetPatParses,+          testCase "lambda set pattern carries typed annotation" lambdaSetPatCarriesTypedAnn,+          testCase "lambda set pattern leading as owns at token" lambdaLeadingAsCarriesTypedAnn,+          testCase "lambda trailing as-pattern" lambdaTrailingAsParses+        ],+      testGroup+        "selection and operators"+        [ testCase "selection with default" selectOrParses,+          testCase "has-attr operator" hasAttrParses,+          testCase "has-attr carries typed annotation" hasAttrCarriesTypedAnn,+          testCase "application precedence" applicationParses,+          testCase "binary operator carries common absolute position" binaryOperatorCarriesCommonAbsPosition,+          testCase "binary operator precedence" operatorPrecedenceParses,+          testCase "unary operators" unaryOperatorsParse+        ],+      testGroup+        "unsupported syntax"+        [ testCase "legacy let is rejected" legacyLetRejected,+          testCase "leading-decimal float is rejected" leadingDecimalRejected,+          testCase "multiple has-attr is rejected" multiHasAttrRejected,+          testCase "operator without whitespace is rejected" operatorWithoutWhitespaceRejected+        ]+    ]++sampleFixtureParses :: Assertion+sampleFixtureParses = do+  src <- T.readFile "test/sample.nix"+  case runNixParser nixFile "test/sample.nix" src of+    (Right _, _) -> pure ()+    (Left err, _) -> assertFailure $ errorBundlePretty err++nixpkgsParseTest :: TestTree+nixpkgsParseTest =+  testCase "nixpkgs tree parses (if found)" $ do+    mRoot <- findNixpkgsRoot+    case mRoot of+      Nothing -> putStrLn "nixpkgs not found"+      Just root -> do+        exists <- doesDirectoryExist root+        if exists+          then assertNixpkgsCorpusParses root+          else assertFailure $ "<nixpkgs> path does not exist: " <> root++findNixpkgsRoot :: IO (Maybe FilePath)+findNixpkgsRoot = do+  (exitCode, stdout, _) <- readProcessWithExitCode "nix-instantiate" ["--find-file", "nixpkgs"] ""+  pure $ case exitCode of+    ExitSuccess ->+      case lines stdout of+        path : _ | not (null path) -> Just path+        _ -> Nothing+    _ -> Nothing++assertNixpkgsCorpusParses :: FilePath -> Assertion+assertNixpkgsCorpusParses root = do+  nixFiles <- listNixFiles root+  failures <- go [] nixFiles+  case failures of+    [] -> pure ()+    _ ->+      assertFailure . unlines $+        ["failed to parse " <> show (length failures) <> " nixpkgs files out of " <> show (length nixFiles)]+          <> fmap renderFailure (take 20 failures)+  where+    go !failures [] = pure (reverse failures)+    go !failures (fp : rest) = do+      src <- T.readFile fp+      case runNixParser nixFile fp src of+        (Right _, _) -> go failures rest+        (Left err, _) ->+          go ((makeRelative root fp, errorBundlePretty err) : failures) rest++    renderFailure (fp, err) = "FAIL " <> fp <> "\n" <> err++listNixFiles :: FilePath -> IO [FilePath]+listNixFiles root = do+  paths <- walk root+  pure $ List.sort [path | path <- paths, takeExtension path == ".nix"]+  where+    walk dir = do+      entries <- listDirectory dir+      let paths = fmap (dir </>) entries+      files <- filterM doesFileExist paths+      dirs <- filterM doesDirectoryExist paths+      nested <- go [] dirs+      pure (files <> nested)++    go acc [] = pure acc+    go acc (dir : rest) = do+      nested <- walk dir+      let acc' = nested <> acc+      acc' `seq` go acc' rest++integerLiteralParses :: Assertion+integerLiteralParses = do+  expr <- parseExprOrFail "233"+  case expr of+    NixLit _ (L _ (NixInteger _ 233)) -> pure ()+    _ -> assertFailure $ "expected integer literal, got: " <> show expr++floatLiteralParses :: Assertion+floatLiteralParses = do+  expr <- parseExprOrFail "2.33"+  case expr of+    NixLit _ (L _ (NixFloat _ f)) | abs (f - 2.33) < 0.0001 -> pure ()+    _ -> assertFailure $ "expected float literal, got: " <> show expr++booleanLiteralParses :: Assertion+booleanLiteralParses = do+  t <- parseExprOrFail "true"+  f <- parseExprOrFail "false"+  case (t, f) of+    (NixLit _ (L _ (NixBoolean _ True)), NixLit _ (L _ (NixBoolean _ False))) -> pure ()+    _ -> assertFailure "expected true/false boolean literals"++nullLiteralParses :: Assertion+nullLiteralParses = do+  expr <- parseExprOrFail "null"+  case expr of+    NixLit _ (L _ (NixNull _)) -> pure ()+    _ -> assertFailure $ "expected null literal, got: " <> show expr++uriLiteralParses :: Assertion+uriLiteralParses = do+  expr <- parseExprOrFail "https://www.example.com"+  case expr of+    NixLit _ (L _ (NixUri _ uri)) | uri == "https://www.example.com" -> pure ()+    _ -> assertFailure $ "expected uri literal, got: " <> show expr++variableParses :: Assertion+variableParses = do+  expr <- parseExprOrFail "hello_world"+  case expr of+    NixVar _ (L _ name) | name == "hello_world" -> pure ()+    _ -> assertFailure $ "expected variable, got: " <> show expr++parensParse :: Assertion+parensParse = do+  expr <- parseExprOrFail "(1)"+  case expr of+    NixPar _ (L _ (NixLit _ (L _ (NixInteger _ 1)))) -> pure ()+    _ -> assertFailure $ "expected parenthesized integer, got: " <> show expr++envPathParses :: Assertion+envPathParses = do+  expr <- parseExprOrFail "<nixpkgs/nixos>"+  case expr of+    NixEnvPath _ (L _ path) | path == "nixpkgs/nixos" -> pure ()+    _ -> assertFailure $ "expected environment path, got: " <> show expr++literalPathParses :: Assertion+literalPathParses = do+  expr <- parseExprOrFail "./nix"+  case expr of+    NixPath _ (L _ (NixLiteralPath _ path)) | path == "./nix" -> pure ()+    _ -> assertFailure $ "expected literal path, got: " <> show expr++interpolatedPathParses :: Assertion+interpolatedPathParses = do+  expr <- parseExprOrFail "./${a}-${b}/c/d${e}"+  case expr of+    NixPath _ (L _ (NixInterpolPath NoExtF parts)) -> do+      length parts @?= 6+    _ -> assertFailure $ "expected interpolated path, got: " <> show expr++underscorePathParses :: Assertion+underscorePathParses = do+  expr <- parseExprOrFail "_/foo"+  case expr of+    NixPath _ (L _ (NixLiteralPath _ path)) | path == "_/foo" -> pure ()+    _ -> assertFailure $ "expected underscore-starting path, got: " <> show expr++alphaPathParses :: Assertion+alphaPathParses = do+  expr <- parseExprOrFail "abc/foo"+  case expr of+    NixPath _ (L _ (NixLiteralPath _ path)) | path == "abc/foo" -> pure ()+    _ -> assertFailure $ "expected alpha-starting path, got: " <> show expr++recPathParses :: Assertion+recPathParses = do+  expr <- parseExprOrFail "rec/foo"+  case expr of+    NixPath _ (L _ (NixLiteralPath _ path)) | path == "rec/foo" -> pure ()+    _ -> assertFailure $ "expected rec-starting path, got: " <> show expr++doubleQuotedStringParses :: Assertion+doubleQuotedStringParses = do+  expr <- parseExprOrFail "\"a${b}c\""+  case expr of+    NixString _ (L _ (NixDoubleQuotesString (SourceText src) parts)) -> do+      src @?= "a${b}c"+      length parts @?= 3+    _ -> assertFailure $ "expected double quoted string, got: " <> show expr++indentedStringParses :: Assertion+indentedStringParses = do+  expr <- parseExprOrFail "''a${b}c''"+  case expr of+    NixString _ (L _ (NixDoubleSingleQuotesString (SourceText src) parts)) -> do+      src @?= "a${b}c"+      length parts @?= 3+    _ -> assertFailure $ "expected indented string, got: " <> show expr++emptySetParses :: Assertion+emptySetParses = do+  expr <- parseExprOrFail "{ }"+  case expr of+    NixSet _ NixSetNonRecursive (L _ []) -> pure ()+    _ -> assertFailure $ "expected empty set, got: " <> show expr++recursiveSetParses :: Assertion+recursiveSetParses = do+  expr <- parseExprOrFail "rec { k = 233; }"+  case expr of+    NixSet _ NixSetRecursive (L _ [L _ (NixNormalBinding _ _ (L _ (NixLit _ (L _ (NixInteger _ 233)))))]) -> pure ()+    _ -> assertFailure $ "expected recursive set, got: " <> show expr++inheritBindingParses :: Assertion+inheritBindingParses = do+  expr <- parseExprOrFail "{ inherit (s) m g; }"+  case expr of+    NixSet _ _ (L _ [L _ (NixInheritBinding _ (Just (L _ (NixPar _ _))) keys)]) -> length keys @?= 2+    _ -> assertFailure $ "expected inherit binding set, got: " <> show expr++inheritBindingCarriesTypedAnn :: Assertion+inheritBindingCarriesTypedAnn = do+  expr <- parseExprOrFail "{ inherit (s) m g; }"+  case expr of+    NixSet _ _ (L _ [L _ (NixInheritBinding ann _ _)]) -> do+      fmap srcSpanStartColumn (annTokenSrcSpan (aibInherit ann)) @?= Just 3+      fmap srcSpanStartColumn (annTokenSrcSpan (aibSemicolon ann)) @?= Just 18+    _ -> assertFailure $ "expected inherit binding set, got: " <> show expr++dynamicAttrBindingParses :: Assertion+dynamicAttrBindingParses = do+  expr <- parseExprOrFail "{ \"${dynamic}\".${attr} = g; }"+  case expr of+    NixSet _ _ (L _ [L _ (NixNormalBinding _ (L _ (NixAttrPath _ [L _ (NixDynamicStringAttrKey NoExtF parts), L _ (NixDynamicInterpolAttrKey NoExtF (L _ (NixVar _ (L _ attr))))])) (L _ (NixVar _ (L _ "g"))))]) -> do+      length parts @?= 1+      attr @?= "attr"+    _ -> assertFailure $ "expected dynamic attr binding, got: " <> show expr++normalBindingCarriesTypedAnn :: Assertion+normalBindingCarriesTypedAnn = do+  expr <- parseExprOrFail "{ x = 1; }"+  case expr of+    NixSet _ _ (L _ [L _ (NixNormalBinding ann _ _)]) -> do+      fmap srcSpanStartColumn (annTokenSrcSpan (anbEqual ann)) @?= Just 5+      fmap srcSpanStartColumn (annTokenSrcSpan (anbSemicolon ann)) @?= Just 8+    _ -> assertFailure $ "expected normal binding set, got: " <> show expr++listParses :: Assertion+listParses = do+  expr <- parseExprOrFail "[1 2 3]"+  case expr of+    NixList _ xs -> length xs @?= 3+    _ -> assertFailure $ "expected list, got: " <> show expr++assertParsesToAssert :: Assertion+assertParsesToAssert =+  case runNixParser (nixExpr <* eof) "<assert>" (T.pack "assert a == 1; 2") of+    (Right (NixAssert _ _ _), _) -> pure ()+    (Right expr, _) -> assertFailure $ "expected NixAssert, got: " <> show expr+    (Left err, _) -> assertFailure $ errorBundlePretty err++assertCarriesTypedAnn :: Assertion+assertCarriesTypedAnn =+  case runNixParser (nixExpr <* eof) "<assert>" (T.pack "assert a == 1; 2") of+    (Right (NixAssert ann _ _), _) -> do+      fmap srcSpanStartColumn (annTokenSrcSpan (aaAssert ann)) @?= Just 1+      fmap srcSpanStartColumn (annTokenSrcSpan (aaSemicolon ann)) @?= Just 14+      acComments (aaCommon ann) @?= emptyComments+    (Right expr, _) -> assertFailure $ "expected NixAssert, got: " <> show expr+    (Left err, _) -> assertFailure $ errorBundlePretty err++assertCarriesCommonAbsPosition :: Assertion+assertCarriesCommonAbsPosition =+  case runNixParser (nixExpr <* eof) "<assert>" (T.pack "assert a == 1; 2") of+    (Right (NixAssert ann _ _), _) -> do+      annPos ann @?= AnnSpan (mkSrcSpan "<assert>" (1, 1) (1, 17))+      annSrcSpan ann @?= Just (mkSrcSpan "<assert>" (1, 1) (1, 17))+    (Right expr, _) -> assertFailure $ "expected NixAssert, got: " <> show expr+    (Left err, _) -> assertFailure $ errorBundlePretty err++withParses :: Assertion+withParses = do+  expr <- parseExprOrFail "with l; 1"+  case expr of+    NixWith _ (L _ (NixVar _ (L _ "l"))) (L _ (NixLit _ (L _ (NixInteger _ 1)))) -> pure ()+    _ -> assertFailure $ "expected with expression, got: " <> show expr++ifParses :: Assertion+ifParses = do+  expr <- parseExprOrFail "if a then b else c"+  case expr of+    NixIf _ (L _ (NixVar _ (L _ "a"))) (L _ (NixVar _ (L _ "b"))) (L _ (NixVar _ (L _ "c"))) -> pure ()+    _ -> assertFailure $ "expected if expression, got: " <> show expr++letParses :: Assertion+letParses = do+  expr <- parseExprOrFail "let x = 1; in x"+  case expr of+    NixLet _ (L _ [L _ (NixNormalBinding _ _ _)]) (L _ (NixVar _ (L _ "x"))) -> pure ()+    _ -> assertFailure $ "expected let expression, got: " <> show expr++lambdaVarPatParses :: Assertion+lambdaVarPatParses = do+  expr <- parseExprOrFail "x: x"+  case expr of+    NixLam _ (L _ (NixVarPat _ (L _ "x"))) (L _ (NixVar _ (L _ "x"))) -> pure ()+    _ -> assertFailure $ "expected lambda with var pattern, got: " <> show expr++lambdaVarPatCarriesTypedAnn :: Assertion+lambdaVarPatCarriesTypedAnn = do+  expr <- parseExprOrFail "x: x"+  case expr of+    NixLam ann (L _ (NixVarPat patAnn _)) _ -> do+      fmap srcSpanStartColumn (annTokenSrcSpan (alamColon ann)) @?= Just 2+      srcSpanStartColumn (avpId patAnn) @?= 1+    _ -> assertFailure $ "expected lambda with var pattern, got: " <> show expr++lambdaSetPatParses :: Assertion+lambdaSetPatParses = do+  expr <- parseExprOrFail "{x ? 1, y ? {}, ...}: x"+  case expr of+    NixLam _ (L _ (NixSetPat _ NixSetPatIsEllipses Nothing bindings)) _ -> length bindings @?= 2+    _ -> assertFailure $ "expected lambda with set pattern, got: " <> show expr++lambdaSetPatCarriesTypedAnn :: Assertion+lambdaSetPatCarriesTypedAnn = do+  expr <- parseExprOrFail "{x ? 1, y ? {}, ...}: x"+  case expr of+    NixLam lamAnn (L _ (NixSetPat _ _ _ bindings)) _ -> do+      fmap srcSpanStartColumn (annTokenSrcSpan (alamColon lamAnn)) @?= Just 21+      case bindings of+        L _ NixSetPatBinding {nspbAnn = AnnSetPatBinding {aspbQuestion = Just question}} : _ ->+          fmap srcSpanStartColumn (annTokenSrcSpan question) @?= Just 4+        _ -> assertFailure "expected first set-pattern binding to own question token"+    _ -> assertFailure $ "expected lambda with set pattern, got: " <> show expr++lambdaTrailingAsParses :: Assertion+lambdaTrailingAsParses = do+  expr <- parseExprOrFail "{x}@a: x"+  case expr of+    NixLam _ (L _ (NixSetPat _ _ (Just (L _ NixSetPatAs {nspaLocation = NixSetPatAsTrailing, nspaVar = L _ "a"})) _)) _ -> pure ()+    _ -> assertFailure $ "expected lambda with trailing as-pattern, got: " <> show expr++lambdaLeadingAsCarriesTypedAnn :: Assertion+lambdaLeadingAsCarriesTypedAnn = do+  expr <- parseExprOrFail "a@{x}: x"+  case expr of+    NixLam _ (L _ (NixSetPat _ _ (Just (L _ NixSetPatAs {nspaAnn = AnnSetPatAs {aspaAt = atTok}, nspaLocation = NixSetPatAsLeading, nspaVar = L _ "a"})) _)) _ ->+      fmap srcSpanStartColumn (annTokenSrcSpan atTok) @?= Just 2+    _ -> assertFailure $ "expected lambda with leading as-pattern token ownership, got: " <> show expr++selectOrParses :: Assertion+selectOrParses = do+  expr <- parseExprOrFail "a.b.c or d.e"+  case expr of+    NixSelect _ (L _ (NixVar _ (L _ "a"))) (L _ (NixAttrPath _ keys)) (Just (L _ (NixSelect _ _ _ Nothing))) ->+      length keys @?= 2+    _ -> assertFailure $ "expected selection with default, got: " <> show expr++hasAttrParses :: Assertion+hasAttrParses = do+  expr <- parseExprOrFail "a ? b"+  case expr of+    NixHasAttr _ (L _ (NixVar _ (L _ "a"))) (L _ (NixAttrPath _ keys)) -> length keys @?= 1+    _ -> assertFailure $ "expected has-attr expression, got: " <> show expr++hasAttrCarriesTypedAnn :: Assertion+hasAttrCarriesTypedAnn = do+  expr <- parseExprOrFail "a ? b"+  case expr of+    NixHasAttr ann _ _ -> fmap srcSpanStartColumn (annTokenSrcSpan (ahaQuestion ann)) @?= Just 3+    _ -> assertFailure $ "expected has-attr expression, got: " <> show expr++applicationParses :: Assertion+applicationParses = do+  expr <- parseExprOrFail "a b c"+  case expr of+    NixApp _ (L _ (NixApp _ (L _ (NixVar _ (L _ "a"))) (L _ (NixVar _ (L _ "b"))))) (L _ (NixVar _ (L _ "c"))) -> pure ()+    _ -> assertFailure $ "expected left-associative application, got: " <> show expr++binaryOperatorCarriesCommonAbsPosition :: Assertion+binaryOperatorCarriesCommonAbsPosition = do+  expr <- parseExprOrFail "a + b"+  case expr of+    NixBinApp ann OpAdd _ _ -> do+      annPos ann @?= AnnSpan (mkSrcSpan "<expr>" (1, 1) (1, 6))+      annSrcSpan ann @?= Just (mkSrcSpan "<expr>" (1, 1) (1, 6))+    _ -> assertFailure $ "expected binary application, got: " <> show expr++operatorPrecedenceParses :: Assertion+operatorPrecedenceParses = do+  expr <- parseExprOrFail "a >= b || c < d && e == f"+  case expr of+    NixBinApp _ OpOr (L _ (NixBinApp _ OpGE _ _)) (L _ (NixBinApp _ OpAnd (L _ (NixBinApp _ OpLT _ _)) (L _ (NixBinApp _ OpEq _ _)))) -> pure ()+    _ -> assertFailure $ "expected precedence tree, got: " <> show expr++unaryOperatorsParse :: Assertion+unaryOperatorsParse = do+  expr <- parseExprOrFail "!true || -5 == -5"+  case expr of+    NixBinApp _ OpOr (L _ (NixNotApp _ _)) (L _ (NixBinApp _ OpEq (L _ (NixNegApp _ _)) (L _ (NixNegApp _ _)))) -> pure ()+    _ -> assertFailure $ "expected unary operators, got: " <> show expr++legacyLetRejected :: Assertion+legacyLetRejected = parseExprFails "let { x = 233; body = x; }"++leadingDecimalRejected :: Assertion+leadingDecimalRejected = parseExprFails ".233"++multiHasAttrRejected :: Assertion+multiHasAttrRejected = parseExprFails "a ? b ? c ? d"++operatorWithoutWhitespaceRejected :: Assertion+operatorWithoutWhitespaceRejected = parseExprFails "1+-1"++referenceParserFixtureTests :: TestTree+referenceParserFixtureTests =+  testGroup+    "reference parser fixtures"+    [ testGroup "valid parse" (fixtureParseCases <$> validFixtures),+      testGroup "invalid parse" (fixtureRejectCases <$> invalidFixtures),+      testGroup "known divergence" (fixtureAcceptedCases <$> acceptedInvalidFixtures)+    ]++fixtureParseCases :: FilePath -> TestTree+fixtureParseCases relPath =+  testCase relPath $ do+    src <- T.readFile fullPath+    case runNixParser nixFile fullPath src of+      (Right _, _) -> pure ()+      (Left err, _) -> assertFailure $ errorBundlePretty err+  where+    fullPath = fixtureRoot <> "/" <> relPath++fixtureRejectCases :: FilePath -> TestTree+fixtureRejectCases relPath =+  testCase relPath $ do+    src <- T.readFile fullPath+    parseFileFails fullPath src+  where+    fullPath = fixtureRoot <> "/" <> relPath++fixtureAcceptedCases :: FilePath -> TestTree+fixtureAcceptedCases relPath =+  testCase relPath $ do+    src <- T.readFile fullPath+    case runNixParser nixFile fullPath src of+      (Right _, _) -> pure ()+      (Left err, _) -> assertFailure $ errorBundlePretty err+  where+    fullPath = fixtureRoot <> "/" <> relPath++fixtureRoot :: FilePath+fixtureRoot = "test/fixtures/nixfmt"++validFixtures :: [FilePath]+validFixtures =+  [ "correct/standalone-comments.nix",+    "correct/blank-line-in-interpolation.nix",+    "correct/indented-string.nix",+    "correct/string-with-single-quote-at-end.nix",+    "correct/paths-with-interpolations.nix",+    "correct/quotes-in-inherit.nix",+    "correct/if-with-comments.nix",+    "correct/commented-list.nix",+    "correct/commented-params-list.nix",+    "correct/final-comments-in-sets.nix",+    "diff/comment/in.nix",+    "diff/string/in.nix",+    "diff/string_interpol/in.nix",+    "diff/leading_blank/in.nix",+    "diff/inherit_comment/in.nix",+    "diff/strip_space/in.nix"+  ]++invalidFixtures :: [FilePath]+invalidFixtures =+  [ "invalid/naked-interpolation.nix",+    "invalid/path-starting-with-interpolation.nix",+    "invalid/path-with-interpolation-before-slash.nix",+    "invalid/path-with-escaped-interpolation.nix",+    "invalid/interpolation-in-env-path.nix",+    "invalid/smiley.nix"+  ]++acceptedInvalidFixtures :: [FilePath]+acceptedInvalidFixtures =+  [ "invalid/interpolation-in-inherit-1.nix",+    "invalid/interpolation-in-inherit-2.nix"+  ]
+ test/RFCPrint.hs view
@@ -0,0 +1,511 @@+{-# LANGUAGE EmptyCase #-}++module RFCPrint where++import qualified Data.Text as T+import Nix.Lang.Parser (nixExpr, runNixParser)+import Nix.Lang.RFCPrint (formatExpr, formatExprWithHints)+import Nix.Lang.RFCPrint.LayoutHints+import Nix.Lang.Types+import Nix.Lang.Types.Syn+import System.Exit (ExitCode (..))+import System.Process (readProcessWithExitCode)+import Test.Tasty+import Test.Tasty.HUnit+import Text.Megaparsec (eof, errorBundlePretty)+import Utils (parseExprOrFail, parseFileOrFail)++tests :: TestTree+tests =+  testGroup+    "rfc print"+    [ testGroup+        "precedence and parentheses"+        [ testCase "formats application argument with required parentheses" applicationArgumentParens,+          testCase "formats application chains" applicationChainFormatting,+          testCase "formats binary precedence correctly" binaryPrecedence,+          testCase "formats right associative concat" concatAssociativity,+          testCase "formats right associative updates" updateAssociativity,+          testCase "formats right associative implication" implicationAssociativity,+          testCase "formats non associative equality with parentheses" equalityParens,+          testCase "formats select default without losing precedence" selectDefaultFormatting,+          testCase "formats inherit scope with parentheses" inheritScopeParens+        ],+      testGroup+        "containers and bindings"+        [ testCase "formats empty list" emptyListFormatting,+          testCase "formats empty set" emptySetFormatting,+          testCase "formats empty recursive set" emptyRecSetFormatting,+          testCase "formats empty let" emptyLetFormatting,+          testCase "formats long lists multiline" longListFormatting,+          testCase "formats multiline inherit bindings" inheritManyFormatting,+          testCase "formats multiline inherit-from bindings" inheritFromManyFormatting,+          testCase "formats nested set binding values multiline" nestedSetBindingFormatting,+          testCase "formats let binding values multiline" letValueBindingFormatting,+          testCase "formats empty set-pattern lambdas with spaced braces" emptySetPatternFormatting,+          testCase "formats ellipsis-only set-pattern lambdas with spaced braces" ellipsisOnlySetPatternFormatting,+          testCase "formats simple set-pattern lambdas multiline with trailing commas" simpleSetPatternFormatting,+          testCase "formats set recursively" recursiveSetFormatting,+          testCase "formats let expression multiline" letFormatting,+          testCase "formats lambda set pattern" lambdaSetPatternFormatting+        ],+      testGroup+        "keyword expressions"+        [ testCase "formats with expression inline when compact" withFormatting,+          testCase "formats with attrset body multiline" withAttrsetFormatting,+          testCase "formats assert expression multiline" assertFormatting,+          testCase "formats assert attrset body multiline" assertAttrsetFormatting,+          testCase "formats if expression inline when compact" ifFormatting,+          testCase "formats else-if chains without indentation creep" elseIfFormatting+        ],+      testGroup+        "application layout"+        [ testCase "formats attrset application arguments multiline" applicationAttrsetFormatting,+          testCase "formats let application arguments multiline" applicationLetFormatting+        ],+      testGroup+        "strings and reparsing"+        [ testCase "formats strings and reparses" stringFormatting+        ],+      testGroup+        "nixfmt fixed point"+        [ testCase "nixfmt is no-op for fresh Syn output" nixfmtIsNoOp+        ],+      testGroup+        "layout hints"+        [ testCase "collects multiline list hint from parsed source" collectsMultilineListHint,+          testCase "parsed source can preserve inline list layout with hints" preservesInlineListWithHints,+          testCase "parsed source can preserve inline let layout with hints" preservesInlineLetWithHints,+          testCase "parsed source can preserve inline interpolation list layout with hints" preservesInlineInterpolationListWithHints,+          testCase "parsed source can preserve inline path interpolation list layout with hints" preservesInlinePathInterpolationListWithHints,+          testCase "parsed source can preserve root leading comments canonically" preservesRootLeadingCommentsWithHints,+          testCase "parsed source can preserve root trailing comments canonically" preservesRootTrailingCommentsWithHints,+          testCase "parsed source can preserve multiple root comments with blank lines canonically" preservesMultipleRootCommentsWithBlankLinesWithHints,+          testCase "parsed source can preserve nested set comments canonically" preservesNestedSetCommentsWithHints,+          testCase "parsed source can preserve blank line before nested binding comments canonically" preservesNestedBlankLineCommentsWithHints,+          testCase "parsed source can preserve blank line before second binding comment canonically" preservesSecondBindingBlankLineCommentsWithHints,+          testCase "parsed source can preserve inherit-binding comments canonically" preservesInheritBindingCommentsWithHints+        ]+    ]++assertFormatsTo :: Expr -> String -> Assertion+assertFormatsTo expr expected = formatExpr expr @?= T.pack expected++assertRoundtrips :: Expr -> Assertion+assertRoundtrips expr = do+  let rendered = formatExpr expr+  case runNixParser (nixExpr <* eof) "<rfc>" rendered of+    (Right actual, _) -> show (stripParensExpr (lowerParsedExpr actual)) @?= show (stripParensExpr expr)+    (Left err, _) -> assertFailure (errorBundlePretty err)++assertNixfmtFixedPoint :: Expr -> Assertion+assertNixfmtFixedPoint expr = do+  let rendered = T.unpack (formatExpr expr) <> "\n"+  (exitCode, stdout, stderr) <- readProcessWithExitCode "nixfmt" [] rendered+  case exitCode of+    ExitSuccess -> stdout @?= rendered+    ExitFailure code -> assertFailure $ unlines ["nixfmt failed with exit code " <> show code, stderr]++applicationArgumentParens :: Assertion+applicationArgumentParens = do+  let expr = mkApp (mkVar "f") (mkBinApp OpAdd (mkInt 1) (mkInt 2))+  assertFormatsTo expr "f (1 + 2)"+  assertRoundtrips expr++applicationChainFormatting :: Assertion+applicationChainFormatting = do+  let expr = mkApp (mkApp (mkVar "f") (mkVar "x")) (mkVar "y")+  assertFormatsTo expr "f x y"+  assertRoundtrips expr++binaryPrecedence :: Assertion+binaryPrecedence = do+  let expr = mkBinApp OpMul (mkPar (mkBinApp OpAdd (mkInt 1) (mkInt 2))) (mkInt 3)+  assertFormatsTo expr "(1 + 2) * 3"+  assertRoundtrips expr++concatAssociativity :: Assertion+concatAssociativity = do+  let expr = mkBinApp OpConcat (mkVar "a") (mkBinApp OpConcat (mkVar "b") (mkVar "c"))+  assertFormatsTo expr "a ++ b ++ c"+  assertRoundtrips expr++updateAssociativity :: Assertion+updateAssociativity = do+  let expr = mkBinApp OpUpdate (mkVar "a") (mkBinApp OpUpdate (mkVar "b") (mkVar "c"))+  assertFormatsTo expr "a // b // c"+  assertRoundtrips expr++implicationAssociativity :: Assertion+implicationAssociativity = do+  let expr = mkBinApp OpImpl (mkVar "a") (mkBinApp OpImpl (mkVar "b") (mkVar "c"))+  assertFormatsTo expr "a -> b -> c"+  assertRoundtrips expr++equalityParens :: Assertion+equalityParens = do+  let expr = mkBinApp OpEq (mkVar "a") (mkPar (mkBinApp OpEq (mkVar "b") (mkVar "c")))+  assertFormatsTo expr "a == (b == c)"+  assertRoundtrips expr++inheritScopeParens :: Assertion+inheritScopeParens = do+  let expr = mkSet [mkInheritFrom (mkIf (mkVar "cond") (mkVar "pkg") (mkVar "fallback")) ["name"]]+  assertFormatsTo expr "{\n  inherit (if cond then pkg else fallback) name;\n}"+  assertRoundtrips expr++emptyListFormatting :: Assertion+emptyListFormatting = do+  let expr = mkList []+  assertFormatsTo expr "[ ]"+  assertRoundtrips expr++emptySetFormatting :: Assertion+emptySetFormatting = do+  let expr = mkSet []+  assertFormatsTo expr "{ }"+  assertRoundtrips expr++emptyRecSetFormatting :: Assertion+emptyRecSetFormatting = do+  let expr = mkRecSet []+  assertFormatsTo expr "rec { }"+  assertRoundtrips expr++emptyLetFormatting :: Assertion+emptyLetFormatting = do+  let expr = mkLet [] (mkVar "x")+  assertFormatsTo expr "let\nin\nx"+  assertRoundtrips expr++longListFormatting :: Assertion+longListFormatting = do+  let expr = mkList [mkInt 1, mkInt 2, mkInt 3, mkInt 4, mkInt 5]+  assertFormatsTo expr "[\n  1\n  2\n  3\n  4\n  5\n]"+  assertRoundtrips expr++inheritManyFormatting :: Assertion+inheritManyFormatting = do+  let keys = map mkAttr ["a", "b", "c", "d"]+      expr = mkSet [mkInheritKeys keys]+  assertFormatsTo expr "{\n  inherit\n    a\n    b\n    c\n    d\n    ;\n}"+  assertRoundtrips expr++inheritFromManyFormatting :: Assertion+inheritFromManyFormatting = do+  let keys = map mkAttr ["a", "b", "c", "d"]+      expr = mkSet [mkInheritFromKeys (mkVar "pkgs") keys]+  assertFormatsTo expr "{\n  inherit (pkgs)\n    a\n    b\n    c\n    d\n    ;\n}"+  assertRoundtrips expr++nestedSetBindingFormatting :: Assertion+nestedSetBindingFormatting = do+  let expr = mkSet [mkBinding ["x"] (mkSet [mkBinding ["a"] (mkInt 1), mkBinding ["b"] (mkInt 2)])]+  assertFormatsTo expr "{\n  x = {\n    a = 1;\n    b = 2;\n  };\n}"+  assertRoundtrips expr++letValueBindingFormatting :: Assertion+letValueBindingFormatting = do+  let expr = mkSet [mkBinding ["x"] (mkLet [mkBinding ["a"] (mkInt 1)] (mkVar "a"))]+  assertFormatsTo expr "{\n  x =\n    let\n      a = 1;\n    in\n    a;\n}"+  assertRoundtrips expr++emptySetPatternFormatting :: Assertion+emptySetPatternFormatting = do+  let expr = mkLam (mkSetPat NixSetPatNotEllipses Nothing []) (mkVar "body")+  assertFormatsTo expr "{ }: body"+  assertRoundtrips expr++ellipsisOnlySetPatternFormatting :: Assertion+ellipsisOnlySetPatternFormatting = do+  let expr = mkLam (mkSetPat NixSetPatIsEllipses Nothing []) (mkVar "body")+  assertFormatsTo expr "{ ... }: body"+  assertRoundtrips expr++simpleSetPatternFormatting :: Assertion+simpleSetPatternFormatting = do+  let pat = mkSetPat NixSetPatNotEllipses Nothing [mkSetPatBinding "a" Nothing, mkSetPatBinding "b" (Just (mkInt 1))]+      expr = mkLam pat (mkVar "body")+  assertFormatsTo expr "{\n  a,\n  b ? 1,\n}:\nbody"+  assertRoundtrips expr++recursiveSetFormatting :: Assertion+recursiveSetFormatting = do+  let expr = mkRecSet [mkBinding ["a"] (mkInt 1), mkBinding ["b"] (mkList [mkInt 1, mkInt 2])]+  assertFormatsTo expr "rec {\n  a = 1;\n  b = [\n    1\n    2\n  ];\n}"+  assertRoundtrips expr++letFormatting :: Assertion+letFormatting = do+  let expr = mkLet [mkBinding ["a"] (mkInt 1), mkBinding ["b"] (mkVar "a")] (mkVar "b")+  assertFormatsTo expr "let\n  a = 1;\n  b = a;\nin\nb"+  assertRoundtrips expr++selectDefaultFormatting :: Assertion+selectDefaultFormatting = do+  let expr = mkApp (mkVar "f") (mkSelectOrAttrs (mkVar "cfg") ["a", "b"] (mkIf (mkVar "cond") (mkInt 1) (mkInt 2)))+  assertFormatsTo expr "f (cfg.a.b or (if cond then 1 else 2))"+  assertRoundtrips expr++stringFormatting :: Assertion+stringFormatting = do+  let expr = mkText "a\n${b}\""+  assertFormatsTo expr "\"a\\n\\${b}\\\"\""+  _ <- parseExprOrFail (formatExpr expr)+  pure ()++lambdaSetPatternFormatting :: Assertion+lambdaSetPatternFormatting = do+  let pat = mkSetPat NixSetPatIsEllipses (Just (mkSetPatAs NixSetPatAsLeading "args")) [mkSetPatBinding "x" Nothing, mkSetPatBinding "y" (Just (mkInt 1))]+      expr = mkLam pat (mkVar "x")+  assertFormatsTo expr "args@{\n  x,\n  y ? 1,\n  ...\n}:\nx"+  assertRoundtrips expr++applicationAttrsetFormatting :: Assertion+applicationAttrsetFormatting = do+  let attrset = mkSet [mkBinding ["a"] (mkInt 1), mkBinding ["b"] (mkInt 2)]+      expr = mkApp (mkApp (mkVar "f") attrset) (mkVar "x")+  assertFormatsTo expr "f {\n  a = 1;\n  b = 2;\n} x"+  assertRoundtrips expr++applicationLetFormatting :: Assertion+applicationLetFormatting = do+  let letExpr = mkLet [mkBinding ["a"] (mkInt 1)] (mkVar "a")+      expr = mkApp (mkApp (mkVar "f") letExpr) (mkVar "z")+  assertFormatsTo expr "f (\n  let\n    a = 1;\n  in\n  a\n) z"+  assertRoundtrips expr++withFormatting :: Assertion+withFormatting = do+  let expr = mkWith (mkVar "pkgs") (mkApp (mkVar "callPackage") (mkVar "drv"))+  assertFormatsTo expr "with pkgs; callPackage drv"+  assertRoundtrips expr++withAttrsetFormatting :: Assertion+withAttrsetFormatting = do+  let body = mkSet [mkBinding ["a"] (mkInt 1), mkBinding ["b"] (mkInt 2)]+      expr = mkWith (mkVar "pkgs") body+  assertFormatsTo expr "with pkgs;\n{\n  a = 1;\n  b = 2;\n}"+  assertRoundtrips expr++assertFormatting :: Assertion+assertFormatting = do+  let expr = mkAssert (mkVar "cond") (mkApp (mkVar "f") (mkVar "x"))+  assertFormatsTo expr "assert cond;\nf x"+  assertRoundtrips expr++assertAttrsetFormatting :: Assertion+assertAttrsetFormatting = do+  let body = mkSet [mkBinding ["a"] (mkInt 1), mkBinding ["b"] (mkInt 2)]+      expr = mkAssert (mkVar "cond") body+  assertFormatsTo expr "assert cond;\n{\n  a = 1;\n  b = 2;\n}"+  assertRoundtrips expr++ifFormatting :: Assertion+ifFormatting = do+  let expr = mkIf (mkVar "cond") (mkVar "yes") (mkVar "no")+  assertFormatsTo expr "if cond then yes else no"+  assertRoundtrips expr++elseIfFormatting :: Assertion+elseIfFormatting = do+  let expr = mkIf (mkVar "a") (mkVar "b") (mkIf (mkVar "c") (mkVar "d") (mkVar "e"))+  assertFormatsTo expr "if a then\n  b\nelse if c then\n  d\nelse\n  e"+  assertRoundtrips expr++nixfmtIsNoOp :: Assertion+nixfmtIsNoOp = mapM_ assertNixfmtFixedPoint examples+  where+    examples =+      [ mkInt 1,+        mkList [],+        mkSet [],+        mkRecSet [],+        mkList [mkInt 1, mkInt 2, mkInt 3, mkInt 4, mkInt 5],+        mkApp (mkVar "f") (mkBinApp OpAdd (mkInt 1) (mkInt 2)),+        mkApp (mkApp (mkVar "f") (mkVar "x")) (mkVar "y"),+        mkApp (mkApp (mkVar "f") (mkSet [mkBinding ["a"] (mkInt 1), mkBinding ["b"] (mkInt 2)])) (mkVar "x"),+        mkApp (mkApp (mkVar "f") (mkLet [mkBinding ["a"] (mkInt 1)] (mkVar "a"))) (mkVar "z"),+        mkBinApp OpUpdate (mkVar "a") (mkBinApp OpUpdate (mkVar "b") (mkVar "c")),+        mkBinApp OpImpl (mkVar "a") (mkBinApp OpImpl (mkVar "b") (mkVar "c")),+        mkSet [mkInheritKeys (map mkAttr ["a", "b", "c", "d"])],+        mkSet [mkInheritFromKeys (mkVar "pkgs") (map mkAttr ["a", "b", "c", "d"])],+        mkSet [mkBinding ["x"] (mkSet [mkBinding ["a"] (mkInt 1), mkBinding ["b"] (mkInt 2)])],+        mkSet [mkBinding ["x"] (mkLet [mkBinding ["a"] (mkInt 1)] (mkVar "a"))],+        mkRecSet [mkBinding ["a"] (mkInt 1), mkBinding ["b"] (mkList [mkInt 1, mkInt 2])],+        mkLet [] (mkVar "x"),+        mkLet [mkBinding ["a"] (mkInt 1), mkBinding ["b"] (mkVar "a")] (mkVar "b"),+        mkLam (mkSetPat NixSetPatNotEllipses Nothing []) (mkVar "body"),+        mkLam (mkSetPat NixSetPatIsEllipses Nothing []) (mkVar "body"),+        mkLam (mkSetPat NixSetPatNotEllipses Nothing [mkSetPatBinding "a" Nothing, mkSetPatBinding "b" (Just (mkInt 1))]) (mkVar "body"),+        mkApp (mkVar "f") (mkSelectOrAttrs (mkVar "cfg") ["a", "b"] (mkIf (mkVar "cond") (mkInt 1) (mkInt 2))),+        mkLam (mkSetPat NixSetPatIsEllipses (Just (mkSetPatAs NixSetPatAsLeading "args")) [mkSetPatBinding "x" Nothing, mkSetPatBinding "y" (Just (mkInt 1))]) (mkVar "x"),+        mkSet [mkInheritFrom (mkIf (mkVar "cond") (mkVar "pkg") (mkVar "fallback")) ["name"]],+        mkWith (mkVar "pkgs") (mkApp (mkVar "callPackage") (mkVar "drv")),+        mkWith (mkVar "pkgs") (mkSet [mkBinding ["a"] (mkInt 1), mkBinding ["b"] (mkInt 2)]),+        mkAssert (mkVar "cond") (mkApp (mkVar "f") (mkVar "x")),+        mkAssert (mkVar "cond") (mkSet [mkBinding ["a"] (mkInt 1), mkBinding ["b"] (mkInt 2)]),+        mkIf (mkVar "a") (mkVar "b") (mkIf (mkVar "c") (mkVar "d") (mkVar "e"))+      ]++collectsMultilineListHint :: Assertion+collectsMultilineListHint = do+  parsed <- parseExprOrFail "[\n  1\n  2\n]"+  let hints = collectLayoutHints parsed+  lookupLayoutHint [] hints @?= Just (LayoutHint (Just PreferMultiline) [] [])++preservesInlineListWithHints :: Assertion+preservesInlineListWithHints = do+  parsed <- parseExprOrFail "[ 1 2 ]"+  let (expr, hints) = fromParsedExpr parsed+  formatExprWithHints hints expr @?= "[ 1 2 ]"++preservesInlineLetWithHints :: Assertion+preservesInlineLetWithHints = do+  parsed <- parseExprOrFail "let a = 1; b = a; in b"+  let (expr, hints) = fromParsedExpr parsed+  formatExprWithHints hints expr @?= "let a = 1; b = a; in b"++preservesInlineInterpolationListWithHints :: Assertion+preservesInlineInterpolationListWithHints = do+  parsed <- parseExprOrFail "\"${[ 1 2 ]}\""+  let (expr, hints) = fromParsedExpr parsed+  formatExprWithHints hints expr @?= "\"${[ 1 2 ]}\""++preservesInlinePathInterpolationListWithHints :: Assertion+preservesInlinePathInterpolationListWithHints = do+  parsed <- parseExprOrFail "./${[ 1 2 ]}"+  let (expr, hints) = fromParsedExpr parsed+  formatExprWithHints hints expr @?= "./${[ 1 2 ]}"++preservesRootLeadingCommentsWithHints :: Assertion+preservesRootLeadingCommentsWithHints = do+  parsed <- parseFileOrFail "<comments>" "# hello\n1"+  let (expr, hints) = fromParsedExpr parsed+  formatExprWithHints hints expr @?= "# hello\n1"++preservesRootTrailingCommentsWithHints :: Assertion+preservesRootTrailingCommentsWithHints = do+  parsed <- parseFileOrFail "<comments>" "1\n# bye"+  let (expr, hints) = fromParsedExpr parsed+  formatExprWithHints hints expr @?= "1\n# bye"++preservesMultipleRootCommentsWithBlankLinesWithHints :: Assertion+preservesMultipleRootCommentsWithBlankLinesWithHints = do+  parsed <- parseFileOrFail "<comments>" "# first\n\n# second\n1"+  let (expr, hints) = fromParsedExpr parsed+  lookupLayoutHint [] hints+    @?= Just+      ( LayoutHint+          Nothing+          [CanonicalLineComment " first", CanonicalBlankLine, CanonicalLineComment " second"]+          []+      )+  formatExprWithHints hints expr @?= "# first\n\n# second\n1"++preservesNestedSetCommentsWithHints :: Assertion+preservesNestedSetCommentsWithHints = do+  parsed <- parseFileOrFail "<comments>" "{\n  # before x\n  x = 1;\n}"+  let (expr, hints) = fromParsedExpr parsed+  lookupLayoutHint [0] hints @?= Nothing+  lookupLayoutHint [0, 0] hints+    @?= Just+      ( LayoutHint+          Nothing+          [CanonicalLineComment " before x"]+          []+      )+  formatExprWithHints hints expr @?= "{\n  # before x\n  x = 1;\n}"++preservesNestedBlankLineCommentsWithHints :: Assertion+preservesNestedBlankLineCommentsWithHints = do+  parsed <- parseFileOrFail "<comments>" "{\n\n  # before x\n  x = 1;\n}"+  let (expr, hints) = fromParsedExpr parsed+  lookupLayoutHint [0] hints @?= Nothing+  lookupLayoutHint [0, 0] hints+    @?= Just+      ( LayoutHint+          Nothing+          [CanonicalBlankLine, CanonicalLineComment " before x"]+          []+      )+  formatExprWithHints hints expr @?= "{\n\n  # before x\n  x = 1;\n}"++preservesSecondBindingBlankLineCommentsWithHints :: Assertion+preservesSecondBindingBlankLineCommentsWithHints = do+  parsed <- parseFileOrFail "<comments>" "{\n  x = 1;\n\n  # before y\n  y = 2;\n}"+  let (expr, hints) = fromParsedExpr parsed+  lookupLayoutHint [1] hints @?= Nothing+  lookupLayoutHint [1, 0] hints+    @?= Just+      ( LayoutHint+          Nothing+          [CanonicalBlankLine, CanonicalLineComment " before y"]+          []+      )+  formatExprWithHints hints expr @?= "{\n  x = 1;\n\n  # before y\n  y = 2;\n}"++preservesInheritBindingCommentsWithHints :: Assertion+preservesInheritBindingCommentsWithHints = do+  parsed <- parseFileOrFail "<comments>" "{\n  # before inherit\n  inherit x;\n}"+  let (expr, hints) = fromParsedExpr parsed+  formatExprWithHints hints expr @?= "{\n  # before inherit\n  inherit x;\n}"++stripParensExpr :: Expr -> Expr+stripParensExpr = \case+  NixVar x name -> NixVar x name+  NixLit x lit -> NixLit x lit+  NixPar _ expr -> stripParensExpr expr+  NixString x str -> NixString x (stripParensString str)+  NixPath x path -> NixPath x (stripParensPath path)+  NixEnvPath x path -> NixEnvPath x path+  NixLam x pat expr -> NixLam x (stripParensFuncPat pat) (stripParensExpr expr)+  NixApp x f y -> NixApp x (stripParensExpr f) (stripParensExpr y)+  NixBinApp x op l r -> NixBinApp x op (stripParensExpr l) (stripParensExpr r)+  NixNotApp x expr -> NixNotApp x (stripParensExpr expr)+  NixNegApp x expr -> NixNegApp x (stripParensExpr expr)+  NixList x xs -> NixList x (stripParensExpr <$> xs)+  NixSet x recFlag bindings -> NixSet x recFlag (stripParensBinding <$> bindings)+  NixLet x bindings expr -> NixLet x (stripParensBinding <$> bindings) (stripParensExpr expr)+  NixHasAttr x expr path -> NixHasAttr x (stripParensExpr expr) (stripParensAttrPath path)+  NixSelect x expr path mDefault -> NixSelect x (stripParensExpr expr) (stripParensAttrPath path) (stripParensExpr <$> mDefault)+  NixIf x c t f -> NixIf x (stripParensExpr c) (stripParensExpr t) (stripParensExpr f)+  NixWith x scope expr -> NixWith x (stripParensExpr scope) (stripParensExpr expr)+  NixAssert x assertion expr -> NixAssert x (stripParensExpr assertion) (stripParensExpr expr)++stripParensString :: NString -> NString+stripParensString = \case+  NixDoubleQuotesString x parts -> NixDoubleQuotesString x (stripParensStringPart <$> parts)+  NixDoubleSingleQuotesString x parts -> NixDoubleSingleQuotesString x (stripParensStringPart <$> parts)++stripParensPath :: Path -> Path+stripParensPath = \case+  NixLiteralPath x path -> NixLiteralPath x path+  NixInterpolPath x parts -> NixInterpolPath x (stripParensStringPart <$> parts)++stripParensStringPart :: StringPart -> StringPart+stripParensStringPart = \case+  NixStringLiteral x text -> NixStringLiteral x text+  NixStringInterpol x expr -> NixStringInterpol x (stripParensExpr expr)++stripParensAttrPath :: AttrPath -> AttrPath+stripParensAttrPath (NixAttrPath x keys) = NixAttrPath x (stripParensAttrKey <$> keys)++stripParensAttrKey :: AttrKey -> AttrKey+stripParensAttrKey = \case+  NixStaticAttrKey x name -> NixStaticAttrKey x name+  NixDynamicStringAttrKey x parts -> NixDynamicStringAttrKey x (stripParensStringPart <$> parts)+  NixDynamicInterpolAttrKey x expr -> NixDynamicInterpolAttrKey x (stripParensExpr expr)++stripParensBinding :: Binding -> Binding+stripParensBinding = \case+  NixNormalBinding x path expr -> NixNormalBinding x (stripParensAttrPath path) (stripParensExpr expr)+  NixInheritBinding x mScope keys -> NixInheritBinding x (stripParensExpr <$> mScope) (stripParensAttrKey <$> keys)++stripParensFuncPat :: FuncPat -> FuncPat+stripParensFuncPat = \case+  NixVarPat x name -> NixVarPat x name+  NixSetPat x ellipses mAs bindings -> NixSetPat x ellipses mAs (stripParensSetPatBinding <$> bindings)++stripParensSetPatBinding :: SetPatBinding -> SetPatBinding+stripParensSetPatBinding NixSetPatBinding {..} = NixSetPatBinding nspbAnn nspbVar (stripParensExpr <$> nspbDefault)
+ test/Spec.hs view
@@ -0,0 +1,20 @@+module Main (main) where++import qualified Edit+import qualified ExactPrint+import qualified Parser+import qualified RFCPrint+import Test.Tasty++main :: IO ()+main = defaultMain tests++tests :: TestTree+tests =+  testGroup+    "nix-lang"+    [ Parser.tests,+      ExactPrint.tests,+      RFCPrint.tests,+      Edit.tests+    ]
+ test/Utils.hs view
@@ -0,0 +1,32 @@+module Utils where++import qualified Data.Text as T+import Nix.Lang.Parser+import Nix.Lang.Types+import Nix.Lang.Types.Ps+import Test.Tasty.HUnit+import Text.Megaparsec (eof, errorBundlePretty)++parseExprOrFail :: T.Text -> IO (NixExpr Ps)+parseExprOrFail src =+  case runNixParser (nixExpr <* eof) "<expr>" src of+    (Right expr, _) -> pure expr+    (Left err, _) -> assertFailure (errorBundlePretty err)++parseFileOrFail :: FilePath -> T.Text -> IO (NixExpr Ps)+parseFileOrFail fp src =+  case runNixParser nixFile fp src of+    (Right expr, _) -> pure expr+    (Left err, _) -> assertFailure (errorBundlePretty err)++parseExprFails :: T.Text -> Assertion+parseExprFails src =+  case runNixParser (nixExpr <* eof) "<expr>" src of+    (Left _, _) -> pure ()+    (Right expr, _) -> assertFailure $ "expected parse failure, got: " <> show expr++parseFileFails :: FilePath -> T.Text -> Assertion+parseFileFails fp src =+  case runNixParser nixFile fp src of+    (Left _, _) -> pure ()+    (Right expr, _) -> assertFailure $ "expected parse failure, got: " <> show expr
+ test/fixtures/nixfmt/correct/blank-line-in-interpolation.nix view
@@ -0,0 +1,11 @@+# Blank lines inside interpolations must be preserved.+{+  postInstall = ''+    --prefix PATH : ${++      lib.makeBinPath [+        pkgs.fzf+      ]+    }+  '';+}
+ test/fixtures/nixfmt/correct/commented-list.nix view
@@ -0,0 +1,2 @@+# bar and baz+[ bar ]
+ test/fixtures/nixfmt/correct/commented-params-list.nix view
@@ -0,0 +1,35 @@+{+  # param1,+  param2,+  # param3,+  # param4,+  param5,+  # param6,+}:+{+  # param7,+  param8,+  # param9,+  /*+    multiline comment+    1+  */+  # param10,+  param11,+  /*+    multiline comment+    2+  */+}:+{+  param12,++  # param13+}:+{+  param13,++  # param14++}:+{ }
+ test/fixtures/nixfmt/correct/final-comments-in-sets.nix view
@@ -0,0 +1,19 @@+[+  {+    # foo1 = bar;+    # foo2 = bar;+    # foo3 = bar;+  }++  {+    foo1 = bar;+    # foo2 = bar;+    # foo3 = bar;+  }++  {+    foo1 = bar;+    foo2 = bar;+    # foo3 = bar;+  }+]
+ test/fixtures/nixfmt/correct/if-with-comments.nix view
@@ -0,0 +1,15 @@+v:++# check if v is int+if isInt v then+  # handle int v+  fromInt v+# comments here apply to the branch below, not to the value above+# check if v is bool+else if isBool v then+  # handle bool v+  fromBool v+# no idea what v could be+else+  # we give up+  error "iunno"
+ test/fixtures/nixfmt/correct/indented-string.nix view
@@ -0,0 +1,14 @@+# These come from previous parser bugs in Nix+[+  ''+    ''\a+    ''\++    '${true}'++    $'\t'+  ''++  ''"ending dollar $''+  ''"$''+]
+ test/fixtures/nixfmt/correct/leading-semi.nix view
@@ -0,0 +1,1 @@+404: Not Found
+ test/fixtures/nixfmt/correct/math.nix view
@@ -0,0 +1,1 @@+404: Not Found
+ test/fixtures/nixfmt/correct/multiline-lambdas.nix view
@@ -0,0 +1,1 @@+404: Not Found
+ test/fixtures/nixfmt/correct/paths-with-interpolations.nix view
@@ -0,0 +1,14 @@+let+  foo = "foo";+  bar = "bar";++in+[+  /${foo}+  ./${foo}+  /foo/${bar}+  /foo${bar}+  /${foo}/bar+  /${foo}bar+  /foo${bar}baz+]
+ test/fixtures/nixfmt/correct/quotes-in-inherit.nix view
@@ -0,0 +1,1 @@+{ inherit ({ "in" = 1; }) "in"; }
+ test/fixtures/nixfmt/correct/rec-path.nix view
@@ -0,0 +1,1 @@+404: Not Found
+ test/fixtures/nixfmt/correct/standalone-comments-2.nix view
@@ -0,0 +1,1 @@+404: Not Found
+ test/fixtures/nixfmt/correct/standalone-comments-3.nix view
@@ -0,0 +1,1 @@+404: Not Found
+ test/fixtures/nixfmt/correct/standalone-comments.nix view
@@ -0,0 +1,25 @@+# This tests whether empty lines are correctly preserved in lists+[+  a+  b+  # c+  c+  # 1++  d++  # e+  e++  # 2++  f+  # 3++  # g+  g++  # 8++  # 9+]
+ test/fixtures/nixfmt/correct/string-with-single-quote-at-end.nix view
@@ -0,0 +1,3 @@+''+  ${"w '%{http_code}\n'"}+''
+ test/fixtures/nixfmt/correct/urls.nix view
@@ -0,0 +1,1 @@+404: Not Found
+ test/fixtures/nixfmt/diff/comment/in.nix view
@@ -0,0 +1,168 @@+[+/*+*/+  /*+   */++  /*+  */++  /*+    */++    /*+  */++    /*+    */++  /*@*/++  /**+    @+   **/++    /*@+   @+  @*/++  /*@+   @+    @*/++    /*@+@+    @*/++    /*@+     @+    @*/++ /* test+  * test+  */++ /* test+  * test+  *+  */++  /*+   * FOO+   */++  /**+   * FOO+   */++  /*+   * FOO+   * BAR+   */++  /**+    Concatenate a list of strings with a separator between each element++    # Example++    ```nix+    concatStringsSep "/" ["usr" "local" "bin"]+    => "usr/local/bin"+    ```++    # Type++    ```+    concatStringsSep :: string -> [string] -> string+    ```+  */++  /*+    Concatenate a list of strings with a separator between each element++    # Example++    ```nix+    concatStringsSep "/" ["usr" "local" "bin"]+    => "usr/local/bin"+    ```++    # Type++    ```+    concatStringsSep :: string -> [string] -> string+    ```+  */++  /*+   * Concatenate a list of strings with a separator between each element+   *+   * # Example+   *+   * ```nix+   * concatStringsSep "/" ["usr" "local" "bin"]+   * => "usr/local/bin"+   * ```+   *+   * # Type+   *+   * ```+   * concatStringsSep :: string -> [string] -> string+   * ```+   */+++  [  # 1+  #2+    a  # 3+    b+    c # 4+    #5++    #6++    d+    #7+  ]++  {+    a = 123;   # comment+  }++  {  # 1+  #2+    a=1;  # 3+    b=1;+    c=1; # 4+    #5++    #6++    d=1;+    #7+  }++  (let  # 1+  #2+    a=1;  # 3+    b=1;+    c=1; # 4+    #5++    #6++    d=1;+    #7+  in+  d)++  ({+    a,    # comment+    b ? 2,# comment+  }: _)++  # Trailing comment after multiline string must not break idempotency+  "+" # c+  t+]
+ test/fixtures/nixfmt/diff/inherit_comment/in.nix view
@@ -0,0 +1,18 @@+{+  inherit # eeby deeby+    a+    # b+    c+    ;++  # https://github.com/kamadorueda/alejandra/issues/372+  inherit (pkgs.haskell.lib)+    # doJailbreak - remove package bounds from build-depends of a package+    doJailbreak+    # dontCheck - skip tests+    dontCheck+    # override deps of a package+    # see what can be overridden - https://github.com/NixOS/nixpkgs/blob/0ba44a03f620806a2558a699dba143e6cf9858db/pkgs/development/haskell-modules/generic-builder.nix#L13+    overrideCabal+    ;+}
+ test/fixtures/nixfmt/diff/leading_blank/in.nix view
@@ -0,0 +1,6 @@+++c "${+  f++}"
+ test/fixtures/nixfmt/diff/string/in.nix view
@@ -0,0 +1,101 @@+[+  ''+  foo+ bar+''+  ""+###+  "+  "+###+  "a+   ${x}+   b+  "+###+  ''''+###+  ''a''+###+  ''${""}''+###+  ''${""}++  ''+###+  ''a+      ''+###+  ''a++      ''+###+  ''  a+      ''+###++    ''a+  ''+###+              ''+        a+      ${""}+         b+        ${""}+         c ${""} d+         e+            ''+###+  ''+  ''+###+          ''+              declare -a makefiles=(./*.mak)+              sed -i -f ${makefile-sed} "''${makefiles[@]}"+              ### assign Makefile variables eagerly & change backticks to `$(shell …)`+              sed -i -e 's/ = `\([^`]\+\)`/ := $(shell \1)/' \+                -e 's/`\([^`]\+\)`/$(shell \1)/' \+                "''${makefiles[@]}"+            ''+###+        ''+                [${ mkSectionName sectName }]+        ''+###+''-couch_ini ${ cfg.package }/etc/default.ini ${ configFile } ${ pkgs.writeText "couchdb-extra.ini" cfg.extraConfig } ${ cfg.configFile }''+###+              ''exec i3-input -F "mark %s" -l 1 -P 'Mark: ' ''+###+              ''exec i3-input -F '[con_mark="%s"] focus' -l 1 -P 'Go to: ' ''+###+              ''"${ pkgs.name or "<unknown-name>" }";''+###+                  ''+      ${pkgs.replace-secret}/bin/replace-secret '${placeholder}' '${secretFile}' '${targetFile}' ''+###+''+    mkdir -p "$out/lib/modules/${ kernel.modDirVersion }/kernel/net/wireless/"+    ''+###+        ''        <?xml version="1.0" encoding="UTF-8"?>+        <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">+        <plist version="1.0">+        ${ expr "" v }+        </plist>''++  ''+    --${+    "test"+  }+  ''++  "--${+    "test"+  }"+  ''''${pkgs.ghostscript}/bin/ps2pdf''+  '''test''$test''+  '''test'''quotes''+  '''plain''+  '' between spaces ''+  '' !#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~''+]
+ test/fixtures/nixfmt/diff/string_interpol/in.nix view
@@ -0,0 +1,82 @@+[+  "${/*a*/"${/*b*/"${c}"}"/*d*/}"+  ''${/*a*/''${/*b*/''${c}''}''/*d*/}''+  {+    ExecStart = "${pkgs.openarena}/bin/oa_ded +set fs_basepath ${pkgs.openarena}/openarena-0.8.8 +set fs_homepath /var/lib/openarena ${+        concatMapStringsSep (x: x) " " cfg.extraFlags+    }";+    description = "${+        optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1+      } or ${+        optionDescriptionPhrase (class: class == "noun" || class == "conjunction" || class == "composite")+          t2+      }";+    ruleset = ''+      table ip nat {+        chain port_redirects {+          type nat hook prerouting priority dstnat+          policy accept++          ${builtins.concatStringsSep "\n" (map (e:+            "iifname \"${cfg.upstreamIface}\" tcp dport ${builtins.toString e.sourcePort} dnat to ${e.destination}"+            ) tcpPortMap)}++          ${builtins.concatStringsSep "\n" (map (e:+            "ifname \"${cfg.upstreamIface}\" udp dport ${builtins.toString e.sourcePort} dnat to ${e.destination}"+            ) udpPortMap)}+        }+    '';+  }+  {+    system.nixos.versionSuffix1 = ".${+      final.substring 0 8 (+        self.lastModifiedDate or self.lastModified+          or "19700101"+        self.lastModifiedDate or self.lastModified or "19700101"+      )+    }.${self.shortRev or "dirty"}";++    system.nixos.versionSuffix2 = ".${+      final.substring 0 8 (+        self.lastModifiedDate or self.lastModified+          or "19700101"+        self.lastModifiedDate or self.lastModified or "19700101"+      )+    }";++    system.nixos.versionSuffix3 = "${+      final.substring 0 8 (+        self.lastModifiedDate or self.lastModified+          or "19700101"+        self.lastModifiedDate or self.lastModified or "19700101"+      )+    }";+  }+  (+    system nixos versionSuffix1 ".${+      final.substring 0 8 (+        self.lastModifiedDate or self.lastModified+          or "19700101"+        self.lastModifiedDate or self.lastModified or "19700101"+      )+    }.${self.shortRev or "dirty"}"+  )+  (+    system nixos versionSuffix2 ".${+      final.substring 0 8 (+        self.lastModifiedDate or self.lastModified+          or "19700101"+        self.lastModifiedDate or self.lastModified or "19700101"+      )+    }"+  )+  (+    system nixos versionSuffix3 "${+      final.substring 0 8 (+        self.lastModifiedDate or self.lastModified+          or "19700101"+        self.lastModifiedDate or self.lastModified or "19700101"+      )+    }"+  )+]
+ test/fixtures/nixfmt/diff/strip_space/in.nix view
@@ -0,0 +1,10 @@+/*+  a +  b	+*/+# a +# b	+{+  a = 1; +  b = 2;	+}
+ test/fixtures/nixfmt/invalid/interpolation-in-env-path.nix view
@@ -0,0 +1,1 @@+<nixpkgs/${"pkgs"}>
+ test/fixtures/nixfmt/invalid/interpolation-in-inherit-1.nix view
@@ -0,0 +1,4 @@+let+  bar = "bar";++in { inherit ${bar}; }
+ test/fixtures/nixfmt/invalid/interpolation-in-inherit-2.nix view
@@ -0,0 +1,1 @@+{ inherit ${"foo" + "bar"}; }
+ test/fixtures/nixfmt/invalid/interpolation-in-inherit-3.nix view
@@ -0,0 +1,1 @@+404: Not Found
+ test/fixtures/nixfmt/invalid/interpolation-in-inherit-4.nix view
@@ -0,0 +1,1 @@+404: Not Found
+ test/fixtures/nixfmt/invalid/naked-interpolation.nix view
@@ -0,0 +1,1 @@+${"foo"}
+ test/fixtures/nixfmt/invalid/path-starting-with-interpolation.nix view
@@ -0,0 +1,1 @@+${"/foo"}/bar
+ test/fixtures/nixfmt/invalid/path-with-escaped-interpolation.nix view
@@ -0,0 +1,1 @@+/foo\${"bar"}
+ test/fixtures/nixfmt/invalid/path-with-interpolation-before-slash.nix view
@@ -0,0 +1,1 @@+foo${"bar"}/baz
+ test/fixtures/nixfmt/invalid/smiley.nix view
@@ -0,0 +1,1 @@+;-)
+ test/sample.nix view
@@ -0,0 +1,68 @@+let+  apply = a b + 1;+  assert_ = assert a == 1; 2;+  s = {+    m = 3;+    g = 4;+  };+in {+  inherit apply;+  inherit (s) m g;+  inherit "m";+  inherit ${"k"};+  inherit (g);+  inherit;+  a.b.c = 1;+  apply2 = a b c d e f;+  float = 2.33;+  "${dynamic}".${attr} = g;+  empty_set = { };+  rec_set = rec { k = 233; };+  bool_arith = a >= b || c < d && e == f;+  bool = false -> !true || true && false;+  # comment f+  f =+    x:+    # comment y+    y:+    x;+  has_attr = a ? b && c;+  if_else = if a then b else if c then d else e;+  import_nix = import <nixpkgs> { };+  interpol_d = "${qwq}";+  interpol_ds = ''+    ${qwq}+    ${{x = 1;}.x}+  '';+  lambda1 = _:233;+  lambda2 = x:[x];+  lambda3 = x: y: x;+  list = [1 2 3 "4" false k] ++ [l];+  arith = 1 / 2 + (3 + 4) + -5;+  merge = {x = 1;} // {x = 2;};+  path_1 = 1/2;+  # unsupported for now+  #+  # op_no_ws_ = 1*-23;+  # multi_has = {} ? a ? b ? c;+  # legacy_let = let { x = 1; body = x; };+  # floating = .1;+  comment_no_ws = [a/**/];+  or_is_ident = a b or c;+  or = 1;+  x.or = 1;+  path_root = /nix;+  path_home = ~/nix;+  path_env = <nixpkgs/nixos>;+  path_cur = ./nix;+  path_interpol_1 = ./${a}-${b}/c/d${e};+  path_interpol_2 = c/d${e};+  as_left = a@{x}: x;+  as_right = {x}@a: x;+  param = {x ? 1, y ? {}, ...}: x;+  param_comma = {x,}: x;+  sel_or = a.b.c or d.e;+  sel_interpol = a.b.${c}."d"."${"e"}";+  uri = https://www.example.com;+  w = with l; 1;+}