jet (empty) → 0.0.0.0
raw patch · 9 files changed
+1300/−0 lines, 9 filesdep +Hclipdep +aesondep +aeson-extrasetup-changed
Dependencies added: Hclip, aeson, aeson-extra, aeson-pretty, ansi-terminal, base, bytestring, comonad, containers, free, hashable, jet, lens, mtl, prettyprinter, prettyprinter-ansi-terminal, recursion-schemes, recursive-zipper, scientific, text, text-zipper, transformers, unix, unordered-containers, vector, vty
Files
- ChangeLog.md +3/−0
- LICENSE +30/−0
- README.md +59/−0
- Setup.hs +2/−0
- app/Main.hs +6/−0
- jet.cabal +185/−0
- src/Jet.hs +929/−0
- src/Jet/Render.hs +84/−0
- test/Spec.hs +2/−0
+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for structural-json++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2021++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * 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.++ * Neither the name of Author name here nor the names of other+ 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+OWNER 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,59 @@+# structural-json++<!-- toc GFM -->++* [Features](#features)+* [Keymaps](#keymaps)+* [Installation](#installation)+* [Usage](#usage)+* [Roadmap/Known bugs](#roadmapknown-bugs)++<!-- tocstop -->++A structural editor for JSON.++I.e. an editor which is aware of the *structure* of JSON and allows you to manipulate it directly.+The document is _always_ in a valid state.++https://user-images.githubusercontent.com/6439644/143655548-3c556ea8-7673-4439-8624-15b4b503001f.mov+++# Features++* [x] Structurally sound editing, never outputs invalid JSON.+* [x] Copy/Cut/Paste JSON subtrees+* [x] Subtree folding so you can focus on what's important.+* [x] Transpose values around each other in lists.+* [x] Undo/redo system, everyone makes mistakes+* [x] Save functionality+++# Keymaps++Press `?` to see the key map, which should feel familiar to vim users.++# Installation++```shell+cabal install jet+```++# Usage++```shell+# Open a file for editing. Use ctrl-s to save back to the file.+# The edited file is output to stdout even if unsaved.+jet myfile.json ++# Using jet in a pipeline for quick in-line edits.+cat myfile.json | jet > result.json+```++# Roadmap/Known bugs++- [ ] Figure out why vty needs two keystrokes to quit for some reason.+- [ ] Allow cut/paste of _keys_ of objects.+- [ ] Allow inserting when empty key already exists+- [ ] Add search+- [ ] Improved visibility around copy/paste with highlighting+- [ ] Increment/decrement commands for integers.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,6 @@+module Main where++import Jet++main :: IO ()+main = run
+ jet.cabal view
@@ -0,0 +1,185 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.4.+--+-- see: https://github.com/sol/hpack++name: jet+version: 0.0.0.0+description: Please see the README on GitHub at <https://github.com/ChrisPenner/jet#readme>+homepage: https://github.com/ChrisPenner/jet#readme+bug-reports: https://github.com/ChrisPenner/jet/issues+author: Chris Penner+maintainer: example@example.com+copyright: 2021 Chris Penner+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ ChangeLog.md++source-repository head+ type: git+ location: https://github.com/ChrisPenner/jet++library+ exposed-modules:+ Jet+ Jet.Render+ other-modules:+ Paths_jet+ hs-source-dirs:+ src+ default-extensions:+ FlexibleInstances+ FlexibleContexts+ ScopedTypeVariables+ LambdaCase+ ViewPatterns+ TypeApplications+ TypeOperators+ DeriveFunctor+ DeriveTraversable+ DeriveGeneric+ DerivingStrategies+ StandaloneDeriving+ TemplateHaskell+ RankNTypes+ GADTs+ MultiParamTypeClasses+ ghc-options: -Wall+ build-depends:+ Hclip+ , aeson+ , aeson-extra+ , aeson-pretty+ , ansi-terminal+ , base >=4.7 && <5+ , bytestring+ , comonad+ , containers+ , free+ , hashable+ , lens+ , mtl+ , prettyprinter+ , prettyprinter-ansi-terminal+ , recursion-schemes+ , recursive-zipper ==0.0.0.0+ , scientific+ , text+ , text-zipper+ , transformers+ , unix+ , unordered-containers+ , vector+ , vty+ default-language: Haskell2010++executable jet-exe+ main-is: Main.hs+ other-modules:+ Paths_jet+ hs-source-dirs:+ app+ default-extensions:+ FlexibleInstances+ FlexibleContexts+ ScopedTypeVariables+ LambdaCase+ ViewPatterns+ TypeApplications+ TypeOperators+ DeriveFunctor+ DeriveTraversable+ DeriveGeneric+ DerivingStrategies+ StandaloneDeriving+ TemplateHaskell+ RankNTypes+ GADTs+ MultiParamTypeClasses+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ Hclip+ , aeson+ , aeson-extra+ , aeson-pretty+ , ansi-terminal+ , base >=4.7 && <5+ , bytestring+ , comonad+ , containers+ , free+ , hashable+ , jet+ , lens+ , mtl+ , prettyprinter+ , prettyprinter-ansi-terminal+ , recursion-schemes+ , recursive-zipper ==0.0.0.0+ , scientific+ , text+ , text-zipper+ , transformers+ , unix+ , unordered-containers+ , vector+ , vty+ default-language: Haskell2010++test-suite jet-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Paths_jet+ hs-source-dirs:+ test+ default-extensions:+ FlexibleInstances+ FlexibleContexts+ ScopedTypeVariables+ LambdaCase+ ViewPatterns+ TypeApplications+ TypeOperators+ DeriveFunctor+ DeriveTraversable+ DeriveGeneric+ DerivingStrategies+ StandaloneDeriving+ TemplateHaskell+ RankNTypes+ GADTs+ MultiParamTypeClasses+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ Hclip+ , aeson+ , aeson-extra+ , aeson-pretty+ , ansi-terminal+ , base >=4.7 && <5+ , bytestring+ , comonad+ , containers+ , free+ , hashable+ , jet+ , lens+ , mtl+ , prettyprinter+ , prettyprinter-ansi-terminal+ , recursion-schemes+ , recursive-zipper ==0.0.0.0+ , scientific+ , text+ , text-zipper+ , transformers+ , unix+ , unordered-containers+ , vector+ , vty+ default-language: Haskell2010
+ src/Jet.hs view
@@ -0,0 +1,929 @@+{-# 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
+ src/Jet/Render.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE OverloadedStrings #-}++module Jet.Render where++import Control.Lens+import Control.Monad.State+import qualified Data.List as List+import Data.Maybe+import qualified Graphics.Vty as Vty+import Prettyprinter+import Prettyprinter.Render.Util.StackMachine (renderSimplyDecoratedA)++data Cursor = Cursor+ deriving (Show, Eq)++data Output = LineBreak | ImgChunk Vty.Image++renderScreen :: Int -> SimpleDocStream (Either Cursor Vty.Attr) -> [Vty.Image]+renderScreen winHeight doc = do+ let (outputs, (_, _, mcursor)) =+ doc+ & toLineStream+ & flip runState ([], 0, Nothing)++ let allLines =+ outputs & List.foldr collapse ([], [])+ -- clean up anything remaining in the buffer+ & ( \(buf, rest) ->+ if null buf+ then rest+ else Vty.horizCat buf : rest+ )++ let cropped = case mcursor of+ Nothing -> allLines+ Just cursorPos -> takeSurroundingCursor winHeight cursorPos allLines+ cropped+ where+ collapse out (buf, rest) =+ case out of+ LineBreak -> ([], Vty.horizCat buf : rest)+ ImgChunk img -> (img : buf, rest)++-- Take only enough lines to fill the screen, with the cursor centered don't print any more.+takeSurroundingCursor :: Int -> Int -> [a] -> [a]+takeSurroundingCursor height cursorPos xs+ | cursorPos - half > 0 =+ xs+ & drop (cursorPos - half)+ & take height+ | otherwise = take height xs+ where+ half = height `div` 2++toLineStream ::+ SimpleDocStream (Either Cursor Vty.Attr) ->+ State ([Vty.Attr], Int, Maybe Int) [Output]+toLineStream doc =+ renderSimplyDecoratedA+ renderText+ pushAnn+ popAnn+ doc+ where+ popAnn _ = do+ _1 %= drop 1+ -- Need to clear existing colors or they bleed to the right.+ pure [ImgChunk (Vty.text' Vty.defAttr "")]+ pushAnn = \case+ Left Cursor -> do+ cursorLine <- use _2+ _3 ?= cursorLine+ pure []+ Right ann -> do+ _1 %= (ann :)+ pure mempty+ -- prettyprinter always renders lines as a single text fragment+ renderText "\n" = do+ _2 += 1+ pure [LineBreak]+ -- prettyprinter never passes text with newlines here+ renderText txt = do+ attr <- uses _1 (fromMaybe Vty.defAttr . listToMaybe)+ pure [ImgChunk (Vty.text' attr txt)]
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"