diff options
Diffstat (limited to 'interactive-edit-lens/src/Interact.hs')
| -rw-r--r-- | interactive-edit-lens/src/Interact.hs | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/interactive-edit-lens/src/Interact.hs b/interactive-edit-lens/src/Interact.hs new file mode 100644 index 0000000..3aab5c2 --- /dev/null +++ b/interactive-edit-lens/src/Interact.hs | |||
| @@ -0,0 +1,271 @@ | |||
| 1 | {-# LANGUAGE ScopedTypeVariables | ||
| 2 | , OverloadedStrings | ||
| 3 | #-} | ||
| 4 | |||
| 5 | module Interact | ||
| 6 | ( interactiveEditLens | ||
| 7 | , module Interact.Types | ||
| 8 | , module Config.Dyre | ||
| 9 | ) where | ||
| 10 | |||
| 11 | import Prelude hiding (init) | ||
| 12 | |||
| 13 | import Interact.Types | ||
| 14 | |||
| 15 | import Data.Text (Text) | ||
| 16 | import qualified Data.Text as Text | ||
| 17 | |||
| 18 | import Data.Text.Zipper | ||
| 19 | |||
| 20 | import Data.Sequence (Seq) | ||
| 21 | import qualified Data.Sequence as Seq | ||
| 22 | |||
| 23 | import Control.Lens | ||
| 24 | import Numeric.Lens | ||
| 25 | import System.IO | ||
| 26 | import Control.Monad | ||
| 27 | import Control.Monad.RWS hiding (Last(..), (<>)) | ||
| 28 | import Control.Monad.Trans.Maybe | ||
| 29 | import Control.Monad.Trans.Reader (runReaderT) | ||
| 30 | |||
| 31 | import Data.Bool (bool) | ||
| 32 | import Data.Tuple (swap) | ||
| 33 | import Data.Maybe (fromMaybe) | ||
| 34 | import Data.List (groupBy) | ||
| 35 | import Data.Function (on) | ||
| 36 | import Data.Char (isSpace) | ||
| 37 | |||
| 38 | import Data.Foldable (Foldable(toList)) | ||
| 39 | |||
| 40 | import Brick hiding (on) | ||
| 41 | import Brick.Focus | ||
| 42 | import Brick.Widgets.Center | ||
| 43 | import Brick.Widgets.Border | ||
| 44 | import Graphics.Vty hiding (showCursor) | ||
| 45 | |||
| 46 | import Config.Dyre | ||
| 47 | |||
| 48 | import System.IO.Unsafe | ||
| 49 | import Debug.Trace | ||
| 50 | |||
| 51 | interactiveEditLens :: (Params (InteractConfig c) -> Params (InteractConfig c)) -> InteractConfig c -> IO () | ||
| 52 | interactiveEditLens f = wrapMain . f $ defaultParams | ||
| 53 | { projectName = "interact-edit-lens" | ||
| 54 | , showError = \s err -> s & compileError .~ Just err | ||
| 55 | , realMain = interactiveEditLens' | ||
| 56 | } | ||
| 57 | |||
| 58 | interactiveEditLens' :: forall c. InteractConfig c -> IO () | ||
| 59 | interactiveEditLens' cfg@InteractConfig{..} | ||
| 60 | | Just err <- icfgCompileError | ||
| 61 | = hPutStrLn stderr err | ||
| 62 | | otherwise | ||
| 63 | = void . defaultMain app $! initialState &?~ do | ||
| 64 | let | ||
| 65 | a :: Lens' (InteractState c) (Last Validity, Last (Seq Char, Int), StringEdits Natural Char) | ||
| 66 | a = case icfgInitial of | ||
| 67 | InitialRight _ -> right | ||
| 68 | _other -> left | ||
| 69 | b :: Lens' (InteractState c) (Last Validity, Last (Seq Char, Int), StringEdits Natural Char) | ||
| 70 | b = case icfgInitial of | ||
| 71 | InitialRight _ -> left | ||
| 72 | _other -> right | ||
| 73 | dir :: InteractDirection | ||
| 74 | dir = case icfgInitial of | ||
| 75 | InitialRight _ -> PropagateLeft | ||
| 76 | _other -> PropagateRight | ||
| 77 | aDom :: Seq Char | ||
| 78 | (view charseq -> aDom) = case icfgInitial of | ||
| 79 | InitialRight t -> t | ||
| 80 | InitialLeft t -> t | ||
| 81 | InitialEmpty -> "" | ||
| 82 | doEdit $ divInit aDom & stringEdits . sePos %~ (fromIntegral :: Natural -> Integer) | ||
| 83 | -- a .= (Last Valid, Last (aDom, 0)) | ||
| 84 | -- bEdit <- prop dir $ divInit aDom | ||
| 85 | -- (b %=) . maybe id (<>) <=< runMaybeT $ do | ||
| 86 | -- bDom <- use $ b . _2 . _Wrapped . _1 | ||
| 87 | -- bDom' <- MaybeT . return $ bDom `apply` bEdit | ||
| 88 | -- return $ (Last Valid, Last (bDom', 0)) | ||
| 89 | where | ||
| 90 | infix 1 &?~ | ||
| 91 | |||
| 92 | (&?~) :: a -> RWS (InteractConfig c) () a b -> a | ||
| 93 | st &?~ act = (\(s, ()) -> s) $ execRWS act cfg st | ||
| 94 | |||
| 95 | actOn = (&?~) | ||
| 96 | |||
| 97 | initialState :: InteractState c | ||
| 98 | initialState = InteractState | ||
| 99 | { istComplement = ground icfgLens | ||
| 100 | , istLeft = (Last Valid, Last (init @(StringEdits Natural Char), 0), mempty) | ||
| 101 | , istRight = (Last Valid, Last (init @(StringEdits Natural Char), 0), mempty) | ||
| 102 | , istFocus = focusRing [LeftEditor, RightEditor] & | ||
| 103 | focusSetCurrent (case icfgInitial of InitialRight _ -> RightEditor; _other -> LeftEditor) | ||
| 104 | } | ||
| 105 | |||
| 106 | app :: InteractApp c | ||
| 107 | app = App{..} | ||
| 108 | |||
| 109 | appDraw :: InteractState c -> [Widget InteractName] | ||
| 110 | appDraw InteractState{..} = [ editors ] | ||
| 111 | where | ||
| 112 | editors = hBox | ||
| 113 | [ mbInvalid (withFocusRing istFocus renderEditor') (istLeft `WithName` LeftEditor) | ||
| 114 | , vBorder | ||
| 115 | , mbInvalid (withFocusRing istFocus renderEditor') (istRight `WithName` RightEditor) | ||
| 116 | ] | ||
| 117 | renderEditor' :: Bool -> (Seq Char, Int) `WithName` InteractName -> Widget InteractName | ||
| 118 | renderEditor' foc ((content, cPos) `WithName` n) | ||
| 119 | = txt (review charseq content) | ||
| 120 | & bool id (showCursor n cPos') foc | ||
| 121 | & visibleRegion cPos' (1, 1) | ||
| 122 | & viewport n Both | ||
| 123 | where | ||
| 124 | (cPrefix, _) = Seq.splitAt cPos content | ||
| 125 | newls = Seq.findIndicesR (== '\n') cPrefix | ||
| 126 | cPos' = case newls of | ||
| 127 | (p:_) -> Location (pred $ cPos - p, length newls) | ||
| 128 | [] -> Location (cPos, 0) | ||
| 129 | mbInvalid _ ((Last Invalid, _ , _) `WithName` _) | ||
| 130 | = txt "Invalid" | ||
| 131 | & border | ||
| 132 | & center | ||
| 133 | mbInvalid f ((Last Valid , Last x, _) `WithName` n) = f $ x `WithName` n | ||
| 134 | |||
| 135 | appHandleEvent :: InteractState c -> BrickEvent InteractName InteractEvent -> EventM InteractName (Next (InteractState c)) | ||
| 136 | appHandleEvent st@InteractState{..} (VtyEvent ev) = case ev of | ||
| 137 | EvKey KEsc [] -> halt st | ||
| 138 | EvKey (KChar 'c') [MCtrl] -> halt st | ||
| 139 | EvKey (KChar '\t') [] -> continue $ st & focus %~ focusNext | ||
| 140 | EvKey KBackTab [] -> continue $ st & focus %~ focusPrev | ||
| 141 | EvKey (KChar 'a') [MCtrl] -> continue $ st &?~ doMove | ||
| 142 | (moveSplit (== '\n') $ \(c, (l, p)) -> if any (== '\n') (c l) || Seq.null (c l) then (pred l, 0) else (l, 0)) | ||
| 143 | EvKey (KChar 'e') [MCtrl] -> continue $ st &?~ doMove | ||
| 144 | (moveSplit (== '\n') $ \(c, (l, p)) -> if any (== '\n') $ c l then (pred l, Seq.length . c $ pred l) else (l, Seq.length $ c l)) | ||
| 145 | EvKey KLeft [MCtrl] -> continue $ st &?~ doMove | ||
| 146 | (moveSplit isSpace $ \(c, (l, _)) -> if any isSpace (c l) || Seq.null (c l) then (pred l, 0) else (l - 2, 0)) | ||
| 147 | EvKey KRight [MCtrl] -> continue $ st &?~ doMove | ||
| 148 | (moveSplit isSpace $ \(c, (l, _)) -> if any isSpace $ c l then (succ l, 0) else (l + 2, 0)) | ||
| 149 | EvKey KUp [MCtrl] -> continue $ st &?~ doMove | ||
| 150 | (moveSplit (== '\n') $ \(c, (l, p)) -> if any (== '\n') (c l) || Seq.null (c l) then (pred l, p) else (l - 2, p)) | ||
| 151 | EvKey KDown [MCtrl] -> continue $ st &?~ doMove | ||
| 152 | (moveSplit (== '\n') $ \(c, (l, p)) -> if any (== '\n') (c l) then (succ l, p) else (l + 2, p)) | ||
| 153 | EvKey KLeft [] -> continue $ st &?~ doMove moveLeft | ||
| 154 | EvKey KRight [] -> continue $ st &?~ doMove moveRight | ||
| 155 | EvKey KDel [] -> continue $ st &?~ doEdit (delete 0) | ||
| 156 | EvKey KBS [] -> continue . actOn st $ do | ||
| 157 | focused' <- preuse $ focused . _2 . _Wrapped | ||
| 158 | doEdit . delete $ -1 | ||
| 159 | unless (maybe False ((==) <$> view _2 <*> view (_1 . to Seq.length)) focused') $ | ||
| 160 | doMove moveLeft | ||
| 161 | EvKey (KChar c) [] -> continue . actOn st $ do | ||
| 162 | doEdit $ insert 0 c | ||
| 163 | doMove moveRight | ||
| 164 | EvKey KEnter [] -> continue . actOn st $ do | ||
| 165 | doEdit $ insert 0 '\n' | ||
| 166 | doMove moveRight | ||
| 167 | other -> suspendAndResume $ do | ||
| 168 | traceIO $ "Unhandled event:\n\t" ++ show other | ||
| 169 | return st | ||
| 170 | -- where | ||
| 171 | -- editorMovement f = continue $ st & focused . _Just . editContentsL %~ f | ||
| 172 | appHandleEvent st _ = continue st | ||
| 173 | |||
| 174 | doMove = zoom $ focused . _2 . _Wrapped | ||
| 175 | |||
| 176 | moveLeft, moveRight :: MonadState (Seq Char, Int) m => m () | ||
| 177 | moveLeft = modify $ \now@(_, nowP) -> if | ||
| 178 | | nowP > 0 -> now & _2 %~ pred | ||
| 179 | | otherwise -> now | ||
| 180 | moveRight = modify $ \now@(contents, nowP) -> if | ||
| 181 | | nowP < length contents -> now & _2 %~ succ | ||
| 182 | | otherwise -> now | ||
| 183 | |||
| 184 | moveSplit :: MonadState (Seq Char, Int) m | ||
| 185 | => (Char -> Bool) -- ^ Separator predicate | ||
| 186 | -> (((Int -> Seq Char), (Int, Int)) -> (Int, Int)) -- ^ Move in split coordinates (e.g. @(line, charInLine)@) with access to the focused fragment | ||
| 187 | -> m () | ||
| 188 | moveSplit splitPred relMove = modify $ \now@(toList -> contentsStr, nowP) | ||
| 189 | -> let splitContents = groupBy ((==) `on` splitPred) contentsStr | ||
| 190 | traceShow x y = flip seq y . unsafePerformIO . appendFile "interact.log" . (<> "\n\n") $ show x | ||
| 191 | (before, mCurrent, after) = snd . (\x -> traceShow (nowP, x) x) $ foldl go (0, ([], Nothing, [])) splitContents | ||
| 192 | go acc@(i, st) cGroup | ||
| 193 | | i <= nowP, nowP < i + length cGroup = (i + length cGroup, st & _2 .~ Just cGroup) | ||
| 194 | | i + length cGroup <= nowP = (i + length cGroup, st & _1 %~ (flip snoc cGroup)) | ||
| 195 | | otherwise = (i + length cGroup, st & _3 %~ (flip snoc cGroup)) | ||
| 196 | relPos = (length before, nowP - sum (map length before)) | ||
| 197 | (newL, newS) = relMove (\i -> if 0 <= i && i < length splitContents then Seq.fromList $ splitContents !! i else Seq.empty, relPos) | ||
| 198 | newPos | ||
| 199 | | null splitContents | ||
| 200 | , newL /= 0 || newS /= 0 = (0, 0) | ||
| 201 | | newL >= length splitContents = (pred $ length splitContents, length $ last splitContents) | ||
| 202 | | newL < 0 = (0, 0) | ||
| 203 | | newS < 0 = (newL, 0) | ||
| 204 | | newS > length (splitContents !! newL) = (newL, length $ splitContents !! newL) | ||
| 205 | | otherwise = (newL, newS) | ||
| 206 | in now & _2 .~ sum (map length $ take (fst newPos) splitContents) + snd newPos | ||
| 207 | |||
| 208 | appStartEvent :: InteractState c -> EventM InteractName (InteractState c) | ||
| 209 | appStartEvent = return | ||
| 210 | |||
| 211 | appAttrMap :: InteractState c -> AttrMap | ||
| 212 | appAttrMap = const $ attrMap defAttr [] | ||
| 213 | |||
| 214 | appChooseCursor :: InteractState c -> [CursorLocation InteractName] -> Maybe (CursorLocation InteractName) | ||
| 215 | appChooseCursor = focusRingCursor istFocus | ||
| 216 | |||
| 217 | prop :: forall st cfg m. | ||
| 218 | ( MonadState st m | ||
| 219 | , MonadReader cfg m | ||
| 220 | , HasComplement st (Complement cfg) | ||
| 221 | , HasEditLens cfg (StringEdits Natural Char) (StringEdits Natural Char) | ||
| 222 | ) | ||
| 223 | => InteractDirection -> StringEdits Natural Char -> m (StringEdits Natural Char) | ||
| 224 | prop dir edits = do | ||
| 225 | propD <- case dir of | ||
| 226 | PropagateRight -> asks propR | ||
| 227 | PropagateLeft -> asks propL | ||
| 228 | (c, res) <- propD . (, edits) <$> use complement | ||
| 229 | unsafePerformIO . fmap return . appendFile "interact.log" . (<> "\n\n") $ show (edits, dir, res) | ||
| 230 | res <$ assign complement c | ||
| 231 | |||
| 232 | doEdit :: forall m c. | ||
| 233 | ( MonadState (InteractState c) m | ||
| 234 | , MonadReader (InteractConfig c) m | ||
| 235 | ) | ||
| 236 | => StringEdits Integer Char -> m () | ||
| 237 | doEdit relativeEdit = void . runMaybeT $ do | ||
| 238 | currentFocus <- MaybeT $ uses focus focusGetCurrent | ||
| 239 | let direction | ||
| 240 | | RightEditor <- currentFocus = PropagateLeft | ||
| 241 | | otherwise = PropagateRight | ||
| 242 | aL :: Lens' (InteractState c) (Last Validity, Last (Seq Char, Int), StringEdits Natural Char) | ||
| 243 | aL | PropagateRight <- direction = left | ||
| 244 | | PropagateLeft <- direction = right | ||
| 245 | bL :: Lens' (InteractState c) (Last Validity, Last (Seq Char, Int), StringEdits Natural Char) | ||
| 246 | bL | PropagateRight <- direction = right | ||
| 247 | | PropagateLeft <- direction = left | ||
| 248 | (aN, bN) = bool swap id (direction == PropagateRight) (LeftEditor, RightEditor) | ||
| 249 | currentZipper <- use $ aL . _2 . _Wrapped | ||
| 250 | let currentPos = currentZipper ^. _2 | ||
| 251 | absoluteEdit <- MaybeT . return $ do | ||
| 252 | let minOffset = minimumOf (stringEdits . sePos) relativeEdit | ||
| 253 | guard $ maybe True (\o -> 0 <= currentPos + fromIntegral o) minOffset | ||
| 254 | return $ relativeEdit & stringEdits . sePos %~ (\n -> fromIntegral $ currentPos + fromIntegral n) | ||
| 255 | newContent <- MaybeT . return $ view _1 currentZipper `apply` absoluteEdit | ||
| 256 | let currentPos' | ||
| 257 | | currentPos < 0 = 0 | ||
| 258 | | currentPos > length newContent = length newContent | ||
| 259 | | otherwise = currentPos | ||
| 260 | aL . _2 %= (<> Last (newContent, currentPos')) | ||
| 261 | absoluteEdit' <- uses (aL . _3) (absoluteEdit `mappend`) | ||
| 262 | bEdits <- prop direction absoluteEdit' | ||
| 263 | bDom <- use $ bL . _2 . _Wrapped . _1 | ||
| 264 | case bDom `apply` bEdits of | ||
| 265 | Nothing -> do | ||
| 266 | bL . _1 %= (<> Last Invalid) | ||
| 267 | aL . _3 .= absoluteEdit' | ||
| 268 | Just bDom' -> do | ||
| 269 | bL . _1 %= (<> Last Valid) | ||
| 270 | bL . _2 . _Wrapped . _1 .= bDom' | ||
| 271 | aL . _3 .= mempty | ||
