{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-name-shadowing #-} module Nix.Pretty where import Prelude hiding ( toList, group ) import Nix.Utils import Control.Monad.Free ( Free(Free) ) import Data.Fix ( Fix(..) , foldFix ) import Data.HashMap.Lazy ( toList ) import qualified Data.HashMap.Lazy as M import qualified Data.HashSet as HashSet import qualified Data.List.NonEmpty as NE import Data.Text ( replace , strip ) import qualified Data.Text as Text import Prettyprinter hiding ( list ) import Nix.Atoms import Nix.Cited import Nix.Expr import Nix.Expr.Strings import Nix.Normal import Nix.Parser import Nix.String import Nix.Thunk import Nix.Value -- | This type represents a pretty printed nix expression -- together with some information about the expression. data NixDoc ann = NixDoc { -- | The rendered expression, without any parentheses. withoutParens :: Doc ann -- | The root operator is the operator at the root of -- the expression tree. For example, in '(a * b) + c', '+' would be the root -- operator. It is needed to determine if we need to wrap the expression in -- parentheses. , rootOp :: OperatorInfo , wasPath :: Bool -- This is needed so that when a path is used in a selector path -- we can add brackets appropriately } mkNixDoc :: OperatorInfo -> Doc ann -> NixDoc ann mkNixDoc o d = NixDoc { withoutParens = d, rootOp = o, wasPath = False } -- | A simple expression is never wrapped in parentheses. The expression -- behaves as if its root operator had a precedence higher than all -- other operators (including function application). simpleExpr :: Doc ann -> NixDoc ann simpleExpr = mkNixDoc (OperatorInfo minBound NAssocNone "simple expr") pathExpr :: Doc ann -> NixDoc ann pathExpr d = (simpleExpr d) { wasPath = True } -- | An expression that behaves as if its root operator had a precedence lower -- than all other operators. That ensures that the expression is wrapped in -- parentheses in almost always, but it's still rendered without parentheses -- in cases where parentheses are never required (such as in the LHS of a -- binding). leastPrecedence :: Doc ann -> NixDoc ann leastPrecedence = mkNixDoc (OperatorInfo maxBound NAssocNone "least precedence") appOp :: OperatorInfo appOp = getBinaryOperator NApp appOpNonAssoc :: OperatorInfo appOpNonAssoc = (getBinaryOperator NApp) { associativity = NAssocNone } selectOp :: OperatorInfo selectOp = getSpecialOperator NSelectOp hasAttrOp :: OperatorInfo hasAttrOp = getSpecialOperator NHasAttrOp wrapParens :: OperatorInfo -> NixDoc ann -> Doc ann wrapParens op sub = bool (\ a -> "(" <> a <> ")") id (precedence (rootOp sub) < precedence op || (precedence (rootOp sub) == precedence op && associativity (rootOp sub) == associativity op && associativity op /= NAssocNone) ) (withoutParens sub) -- Used in the selector case to print a path in a selector as -- "${./abc}" wrapPath :: OperatorInfo -> NixDoc ann -> Doc ann wrapPath op sub = bool (wrapParens op sub) ("\"${" <> withoutParens sub <> "}\"") (wasPath sub) prettyString :: NString (NixDoc ann) -> Doc ann prettyString (DoubleQuoted parts) = "\"" <> (mconcat . fmap prettyPart $ parts) <> "\"" where -- It serializes Text -> String, because the helper code is done for String, -- please, can someone break that code. prettyPart (Plain t) = pretty . concatMap escape . toString $ t prettyPart EscapedNewline = "''\\n" prettyPart (Antiquoted r) = "${" <> withoutParens r <> "}" escape '"' = "\\\"" escape x = maybe [x] (('\\' :) . (: mempty)) (toEscapeCode x) prettyString (Indented _ parts) = group $ nest 2 $ vcat ["''", content, "''"] where content = vsep . fmap prettyLine . stripLastIfEmpty . splitLines $ parts stripLastIfEmpty = reverse . f . reverse where f ([Plain t] : xs) | Text.null (strip t) = xs f xs = xs prettyLine = hcat . fmap prettyPart prettyPart (Plain t) = pretty . replace "${" "''${" . replace "''" "'''" $ t prettyPart EscapedNewline = "\\n" prettyPart (Antiquoted r) = "${" <> withoutParens r <> "}" prettyParams :: Params (NixDoc ann) -> Doc ann prettyParams (Param n ) = pretty n prettyParams (ParamSet s v mname) = prettyParamSet s v <> maybe mempty (\ name -> bool mempty ("@" <> pretty name) (not (Text.null name)) ) mname prettyParamSet :: ParamSet (NixDoc ann) -> Bool -> Doc ann prettyParamSet args var = encloseSep "{ " (align " }") sep (fmap prettySetArg args <> prettyVariadic) where prettySetArg (n, maybeDef) = maybe (pretty n) (\x -> pretty n <> " ? " <> withoutParens x) maybeDef prettyVariadic = [ "..." | var ] sep = align ", " prettyBind :: Binding (NixDoc ann) -> Doc ann prettyBind (NamedVar n v _p) = prettySelector n <> " = " <> withoutParens v <> ";" prettyBind (Inherit s ns _p) = "inherit " <> scope <> align (fillSep (fmap prettyKeyName ns)) <> ";" where scope = maybe mempty ((<> " ") . parens . withoutParens) s prettyKeyName :: NKeyName (NixDoc ann) -> Doc ann prettyKeyName (StaticKey "") = "\"\"" prettyKeyName (StaticKey key) | HashSet.member key reservedNames = "\"" <> pretty key <> "\"" prettyKeyName (StaticKey key) = pretty key prettyKeyName (DynamicKey key) = runAntiquoted (DoubleQuoted [Plain "\n"]) prettyString (\ x -> "${" <> withoutParens x <> "}") key prettySelector :: NAttrPath (NixDoc ann) -> Doc ann prettySelector = hcat . punctuate "." . fmap prettyKeyName . NE.toList prettyAtom :: NAtom -> NixDoc ann prettyAtom atom = simpleExpr $ pretty $ atomText atom prettyNix :: NExpr -> Doc ann prettyNix = withoutParens . foldFix exprFNixDoc prettyOriginExpr :: forall t f m ann . HasCitations1 m (NValue t f m) f => NExprLocF (Maybe (NValue t f m)) -> Doc ann prettyOriginExpr = withoutParens . go where go = exprFNixDoc . annotated . getCompose . fmap render render :: Maybe (NValue t f m) -> NixDoc ann render Nothing = simpleExpr "_" render (Just (Free (reverse . citations @m -> p:_))) = go (_originExpr p) render _ = simpleExpr "?" -- render (Just (NValue (citations -> ps))) = -- simpleExpr $ foldr ((\x y -> vsep [x, y]) . parens . indent 2 . withoutParens -- . go . originExpr) -- mempty (reverse ps) exprFNixDoc :: NExprF (NixDoc ann) -> NixDoc ann exprFNixDoc = \case NConstant atom -> prettyAtom atom NStr str -> simpleExpr $ prettyString str NList xs -> prettyContainer "[" (wrapParens appOpNonAssoc) "]" xs NSet NNonRecursive xs -> prettyContainer "{" prettyBind "}" xs NSet NRecursive xs -> prettyContainer "rec {" prettyBind "}" xs NAbs args body -> leastPrecedence $ nest 2 $ vsep [ prettyParams args <> ":" , withoutParens body ] NBinary NApp fun arg -> mkNixDoc appOp (wrapParens appOp fun <> " " <> wrapParens appOpNonAssoc arg) NBinary op r1 r2 -> mkNixDoc opInfo $ hsep [ wrapParens (f NAssocLeft) r1 , pretty $ operatorName opInfo , wrapParens (f NAssocRight) r2 ] where opInfo = getBinaryOperator op f x | associativity opInfo /= x = opInfo { associativity = NAssocNone } | otherwise = opInfo NUnary op r1 -> mkNixDoc opInfo (pretty (operatorName opInfo) <> wrapParens opInfo r1) where opInfo = getUnaryOperator op NSelect r' attr o -> maybe (mkNixDoc selectOp) (const leastPrecedence) o $ wrapPath selectOp r <> "." <> prettySelector attr <> ordoc where r = mkNixDoc selectOp (wrapParens appOpNonAssoc r') ordoc = maybe mempty ((" or " <>) . wrapParens appOpNonAssoc) o NHasAttr r attr -> mkNixDoc hasAttrOp (wrapParens hasAttrOp r <> " ? " <> prettySelector attr) NEnvPath p -> simpleExpr $ pretty ("<" <> p <> ">") NLiteralPath p -> pathExpr $ pretty $ case p of "./" -> "./." "../" -> "../." ".." -> "../." _txt -> bool ("./" <> _txt) _txt (any (`isPrefixOf` _txt) ["/", "~/", "./", "../"]) NSym name -> simpleExpr $ pretty name NLet binds body -> leastPrecedence $ group $ vsep [ "let" , indent 2 (vsep (fmap prettyBind binds)) , "in " <> withoutParens body ] NIf cond trueBody falseBody -> leastPrecedence $ group $ nest 2 $ sep [ "if " <> withoutParens cond , align ("then " <> withoutParens trueBody) , align ("else " <> withoutParens falseBody) ] NWith scope body -> prettyAddScope "with " scope body NAssert cond body -> prettyAddScope "assert " cond body NSynHole name -> simpleExpr $ pretty ("^" <> name) where prettyContainer h f t c = list (simpleExpr (h <> t)) (const $ simpleExpr $ group $ nest 2 $ vsep $ [h] <> (f <$> c) <> [t]) c prettyAddScope h c b = leastPrecedence $ vsep [h <> withoutParens c <> ";", align $ withoutParens b] valueToExpr :: forall t f m . MonadDataContext f m => NValue t f m -> NExpr valueToExpr = iterNValue (\_ _ -> thk) (Fix . phi) where thk = Fix . NSym $ "" phi :: NValue' t f m NExpr -> NExprF NExpr phi (NVConstant' a ) = NConstant a phi (NVStr' ns ) = NStr $ DoubleQuoted [Plain (stringIgnoreContext ns)] phi (NVList' l ) = NList l phi (NVSet' s p) = NSet NNonRecursive [ NamedVar (StaticKey k :| mempty) v (fromMaybe nullPos (M.lookup k p)) | (k, v) <- toList s ] phi (NVClosure' _ _) = NSym "" phi (NVPath' p ) = NLiteralPath p phi (NVBuiltin' name _) = NSym $ "builtins." <> name prettyNValue :: forall t f m ann . MonadDataContext f m => NValue t f m -> Doc ann prettyNValue = prettyNix . valueToExpr prettyNValueProv :: forall t f m ann . ( HasCitations m (NValue t f m) t , HasCitations1 m (NValue t f m) f , MonadThunk t m (NValue t f m) , MonadDataContext f m ) => NValue t f m -> Doc ann prettyNValueProv v = list prettyNVal (\ ps -> fillSep [ prettyNVal , indent 2 $ "(" <> mconcat ("from: " : (prettyOriginExpr . _originExpr <$> ps)) <> ")" ] ) (citations @m @(NValue t f m) v) where prettyNVal = prettyNValue v prettyNThunk :: forall t f m ann . ( HasCitations m (NValue t f m) t , HasCitations1 m (NValue t f m) f , MonadThunk t m (NValue t f m) , MonadDataContext f m ) => t -> m (Doc ann) prettyNThunk t = do let ps = citations @m @(NValue t f m) @t t v' <- prettyNValue <$> dethunk t pure $ fillSep [ v' , indent 2 $ "(" <> mconcat ( "thunk from: " : (prettyOriginExpr . _originExpr <$> ps)) <> ")" ] -- | This function is used only by the testing code. printNix :: forall t f m . MonadDataContext f m => NValue t f m -> String printNix = iterNValue (\_ _ -> thk) phi where thk = "" phi :: NValue' t f m String -> String phi (NVConstant' a ) = toString $ atomText a phi (NVStr' ns) = show $ stringIgnoreContext ns phi (NVList' l ) = toString $ "[ " <> unwords (fmap toText l) <> " ]" phi (NVSet' s _) = "{ " <> concat [ check (toString k) <> " = " <> v <> "; " | (k, v) <- sort $ toList s ] <> "}" where check :: [Char] -> [Char] check v = fromMaybe v (fmap (surround . show) (readMaybe v :: Maybe Int) <|> fmap (surround . show) (readMaybe v :: Maybe Float) ) where surround s = "\"" <> s <> "\"" phi NVClosure'{} = "<>" phi (NVPath' fp ) = fp phi (NVBuiltin' name _) = toString $ "< name <> ">>"