packages feed

nix-diff-1.0.20: src/Nix/Diff/Render/HumanReadable.hs

{-# LANGUAGE ApplicativeDo              #-}
{-# LANGUAGE BlockArguments             #-}
{-# LANGUAGE CPP                        #-}
{-# LANGUAGE DuplicateRecordFields      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE NamedFieldPuns             #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE RecordWildCards            #-}

module Nix.Diff.Render.HumanReadable where

import Control.Monad (forM_)
import Control.Monad.Reader (MonadReader, ReaderT (runReaderT), ask, local)
import Control.Monad.Writer(MonadWriter, Writer, tell, runWriter)
import Data.Set (Set)
import Data.Text (Text)
import Numeric.Natural (Natural)

import qualified Control.Monad.Reader
import qualified Data.Map
import qualified Data.Set
import qualified Data.Text            as Text
import qualified Patience

#if !MIN_VERSION_base(4,15,1)
import Control.Monad.Fail (MonadFail)
#endif

import Nix.Diff
import Nix.Diff.Types


data RenderContext = RenderContext
  { orientation :: Orientation
  , tty         :: TTY
  , indent      :: Natural
  , context     :: Maybe Natural
  }

newtype Render a = Render { unRender :: ReaderT RenderContext (Writer Text) a}
    deriving
    ( Functor
    , Applicative
    , Monad
    , MonadReader RenderContext
    , MonadWriter Text
    )

runRender :: Render a -> RenderContext ->  (a, Text)
runRender render rc = runWriter $  runReaderT (unRender render) rc

runRender' :: Render () -> RenderContext -> Text
runRender' render = snd . runRender render

echo :: Text -> Render ()
echo text = do
    RenderContext { indent } <- ask
    let n = fromIntegral indent
    tellLn (Text.replicate n " " <> text)
  where
    tellLn line = tell (line <> "\n")

indented :: Natural -> Render a -> Render a
indented n = local adapt
  where
    adapt context = context { indent = indent context + n }

data TTY = IsTTY | NotTTY

-- | Color text red
red :: TTY -> Text -> Text
red  IsTTY text = "\ESC[1;31m" <> text <> "\ESC[0m"
red NotTTY text = text

-- | Color text background red
redBackground  :: Orientation -> TTY -> Text -> Text
redBackground Line IsTTY text = "\ESC[41m" <> prefix <> "\ESC[0m" <> suffix
  where
    (prefix, suffix) = Text.break lineBoundary text
redBackground Word IsTTY text = "\ESC[41m" <> prefix <> "\ESC[0m" <> suffix
  where
    (prefix, suffix) = Text.break wordBoundary text
redBackground Character IsTTY text = "\ESC[41m" <> text <> "\ESC[0m"
redBackground Line NotTTY text = "- " <> text
redBackground _    NotTTY text = "←" <> text <> "←"

-- | Color text green
green :: TTY -> Text -> Text
green IsTTY  text = "\ESC[1;32m" <> text <> "\ESC[0m"
green NotTTY text = text

-- | Color text background green
greenBackground :: Orientation -> TTY -> Text -> Text
greenBackground Line IsTTY text = "\ESC[42m" <> prefix <> "\ESC[0m" <> suffix
  where
    (prefix, suffix) = Text.break lineBoundary text
greenBackground Word IsTTY text = "\ESC[42m" <> prefix <> "\ESC[0m" <> suffix
  where
    (prefix, suffix) = Text.break wordBoundary text
greenBackground Character IsTTY  text = "\ESC[42m" <> text <> "\ESC[0m"
greenBackground Line NotTTY text = "+ " <> text
greenBackground _    NotTTY text = "→" <> text <> "→"

-- | Color text grey
grey :: Orientation -> TTY -> Text -> Text
grey _    IsTTY  text = "\ESC[1;2m" <> text <> "\ESC[0m"
grey Line NotTTY text = "  " <> text
grey _    NotTTY text = text

-- | Format the left half of a diff
minus :: TTY -> Text -> Text
minus tty text = red tty ("- " <> text)

-- | Format the right half of a diff
plus :: TTY -> Text -> Text
plus tty text = green tty ("+ " <> text)

-- | Format text explaining a diff
explain :: Text -> Text
explain text = "• " <> text

{-| Utility to automate a common pattern of printing the two halves of a diff.
    This passes the correct formatting function to each half
-}
renderWith :: Changed a -> ((Text -> Text, a) -> Render ()) -> Render ()
renderWith Changed{..} k = do
    RenderContext { tty } <- ask
    k (minus tty, before)
    k (plus  tty, now)

-- | Format the derivation outputs
renderOutputs :: Set Text -> Text
renderOutputs outputs =
    ":{" <> Text.intercalate "," (Data.Set.toList outputs) <> "}"

renderDiffHumanReadable :: DerivationDiff -> Render ()
renderDiffHumanReadable = \case
    DerivationsAreTheSame -> pure ()
    AlreadyCompared ->  echo (explain "These two derivations have already been compared")
    OnlyAlreadyComparedBelow {..} -> do
      renderOutputStructure outputStructure
      echo (explain "Skipping because only derivations that have already been compared and shown in the diff are below")
    NamesDontMatch {..} -> do
      renderOutputStructure outputStructure
      echo (explain "The derivation names do not match")
    OutputsDontMatch {..} -> do
      renderOutputStructure outputStructure
      echo (explain "The requested outputs do not match")
    DerivationDiff {..} -> do
      renderOutputStructure outputStructure
      renderOutputsDiff outputsDiff
      renderPlatformDiff platformDiff
      renderBuilderDiff builderDiff
      renderArgsDiff argumentsDiff
      renderSrcDiff sourcesDiff
      renderInputsDiff inputsDiff
      renderEnvDiff envDiff

  where
    renderOutputStructure os =
      renderWith os \(sign, (OutputStructure path outputs)) -> do
        echo (sign (Text.pack path <> renderOutputs outputs))

    renderOutputsDiff OutputsDiff{..} = do
      ifExist extraOutputs \eo -> do
        echo (explain "The set of outputs do not match:")
        renderWith eo \(sign, extraOutputs') -> do
          forM_ (Data.Map.toList extraOutputs') \(key, _value) -> do
              echo ("    " <> sign ("{" <> key <> "}"))
      mapM_ renderOutputHashDiff outputHashDiff

    renderOutputHashDiff OutputDiff{..} = do
      echo (explain ("{" <> outputName <> "}:"))
      echo (explain "    Hash algorithm:")
      renderWith hashDifference \(sign, hashAlgo) -> do
          echo ("        " <> sign hashAlgo)

    renderPlatformDiff mpd =
      ifExist mpd \pd -> do
        echo (explain "The platforms do not match")
        renderWith pd \(sign, platform) -> do
           echo ("    " <> sign platform)

    renderBuilderDiff mbd =
      ifExist mbd \bd -> do
        echo (explain "The builders do not match")
        renderWith bd \(sign, builder) -> do
          echo ("    " <> sign builder)

    renderArgsDiff mad =
      ifExist mad \(ArgumentsDiff ad) -> do
        RenderContext { tty } <- ask
        echo (explain "The arguments do not match")
        let renderDiff (Patience.Old arg) =
                echo ("    " <> minus tty arg)
            renderDiff (Patience.New arg) =
                echo ("    " <> plus tty arg)
            renderDiff (Patience.Both arg _) =
                echo ("    " <> explain arg)
        mapM_ renderDiff ad

    renderSrcDiff SourcesDiff{..} = do
      ifExist extraSrcNames \esn -> do
        echo (explain "The set of input source names do not match:")
        renderWith esn \(sign, names) -> do
          forM_ names \name -> do
              echo ("    " <> sign name)

      mapM_ renderSrcFileDiff srcFilesDiff

    renderSrcFileDiff OneSourceFileDiff{..} = do
      echo (explain ("The input source named `" <> srcName <> "` differs"))
      ifExist srcContentDiff \scd -> do
        text <- renderText scd
        echo ("    " <> text)
    renderSrcFileDiff SomeSourceFileDiff{..} = do
      echo (explain ("The input sources named `" <> srcName <> "` differ"))
      renderWith srcFileDiff \(sign, paths) -> do
        forM_ paths \path -> do
            echo ("    " <>  sign (Text.pack path))

    renderInputsDiff InputsDiff{..} = do
      renderInputExtraNames inputExtraNames
      mapM_ renderInputDerivationsDiff inputDerivationDiffs

    renderInputExtraNames mien =
      ifExist mien \ien -> do
        echo (explain "The set of input derivation names do not match:")
        renderWith ien \(sign, names) -> do
          forM_ names \name -> do
              echo ("    " <> sign name)

    renderInputDerivationsDiff OneDerivationDiff{..} = do
      echo (explain ("The input derivation named `" <> drvName <> "` differs"))
      indented 2 (renderDiffHumanReadable drvDiff)
    renderInputDerivationsDiff SomeDerivationsDiff{..} = do
      echo (explain ("The set of input derivations named `" <> drvName <> "` do not match"))
      renderWith extraPartsDiff \(sign, extraPaths) -> do
        forM_ (Data.Map.toList extraPaths) \(extraPath, outputs) -> do
          echo ("    " <> sign (Text.pack extraPath <> renderOutputs outputs))

    renderEnvDiff Nothing =
      echo (explain "Skipping environment comparison")
    renderEnvDiff (Just EnvironmentsAreEqual) = pure ()
    renderEnvDiff (Just EnvironmentDiff{..}) = do
      echo (explain "The environments do not match:")
      renderWith extraEnvDiff \(sign, extraEnv) -> do
        forM_ (Data.Map.toList extraEnv) \(key, value) -> do
            echo ("    " <> sign (key <> "=" <> value))
      forM_ envContentDiff \EnvVarDiff{..} -> do
        text <- renderText envValueDiff
        echo ("    " <> envKey <> "=" <> text)

    renderText :: TextDiff -> Render Text
    renderText (TextDiff chunks) = do
      RenderContext{ indent, orientation, tty, context } <- ask

      let n = fromIntegral indent

      let prefix = Text.replicate n " "

      let format text =
              if 80 <= n + Text.length text
              then "''\n" <> indentedText <> prefix <> "''"
              else text
            where
              indentedText =
                  (Text.unlines . fmap indentLine . Text.lines) text
                where
                  indentLine line = prefix <> "    " <> line

      let renderChunk (Patience.Old  l  ) =
              redBackground   orientation tty l
          renderChunk (Patience.New    r) =
              greenBackground orientation tty r
          renderChunk (Patience.Both l _) =
              grey            orientation tty l

      let windowedChunks = case context of
              Nothing -> chunks
              Just m  -> fmap middle (filter predicate (zippers chunks))
                where
                  notBoth (Patience.Both _ _) = False
                  notBoth  _                  = True

                  nat = fromIntegral m

                  predicate (before, line, after) =
                      any notBoth (line : take nat before ++ take nat after)

                  middle (_, line, _) = line
      return (format (Text.concat (fmap renderChunk windowedChunks)))

    ifExist m l = maybe (pure ()) l m

zippers :: [a] -> [([a], a, [a])]
zippers = go []
  where
    go _           []  = []
    go prefix (x : xs) = (prefix, x, xs) : go (x : prefix) xs