jet-0.0.0.0: src/Jet.hs
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Jet (run) where
import Control.Category ((>>>))
import Control.Comonad (extract)
import qualified Control.Comonad as Comonad
import Control.Comonad.Cofree
import qualified Control.Comonad.Trans.Cofree as CofreeF
import Control.Lens hiding ((:<))
import qualified Control.Lens.Cons as Cons
import Control.Monad.State
import Control.Monad.Trans.Maybe (MaybeT (MaybeT, runMaybeT))
import Data.Aeson (Value)
import qualified Data.Aeson as Aeson
import Data.Aeson.Encode.Pretty (encodePretty)
import Data.Aeson.Extra
import qualified Data.ByteString.Lazy.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import Data.Functor.Classes (Eq1 (..), Ord1 (liftCompare))
import qualified Data.Functor.Foldable as FF
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HM
import qualified Data.HashMap.Strict as HashMap
import Data.Hashable (Hashable)
import qualified Data.List as List
import Data.Maybe
import Data.Sequence (Seq)
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Zipper as TZ
import Data.Vector (Vector)
import qualified Data.Vector as Vector
import qualified Graphics.Vty as Vty
import Graphics.Vty.Input.Events
import qualified Jet.Render as Render
import Prettyprinter as P
import System.Environment (getArgs)
import System.Exit (exitFailure)
import System.Hclip
import System.IO (IOMode (ReadWriteMode), openFile)
import qualified System.IO as IO
import qualified System.Posix as Posix
import Text.Read (readMaybe)
import qualified Zipper.Recursive as Z
tabSize :: Int
tabSize = 2
maxUndoStates :: Int
maxUndoStates = 100
hoistMaybe :: Maybe a -> MaybeT Editor a
hoistMaybe = MaybeT . pure
data EditorState = EditorState
{ _undo :: UndoZipper (Z.Zipper JIndex ValueF FocusState),
_mode :: Mode,
_register :: ValueF (Cofree ValueF FocusState),
_vty :: Vty.Vty,
_flash :: Text,
_save :: Z.Zipper JIndex ValueF FocusState -> Editor ()
}
newtype Editor a = Editor {runEditor :: StateT EditorState IO a}
deriving newtype (Functor, Applicative, Monad, MonadState EditorState, MonadIO)
mode_ :: Lens' EditorState Mode
mode_ = lens _mode (\s m -> s {_mode = m})
register_ :: Lens' EditorState (ValueF (Cofree ValueF FocusState))
register_ = lens _register (\s m -> s {_register = m})
undo_ :: Lens' EditorState (UndoZipper (Z.Zipper JIndex ValueF FocusState))
undo_ = lens _undo (\s m -> s {_undo = m})
vty_ :: Lens' EditorState Vty.Vty
vty_ = lens _vty (\s m -> s {_vty = m})
flash_ :: Lens' EditorState Text
flash_ = lens _flash (\s m -> s {_flash = m})
save_ :: Lens' EditorState (Z.Zipper JIndex ValueF FocusState -> Editor ())
save_ = lens _save (\s m -> s {_save = m})
recover :: a -> MaybeT Editor a -> Editor a
recover def m = do
let e = runMaybeT m
s <- get
r <- liftIO $ flip runStateT s . runEditor $ e
case r of
(Just a, newS) -> put newS *> pure a
(Nothing, _) -> pure def
data Focused = Focused | NotFocused
deriving (Eq)
data Folded = Folded | NotFolded
deriving (Eq)
type PrettyJSON = Doc (Either Render.Cursor Vty.Attr)
type Buffer = TZ.TextZipper Text
-- | Nodes are annotated with one of these.
-- This includes information about the node itself, but also
-- a cached render of the node, which allows us to re-render
-- the whole tree much faster.
data FocusState = FocusState
{ isFocused :: Focused,
isFolded :: Folded,
rendered :: PrettyJSON
}
instance Eq FocusState where
a == b =
isFocused a == isFocused b
&& isFolded a == isFolded b
focused_ :: Lens' FocusState Focused
focused_ = lens isFocused (\fs new -> fs {isFocused = new})
folded_ :: Lens' FocusState Folded
folded_ = lens isFolded (\fs new -> fs {isFolded = new})
toggleFold :: Folded -> Folded
toggleFold Folded = NotFolded
toggleFold NotFolded = Folded
run :: IO ()
run = do
(json, srcFile) <-
getArgs >>= \case
[] -> do
json <-
(Aeson.eitherDecode . BS.pack <$> getContents) >>= \case
Left err -> do
IO.hPutStrLn IO.stderr err
exitFailure
Right json -> pure json
pure (json, Nothing)
[f] -> do
json <-
Aeson.eitherDecodeFileStrict f >>= \case
Left err -> do
IO.hPutStrLn IO.stderr err
exitFailure
Right json -> pure json
pure (json, Just f)
_ -> IO.hPutStrLn IO.stderr "usage: structural-json FILE.json" *> exitFailure
result <- edit srcFile $ json
BS.putStrLn $ encodePretty result
edit :: Maybe FilePath -> Value -> IO Value
edit srcFile value = do
-- Use tty so we don't interfere with stdin/stdout
tty <- openFile "/dev/tty" ReadWriteMode >>= Posix.handleToFd
config <- liftIO $ Vty.standardIOConfig
vty <- (liftIO $ Vty.mkVty config {Vty.inputFd = Just tty, Vty.outputFd = Just tty})
-- load the value into a zipper.
let z = Z.zipper . toCofree $ value
v <- flip evalStateT (editorState srcFile vty) . runEditor $ loop z
Vty.shutdown vty
pure (Z.flatten v)
loop ::
Z.Zipper JIndex ValueF FocusState ->
Editor (Z.Zipper JIndex ValueF FocusState)
loop z = do
vty <- use vty_
renderScreen z
flash_ .= ""
e <- liftIO $ Vty.nextEvent vty
nextZ <- handleEvent e z
if (shouldExit e)
then pure nextZ
else (loop nextZ)
renderScreen :: Z.Zipper JIndex ValueF FocusState -> Editor ()
renderScreen z = do
vty <- use vty_
(winWidth, winHeight) <- bounds
rendered <- uses mode_ (\m -> fullRender m z)
footer <- footerImg
let screen = Vty.vertCat . Render.renderScreen (winHeight - Vty.imageHeight footer) . layoutPretty defaultLayoutOptions $ rendered
let spacerHeight = winHeight - (Vty.imageHeight screen + Vty.imageHeight footer)
let spacers = Vty.charFill Vty.defAttr ' ' winWidth spacerHeight
liftIO $ Vty.update vty (Vty.picForImage (screen Vty.<-> spacers Vty.<-> footer))
-- | Get the current bounds of the current terminal screen.
bounds :: Editor (Int, Int)
bounds = use vty_ >>= liftIO . Vty.displayBounds . Vty.outputIface
-- | Render the footer bar to an image
footerImg :: Editor Vty.Image
footerImg = do
(w, _) <- bounds
flash <- gets _flash
let attr = (Vty.defAttr `Vty.withForeColor` Vty.green `Vty.withStyle` Vty.reverseVideo)
helpMsg = Vty.text' attr "| Press '?' for help"
flashMsg = Vty.text' (attr `Vty.withStyle` Vty.bold) (" " <> flash)
pure $
Vty.horizCat
[ flashMsg,
Vty.charFill attr ' ' (w - (Vty.imageWidth helpMsg + Vty.imageWidth flashMsg)) 1,
helpMsg
]
-- | Push the given zipper onto history iff it's distinct from the most recent undo state.
pushUndo :: Z.Zipper JIndex ValueF FocusState -> Editor ()
pushUndo z =
undo_ %= \case
(UndoZipper (ls Cons.:> _) _) | length ls >= maxUndoStates -> UndoZipper (z <| ls) Empty
(UndoZipper ls _) -> UndoZipper (z <| ls) Empty
editorState :: Maybe FilePath -> Vty.Vty -> EditorState
editorState srcFile vty =
EditorState
{ _undo = UndoZipper Empty Empty,
_mode = Move,
_register = NullF,
_vty = vty,
_flash = "Hello World",
_save = saveFile
}
where
saveFile = case srcFile of
Nothing -> const (pure ())
Just fp -> \z -> do
liftIO $ BS.writeFile fp (z & Z.flatten & encodePretty @Value)
flash_ .= "Saved to " <> Text.pack fp
shouldExit :: Vty.Event -> Bool
shouldExit = \case
EvKey (KChar 'c') [Vty.MCtrl] -> True
EvKey (KChar 'q') [] -> True
_ -> False
bufferText :: Buffer -> Text
bufferText = Text.concat . TZ.getText
-- | Apply the state that's in the current mode's buffer to the selected node if possible.
applyBuf :: Z.Zipper JIndex ValueF FocusState -> Editor (Z.Zipper JIndex ValueF FocusState)
applyBuf z = do
use mode_ >>= \case
Edit buf -> do
let txt = buf ^. to bufferText
mode_ .= Move
pure
( z & Z.unwrapped_ . _unwrap
%~ ( \case
StringF _ -> StringF txt
(NumberF n) -> NumberF . fromMaybe n . readMaybe $ Text.unpack txt
x -> x
)
)
KeyEdit key buf -> do
let txt = buf ^. to bufferText
mode_ .= (KeyMove txt)
pure
( z & Z.unwrapped_ . _unwrap
%~ ( \case
(ObjectF hm) -> ObjectF $ renameKey key txt hm
x -> x
)
)
_ -> pure z
renameKey :: (Hashable k, Eq k) => k -> k -> HashMap k v -> HashMap k v
renameKey srcKey destKey hm =
hm
&~ do
v <- use (at srcKey)
at srcKey .= Nothing
at destKey .= v
-- | Create a buffer using the text from the current value.
bufferForValueF :: ValueF x -> Maybe Buffer
bufferForValueF = \case
(ObjectF _hm) -> Nothing
(ArrayF _vec) -> Nothing
StringF txt -> Just $ newBuffer txt
(NumberF sci) ->
Just $ newBuffer (Text.pack . show $ sci)
(BoolF True) -> Just $ newBuffer "true"
(BoolF False) -> Just $ newBuffer "true"
NullF -> Just $ newBuffer "null"
boolText_ :: Prism' Text Bool
boolText_ = prism' toText toBool
where
toText True = "true"
toText False = "false"
toBool "true" = Just True
toBool "false" = Just False
toBool _ = Nothing
data Mode
= Edit {_buf :: Buffer}
| Move
| KeyMove {_selectedKey :: Text}
| KeyEdit {_selectedKey :: Text, _buf :: Buffer}
deriving (Show)
buf_ :: Traversal' Mode Buffer
buf_ f = \case
Edit b -> Edit <$> f b
Move -> pure Move
KeyMove txt -> pure (KeyMove txt)
KeyEdit txt b -> KeyEdit txt <$> f b
-- | Main event handler
handleEvent :: Vty.Event -> Z.Zipper JIndex ValueF FocusState -> Editor (Z.Zipper JIndex ValueF FocusState)
handleEvent evt zipper = do
use mode_ >>= \case
KeyMove {} -> handleMove zipper
Move {} -> handleMove zipper
KeyEdit {} -> handleEdit zipper
Edit {} -> handleEdit zipper
where
handleEdit ::
( Z.Zipper JIndex ValueF FocusState ->
Editor (Z.Zipper JIndex ValueF FocusState)
)
handleEdit z =
case evt of
EvKey key [] ->
-- Perform buffer updates:
case key of
KChar c -> do
mode_ . buf_ %= TZ.insertChar c
pure z
KLeft -> do
mode_ . buf_ %= TZ.moveLeft
pure z
KRight -> do
mode_ . buf_ %= TZ.moveRight
pure z
KBS -> do
mode_ . buf_ %= TZ.deletePrevChar
pure z
KEsc -> do
newZ <- applyBuf z
pure $ newZ
_ -> pure z
_ -> pure z
handleMove ::
( Z.Zipper JIndex ValueF FocusState ->
Editor (Z.Zipper JIndex ValueF FocusState)
)
handleMove z =
case evt of
EvKey key mods -> case key of
-- move up
KChar 'h' -> z & outOf
-- move down
KChar 'l' -> do
z & Z.focus_ . folded_ .~ NotFolded
& into
-- next sibling
KChar 'j' -> z & sibling Forward
-- move down
KChar 'J' -> do
pushUndo z
pure (z & moveElement Forward)
-- prev sibling
KChar 'k' -> z & sibling Backward
-- move up
KChar 'K' -> do
pushUndo z
pure (z & moveElement Backward)
-- add new node
KChar 'i' -> do
pushUndo z
insert z
-- replace with boolean
KChar 'b' -> do
pushUndo z
pure (z & setFocus (BoolF True))
-- replace with object
KChar 'o' -> do
pushUndo z
pure (z & setFocus (ObjectF mempty))
-- replace with array
KChar 'a' -> do
pushUndo z
pure (z & setFocus (ArrayF mempty))
-- replace with number
KChar 'n' -> do
pushUndo z
pure (z & setFocus (NumberF 0))
-- replace with Null
KChar 'N' -> do
pushUndo z
pure (z & setFocus NullF)
-- Save file
KChar 's'
| [Vty.MCtrl] <- mods -> do
saver <- use save_
saver z
pure z
-- replace with string
KChar 's' -> do
pushUndo z
pure (z & setFocus (StringF ""))
-- undo
KChar 'u' -> do
flash_ .= "Undo"
undo_ %%= \case
(UndoZipper (l Cons.:< ls) rs) ->
(l, UndoZipper ls (z Cons.:< rs))
lz -> (z, lz)
-- redo
KChar 'r' | [Vty.MCtrl] <- mods -> do
flash_ .= "Redo"
undo_ %%= \case
(UndoZipper ls (r Cons.:< rs)) -> (r, UndoZipper (z Cons.:< ls) rs)
lz -> (z, lz)
-- toggle bool
KChar ' ' -> do
pushUndo z
pure (z & tryToggleBool)
-- copy
KChar 'y' -> do
flash_ .= "Copied"
copy z
-- paste
KChar 'p' -> do
flash_ .= "Paste"
pushUndo z
paste z
-- cut
KChar 'x' -> do
flash_ .= "Cut"
pushUndo z
copy z >>= delete
-- help
KChar '?' -> do
vty <- use vty_
liftIO $ Vty.update vty (Vty.picForImage helpImg)
void $ liftIO $ Vty.nextEvent vty
pure z
-- add child
KEnter -> do
pushUndo z
tryAddChild z
-- toggle fold
KChar '\t' -> do
-- Exit KeyMove mode if we're in it.
mode_ .= Move
pure $ (z & Z.focus_ . folded_ %~ toggleFold)
-- Fold all children
KChar 'F' -> do
-- Fold all child branches
pure $ mapChildren (mapped . folded_ .~ Folded) z
-- unfold all children
KChar 'f' -> do
-- Unfold all child branches
pure $ mapChildren (mapped . folded_ .~ NotFolded) z
-- delete node
KBS -> do
flash_ .= "Deleted"
pushUndo z
delete z
_ -> pure z
_ -> pure z
paste z = do
reg <- use register_
pure (z & setFocus reg)
copy z = do
let curVal = Z.branches z
register_ .= curVal
liftIO $ setClipboard (encodeValueFCofree curVal)
pure z
insert z = do
use mode_ >>= \case
KeyMove k -> do
mode_ .= KeyEdit k (newBuffer k)
pure $ z & Z.focus_ . folded_ .~ NotFolded
Move
| Just editBuf <- bufferForValueF (z ^. Z.branches_) -> do
mode_ .= Edit editBuf
pure $ z & Z.focus_ . folded_ .~ NotFolded
_ -> pure z
encodeValueFCofree :: ValueF (Cofree ValueF FocusState) -> String
encodeValueFCofree vf = LBS.unpack . encodePretty . FF.embed $ fmap (FF.cata alg) vf
where
alg :: CofreeF.CofreeF ValueF ann Value -> Value
alg (_ CofreeF.:< vf') = FF.embed vf'
-- | Set the value of the focused node.
setFocus ::
ValueF (Cofree ValueF FocusState) ->
Z.Zipper JIndex ValueF FocusState ->
Z.Zipper JIndex ValueF FocusState
setFocus f z = z & Z.branches_ .~ f & rerender
data Dir = Forward | Backward
-- | Move the current value within an array
moveElement :: Dir -> Z.Zipper JIndex ValueF FocusState -> Z.Zipper JIndex ValueF FocusState
moveElement dir z = fromMaybe z $ do
i <- case Z.currentIndex z of
Just (Index i) -> pure i
_ -> Nothing
parent <- z & rerender & Z.up
pure $
case parent ^. Z.branches_ of
ArrayF arr ->
let swapI = case dir of
Forward -> i + 1
Backward -> i - 1
moves =
[ (i, arr Vector.!? swapI),
(swapI, arr Vector.!? i)
]
& sequenceOf (traversed . _2)
& fromMaybe []
in parent
& Z.branches_ .~ ArrayF (arr Vector.// moves)
& fromMaybe z . Z.down (Index swapI)
_ -> z
tryToggleBool :: Z.Zipper JIndex ValueF FocusState -> Z.Zipper JIndex ValueF FocusState
tryToggleBool z =
z & Z.branches_ %~ \case
BoolF b -> BoolF (not b)
x -> x
tryAddChild :: Z.Zipper JIndex ValueF FocusState -> Editor (Z.Zipper JIndex ValueF FocusState)
tryAddChild z =
z & Z.branches_ %%~ \case
ObjectF hm -> do
mode_ .= (KeyEdit "" $ newBuffer "")
pure $ ObjectF $ HM.insert "" (toCofree Aeson.Null) hm
ArrayF arr -> do
mode_ .= Move
pure $ ArrayF $ arr <> pure (toCofree Aeson.Null)
x -> pure x
-- | Delete the current node
delete :: Z.Zipper JIndex ValueF FocusState -> Editor (Z.Zipper JIndex ValueF FocusState)
delete z = do
curMode <- use mode_
mode_ .= Move
pure $ case z ^. Z.branches_ of
-- If we're in a Key focus, delete that key
ObjectF hm
| KeyMove k <- curMode ->
( z & Z.branches_ .~ ObjectF (HM.delete k hm)
)
-- Otherwise move up a layer and delete the key we were in.
_ -> case Z.currentIndex z of
-- If we don't have a parent, set the current node to null
Nothing ->
z & Z.branches_ .~ NullF
Just i -> fromMaybe z $ do
parent <- z & rerender & Z.up
pure $
parent & Z.branches_ %~ \case
ObjectF hm | Key k <- i -> ObjectF (HM.delete k hm)
ArrayF arr | Index j <- i -> ArrayF (Vector.ifilter (\i' _ -> i' /= j) arr)
x -> x
-- | Move to next/previous sibling.
sibling :: Dir -> Z.Zipper JIndex ValueF FocusState -> Editor (Z.Zipper JIndex ValueF FocusState)
sibling dir z = recover z $ do
mode <- use mode_
case (mode, Z.branches z) of
(KeyMove k, ObjectF hm) -> do
case findSiblingIndex (== k) $ HashMap.keys hm of
Nothing -> pure z
Just theKey -> do
mode_ .= KeyMove theKey
pure z
_ -> do
curI <- hoistMaybe $ Z.currentIndex z
parent <- hoistMaybe $ (z & rerender & Z.up)
let newI = case Z.branches parent of
ObjectF hm -> do
let keys = HM.keys hm
newKey <- findSiblingIndex (\k -> Key k == curI) keys
pure $ Key newKey
ArrayF xs -> case curI of
(Index i) -> alterIndex xs i
_ -> Nothing
StringF {} -> Nothing
NumberF {} -> Nothing
BoolF {} -> Nothing
NullF -> Nothing
case newI of
Just i -> hoistMaybe $ Z.down i parent
Nothing -> hoistMaybe Nothing
where
(findSiblingIndex, alterIndex) = case dir of
Forward ->
( findAfter,
\xs i -> if i < length xs - 1 then Just (Index (i + 1)) else Nothing
)
Backward ->
( findBefore,
\_xs i -> if i > 0 then Just (Index (i -1)) else Nothing
)
findAfter :: (a -> Bool) -> [a] -> Maybe a
findAfter p xs = fmap snd . List.find (p . fst) $ zip xs (drop 1 xs)
findBefore :: (a -> Bool) -> [a] -> Maybe a
findBefore p xs = fmap snd . List.find (p . fst) $ zip (drop 1 xs) xs
newBuffer :: Text -> Buffer
newBuffer txt = TZ.gotoEOF $ TZ.textZipper (Text.lines txt) Nothing
-- | Move into the current node
into :: Z.Zipper JIndex ValueF FocusState -> Editor (Z.Zipper JIndex ValueF FocusState)
into z = do
mode <- use mode_
case (Z.branches z, mode) of
(ObjectF _, KeyMove key) -> do
mode_ .= Move
pure (Z.tug (Z.down (Key key)) z)
(ObjectF hm, Move) -> do
case (HM.keys hm) ^? _head of
Just fstKey -> do
mode_ .= KeyMove fstKey
pure z
_ -> pure z
(ArrayF {}, _) -> do
pure $ Z.tug (Z.down (Index 0)) z
_ -> pure z
-- | Move out of the current node
outOf :: Z.Zipper JIndex ValueF FocusState -> Editor (Z.Zipper JIndex ValueF FocusState)
outOf z = do
mode <- use mode_
maybeParentKey <- case (Z.currentIndex z) of
Just (Key k) -> pure $ Just k
_ -> pure Nothing
case (Z.branches z, mode) of
(ObjectF _, KeyMove {}) -> do
mode_ .= Move
pure z
_ -> do
maybe (pure ()) (\k -> mode_ .= KeyMove k) maybeParentKey
pure (Z.tug (rerender >>> Z.up) z)
-- | Render the full zipper using render caches stored in each node.
fullRender :: Mode -> Z.Zipper JIndex ValueF FocusState -> PrettyJSON
fullRender mode z = do
let focusedRender =
z & Z.focus_ . focused_ .~ Focused
& Z.unwrapped_ %~ \(fs :< vf) ->
let rerendered = renderSubtree fs mode (rendered . extract <$> vf)
in (fs {rendered = rerendered} :< vf)
rendered . foldSpine alg $ focusedRender
where
alg fs vf =
fs {rendered = rerenderCached fs (rendered <$> vf)}
rerenderCached fs = \case
ObjectF o -> prettyObj (isFocused fs) mode o
ArrayF a -> prettyArray (isFocused fs) a
-- Nodes without children are never part of the spine, but just to have something
-- we can render the cache.
_ -> rendered fs
-- | Updates the cached render of the current focus, using cached renders for subtrees.
rerender :: Z.Zipper JIndex ValueF FocusState -> Z.Zipper JIndex ValueF FocusState
rerender = Z.unwrapped_ %~ rerenderCofree
-- Rerenders a layer of a cofree structure. Doesn't re-render the children.
rerenderCofree :: Cofree ValueF FocusState -> Cofree ValueF FocusState
rerenderCofree (fs :< vf) =
let rerendered = (renderSubtree fs mode (rendered . extract <$> vf))
in fs {rendered = rerendered} :< vf
where
-- Currently the mode is required by renderSubtree, but for the rerender cache it's
-- irrelevant, because it only matters if we're 'focused', and if we're focused, we'll be
-- manually rerendered later anyways.
mode = Move
-- | Renders a subtree
renderSubtree :: FocusState -> Mode -> ValueF PrettyJSON -> PrettyJSON
renderSubtree (FocusState {isFolded = Folded, isFocused}) _ vf = case vf of
ObjectF {} -> colored' Vty.white "{...}"
ArrayF {} -> colored' Vty.white "[...]"
StringF {} -> colored' Vty.green "\"...\""
NumberF {} -> colored' Vty.blue "..."
NullF {} -> colored' Vty.yellow "..."
BoolF {} -> colored' Vty.magenta "..."
where
colored' :: Vty.Color -> String -> PrettyJSON
colored' col txt =
P.annotate (Right $ if isFocused == Focused then reverseCol col else Vty.defAttr `Vty.withForeColor` col) (pretty txt)
renderSubtree (FocusState {isFocused}) mode vf = case vf of
(StringF txt) -> cursor isFocused $ case (isFocused, mode) of
(Focused, Edit buf) ->
colored' Vty.green "\"" <> renderBuffer Vty.green buf <> colored' Vty.green "\""
_ -> colored' Vty.green "\"" <> colored' Vty.green (Text.unpack txt) <> colored' Vty.green "\""
(NullF) -> cursor isFocused $ colored' Vty.yellow "null"
(NumberF n) -> cursor isFocused $ case (isFocused, mode) of
(Focused, Edit buf) -> renderBuffer Vty.blue buf
_ -> colored' Vty.blue (show n)
(BoolF b) -> cursor isFocused $ colored' Vty.magenta (Text.unpack $ boolText_ # b)
(ArrayF xs) -> prettyArray isFocused xs
(ObjectF xs) -> prettyObj isFocused mode xs
where
colored' :: Vty.Color -> String -> PrettyJSON
colored' col txt =
P.annotate (Right $ if isFocused == Focused then reverseCol col else Vty.defAttr `Vty.withForeColor` col) (pretty txt)
-- | Attr in reverse-video
reverseCol :: Vty.Color -> Vty.Attr
reverseCol col = Vty.defAttr `Vty.withForeColor` col `Vty.withStyle` Vty.reverseVideo
-- | Map over all children of the current node, re-rendering after changes.
mapChildren ::
(Cofree ValueF FocusState -> Cofree ValueF FocusState) ->
Z.Zipper JIndex ValueF FocusState ->
Z.Zipper JIndex ValueF FocusState
mapChildren f = Z.branches_ . mapped %~ FF.cata alg
where
alg :: CofreeF.CofreeF ValueF FocusState (Cofree ValueF FocusState) -> Cofree ValueF FocusState
alg (cf CofreeF.:< vf) = rerenderCofree $ f (cf :< vf)
prettyWith :: Pretty a => Vty.Attr -> a -> PrettyJSON
prettyWith ann a = annotate (Right ann) $ pretty a
colored :: Pretty a => Vty.Color -> a -> PrettyJSON
colored col a = annotate (Right $ Vty.defAttr `Vty.withForeColor` col) $ pretty a
renderBuffer :: Vty.Color -> Buffer -> PrettyJSON
renderBuffer col buf =
let (prefix, suffix) = Text.splitAt (snd $ TZ.cursorPosition buf) (bufferText buf)
suffixImg = case Text.uncons suffix of
Nothing -> prettyWith (reverseCol col) ' '
Just (c, rest) -> prettyWith (reverseCol col) c <> colored col rest
in colored col prefix <> suffixImg
cursor :: Focused -> PrettyJSON -> PrettyJSON
cursor Focused = P.annotate (Left Render.Cursor)
cursor _ = id
prettyArray :: Focused -> Vector PrettyJSON -> PrettyJSON
prettyArray foc vs =
let inner :: [PrettyJSON] =
Vector.toList vs
& imap (\i v -> v <> commaKey i)
in cursor foc $ vsep $ [img "[", indent tabSize (vsep inner), img "]"]
where
img :: Text -> PrettyJSON
img t = case foc of
Focused -> prettyWith (reverseCol Vty.white) t
NotFocused -> pretty t
commaKey i
| i == Vector.length vs - 1 = mempty
| otherwise = ","
prettyObj :: Focused -> Mode -> HashMap Text PrettyJSON -> PrettyJSON
prettyObj focused mode vs =
let inner :: PrettyJSON
inner =
vsep
( HM.toList vs
& imap
( \i (k, v) ->
vsep [imgForKey k <> pretty @Text ": ", indent tabSize (v <> commaKey i)]
)
)
rendered = vsep [img "{", indent tabSize inner, img "}"]
in case mode of
Move -> cursor focused rendered
_ -> rendered
where
hmSize = HM.size vs
commaKey i
| i == hmSize - 1 = mempty
| otherwise = ","
imgForKey k = case focused of
NotFocused -> colored Vty.cyan (show k)
Focused -> case mode of
KeyMove focKey | focKey == k -> cursor Focused $ prettyWith (reverseCol Vty.cyan) (show focKey)
KeyEdit focKey buf | focKey == k -> cursor Focused $ colored Vty.cyan '"' <> renderBuffer Vty.cyan buf <> colored Vty.cyan '"'
_ -> colored Vty.cyan (show k)
img :: Text -> PrettyJSON
img t = case (focused, mode) of
(Focused, Move) -> prettyWith (reverseCol Vty.white) t
_ -> pretty t
-- Orphan instances
instance Eq1 ValueF where
liftEq f vf1 vf2 = case (vf1, vf2) of
(ObjectF l, ObjectF r) -> liftEq f l r
(ArrayF l, ArrayF r) -> liftEq f l r
(NullF, NullF) -> True
(StringF l, StringF r) -> l == r
(NumberF l, NumberF r) -> l == r
(BoolF l, BoolF r) -> l == r
_ -> False
instance Ord1 ValueF where
liftCompare f vf1 vf2 = case (vf1, vf2) of
(ObjectF l, ObjectF r) -> liftCompare f l r
(ArrayF l, ArrayF r) -> liftCompare f l r
(NullF, NullF) -> EQ
(StringF l, StringF r) -> compare l r
(NumberF l, NumberF r) -> compare l r
(BoolF l, BoolF r) -> compare l r
(NullF, _) -> LT
(_, NullF) -> GT
(BoolF _, _) -> LT
(_, BoolF _) -> GT
(NumberF _, _) -> LT
(_, NumberF _) -> GT
(StringF _, _) -> LT
(_, StringF _) -> GT
(ArrayF _, _) -> LT
(_, ArrayF _) -> GT
data JIndex
= Index Int
| Key Text
deriving (Show, Eq, Ord)
instance FunctorWithIndex JIndex ValueF
instance FoldableWithIndex JIndex ValueF
instance TraversableWithIndex JIndex ValueF where
itraverse f = \case
NullF -> pure NullF
StringF txt -> pure (StringF txt)
NumberF sci -> pure (NumberF sci)
BoolF b -> pure (BoolF b)
ObjectF hm -> ObjectF <$> itraverse (\k a -> f (Key k) a) hm
ArrayF arr -> ArrayF <$> itraverse (\k a -> f (Index k) a) arr
type instance Index (ValueF a) = JIndex
type instance IxValue (ValueF a) = a
instance Ixed (ValueF a) where
ix (Index i) f (ArrayF xs) = ArrayF <$> ix i f xs
ix (Key k) f (ObjectF xs) = ObjectF <$> ix k f xs
ix _ _ x = pure x
toCofree :: (Value -> Cofree ValueF FocusState)
toCofree t = FF.hylo alg FF.project $ t
where
defaultFs = FocusState NotFocused NotFolded mempty
mode = Move
alg :: ValueF (Cofree ValueF FocusState) -> Cofree ValueF FocusState
alg vf = defaultFs {rendered = renderSubtree defaultFs mode (rendered . extract <$> vf)} :< vf
helpImg :: Vty.Image
helpImg =
let helps =
[ ("h", "ascend"),
("l", "descend"),
("j", "next sibling"),
("k", "previous sibling"),
("J", "move down (in array)"),
("K", "move up (in array)"),
("i", "enter edit mode (string/number)"),
("<C-s>", "save file"),
("<SPACE>", "toggle boolean"),
("<ESC>", "exit edit mode"),
("<BS>", "delete key/element"),
("<ENTER>", "add new key/element (object/array)"),
("<TAB>", "toggle fold"),
("f", "unfold all children"),
("F", "fold all children"),
("s", "replace element with string"),
("b", "replace element with bool"),
("n", "replace element with number"),
("N", "replace element with null"),
("a", "replace element with array"),
("o", "replace element with object"),
("u", "undo last change (undo buffer keeps 100 states)"),
("<C-r>", "redo from undo states"),
("y", "copy current value into buffer (and clipboard)"),
("p", "paste value from buffer over current value"),
("x", "cut a value, equivalent to a copy -> delete"),
("q | ctrl-c", "quit without saving. Due to a bug, tap twice")
]
(keys, descs) =
unzip
( helps <&> \(key, desc) ->
( Vty.text' (Vty.defAttr `Vty.withForeColor` Vty.green) (key <> ": "),
Vty.text' Vty.defAttr desc
)
)
in (Vty.vertCat keys Vty.<|> Vty.vertCat descs)
-- | Recomputes the spine at the current position, then at every position from that point
-- upwards until the zipper is closed, returning the result.
foldSpine :: (Functor f, Z.Idx i f a) => (a -> f a -> a) -> Z.Zipper i f a -> a
foldSpine f z =
case Z.up z of
Nothing -> z ^. Z.focus_
Just parent ->
let next = f (parent ^. Z.focus_) (fmap Comonad.extract . Z.branches $ parent)
in foldSpine f (parent & Z.focus_ .~ next)
data UndoZipper a
= UndoZipper
(Seq a)
-- ^ undo states
(Seq a)
-- ^ redo states