summaryrefslogtreecommitdiff
path: root/interactive-edit-lens/src/Interact.hs
blob: 3aab5c2eb7603e4d01a2cfc0aa5d44320feb0c8a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
{-# LANGUAGE ScopedTypeVariables
           , OverloadedStrings
  #-}

module Interact
  ( interactiveEditLens
  , module Interact.Types
  , module Config.Dyre
  ) where

import Prelude hiding (init)

import Interact.Types

import Data.Text (Text)
import qualified Data.Text as Text

import Data.Text.Zipper

import Data.Sequence (Seq)
import qualified Data.Sequence as Seq

import Control.Lens
import Numeric.Lens
import System.IO
import Control.Monad
import Control.Monad.RWS hiding (Last(..), (<>))
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Reader (runReaderT)

import Data.Bool (bool)
import Data.Tuple (swap)
import Data.Maybe (fromMaybe)
import Data.List (groupBy)
import Data.Function (on)
import Data.Char (isSpace)

import Data.Foldable (Foldable(toList))

import Brick hiding (on)
import Brick.Focus
import Brick.Widgets.Center
import Brick.Widgets.Border
import Graphics.Vty hiding (showCursor)

import Config.Dyre

import System.IO.Unsafe
import Debug.Trace

interactiveEditLens :: (Params (InteractConfig c) -> Params (InteractConfig c)) -> InteractConfig c -> IO ()
interactiveEditLens f = wrapMain . f $ defaultParams
  { projectName = "interact-edit-lens"
  , showError   = \s err -> s & compileError .~ Just err
  , realMain    = interactiveEditLens'
  }

interactiveEditLens' :: forall c. InteractConfig c -> IO ()
interactiveEditLens' cfg@InteractConfig{..}
  | Just err <- icfgCompileError
  = hPutStrLn stderr err
  | otherwise
  = void . defaultMain app $! initialState &?~ do
      let
        a :: Lens' (InteractState c) (Last Validity, Last (Seq Char, Int), StringEdits Natural Char)
        a = case icfgInitial of
          InitialRight _ -> right
          _other -> left
        b :: Lens' (InteractState c) (Last Validity, Last (Seq Char, Int), StringEdits Natural Char)
        b = case icfgInitial of
          InitialRight _ -> left
          _other -> right
        dir :: InteractDirection
        dir = case icfgInitial of
          InitialRight _ -> PropagateLeft
          _other -> PropagateRight
        aDom :: Seq Char
        (view charseq -> aDom) = case icfgInitial of
          InitialRight t -> t
          InitialLeft t -> t
          InitialEmpty -> ""
      doEdit $ divInit aDom & stringEdits . sePos %~ (fromIntegral :: Natural -> Integer)
      -- a .= (Last Valid, Last (aDom, 0))
      -- bEdit <- prop dir $ divInit aDom
      -- (b %=) . maybe id (<>) <=< runMaybeT $ do
      --   bDom <- use $ b . _2 . _Wrapped . _1
      --   bDom' <- MaybeT . return $ bDom `apply` bEdit
      --   return $ (Last Valid, Last (bDom', 0))
  where
    infix 1 &?~
    
    (&?~) :: a -> RWS (InteractConfig c) () a b -> a
    st &?~ act = (\(s, ()) -> s) $ execRWS act cfg st

    actOn = (&?~)

    initialState :: InteractState c
    initialState = InteractState
      { istComplement = ground icfgLens
      , istLeft  = (Last Valid, Last (init @(StringEdits Natural Char), 0), mempty)
      , istRight = (Last Valid, Last (init @(StringEdits Natural Char), 0), mempty)
      , istFocus = focusRing [LeftEditor, RightEditor] &
                     focusSetCurrent (case icfgInitial of InitialRight _ -> RightEditor; _other -> LeftEditor)
      }

    app :: InteractApp c
    app = App{..}

    appDraw :: InteractState c -> [Widget InteractName]
    appDraw InteractState{..} = [ editors ]
      where
        editors = hBox
          [ mbInvalid (withFocusRing istFocus renderEditor') (istLeft `WithName` LeftEditor)
          , vBorder
          , mbInvalid (withFocusRing istFocus renderEditor') (istRight `WithName` RightEditor)
          ]
        renderEditor' :: Bool -> (Seq Char, Int) `WithName` InteractName -> Widget InteractName
        renderEditor' foc ((content, cPos) `WithName` n)
          = txt (review charseq content)
          & bool id (showCursor n cPos') foc
          & visibleRegion cPos' (1, 1) 
          & viewport n Both
          where
            (cPrefix, _) = Seq.splitAt cPos content
            newls = Seq.findIndicesR (== '\n') cPrefix
            cPos' = case newls of
              (p:_)    -> Location (pred $ cPos - p, length newls)
              []       -> Location (cPos, 0)
        mbInvalid _ ((Last Invalid, _     , _) `WithName` _)
          = txt "Invalid"
          & border
          & center
        mbInvalid f ((Last Valid  , Last x, _) `WithName` n) = f $ x `WithName` n

    appHandleEvent :: InteractState c -> BrickEvent InteractName InteractEvent -> EventM InteractName (Next (InteractState c))
    appHandleEvent st@InteractState{..} (VtyEvent ev) = case ev of
      EvKey  KEsc        []      -> halt st
      EvKey (KChar 'c')  [MCtrl] -> halt st
      EvKey (KChar '\t') []      -> continue $ st & focus %~ focusNext
      EvKey  KBackTab    []      -> continue $ st & focus %~ focusPrev
      EvKey (KChar 'a')  [MCtrl] -> continue $ st &?~ doMove
        (moveSplit (== '\n') $ \(c, (l, p)) -> if any (== '\n') (c l) || Seq.null (c l) then (pred l, 0) else (l, 0))
      EvKey (KChar 'e')  [MCtrl] -> continue $ st &?~ doMove
        (moveSplit (== '\n') $ \(c, (l, p)) -> if any (== '\n') $ c l then (pred l, Seq.length . c $ pred l) else (l, Seq.length $ c l))
      EvKey  KLeft       [MCtrl] -> continue $ st &?~ doMove
        (moveSplit isSpace   $ \(c, (l, _)) -> if any isSpace (c l) || Seq.null (c l) then (pred l, 0) else (l - 2, 0))
      EvKey  KRight      [MCtrl] -> continue $ st &?~ doMove
        (moveSplit isSpace   $ \(c, (l, _)) -> if any isSpace $ c l then (succ l, 0) else (l + 2, 0))
      EvKey  KUp         [MCtrl] -> continue $ st &?~ doMove
        (moveSplit (== '\n') $ \(c, (l, p)) -> if any (== '\n') (c l) || Seq.null (c l) then (pred l, p) else (l - 2, p))
      EvKey  KDown       [MCtrl] -> continue $ st &?~ doMove
        (moveSplit (== '\n') $ \(c, (l, p)) -> if any (== '\n') (c l) then (succ l, p) else (l + 2, p))
      EvKey  KLeft       []      -> continue $ st &?~ doMove moveLeft
      EvKey  KRight      []      -> continue $ st &?~ doMove moveRight
      EvKey  KDel        []      -> continue $ st &?~ doEdit (delete 0)
      EvKey  KBS         []      -> continue . actOn st $ do
        focused' <- preuse $ focused . _2 . _Wrapped
        doEdit . delete $ -1
        unless (maybe False ((==) <$> view _2 <*> view (_1 . to Seq.length)) focused') $
          doMove moveLeft
      EvKey (KChar c)    []      -> continue . actOn st $ do
        doEdit $ insert 0 c
        doMove moveRight
      EvKey KEnter       []      -> continue . actOn st $ do
        doEdit $ insert 0 '\n'
        doMove moveRight
      other                      -> suspendAndResume $ do
        traceIO $ "Unhandled event:\n\t" ++ show other
        return st
      -- where
      --   editorMovement f = continue $ st & focused . _Just . editContentsL %~ f
    appHandleEvent st _ = continue st

    doMove = zoom $ focused . _2 . _Wrapped

    moveLeft, moveRight :: MonadState (Seq Char, Int) m => m ()
    moveLeft = modify $ \now@(_, nowP) -> if
      | nowP > 0  -> now & _2 %~ pred
      | otherwise -> now
    moveRight = modify $ \now@(contents, nowP) -> if
      | nowP < length contents -> now & _2 %~ succ
      | otherwise              -> now

    moveSplit :: MonadState (Seq Char, Int) m
              => (Char -> Bool) -- ^ Separator predicate
              -> (((Int -> Seq Char), (Int, Int)) -> (Int, Int)) -- ^ Move in split coordinates (e.g. @(line, charInLine)@) with access to the focused fragment
              -> m ()
    moveSplit splitPred relMove = modify $ \now@(toList -> contentsStr, nowP)
        -> let splitContents = groupBy ((==) `on` splitPred) contentsStr
               traceShow x y = flip seq y . unsafePerformIO . appendFile "interact.log" . (<> "\n\n") $ show x
               (before, mCurrent, after) = snd . (\x -> traceShow (nowP, x) x) $ foldl go (0, ([], Nothing, [])) splitContents
               go acc@(i, st) cGroup
                 | i <= nowP, nowP < i + length cGroup = (i + length cGroup, st & _2 .~ Just cGroup)
                 | i + length cGroup <= nowP = (i + length cGroup, st & _1 %~ (flip snoc cGroup))
                 | otherwise = (i + length cGroup, st & _3 %~ (flip snoc cGroup))
               relPos = (length before, nowP - sum (map length before))
               (newL, newS) = relMove (\i -> if 0 <= i && i < length splitContents then Seq.fromList $ splitContents !! i else Seq.empty, relPos)
               newPos
                 | null splitContents
                 , newL /= 0 || newS /= 0 = (0, 0)
                 | newL >= length splitContents = (pred $ length splitContents, length $ last splitContents)
                 | newL < 0 = (0, 0)
                 | newS < 0 = (newL, 0)
                 | newS > length (splitContents !! newL) = (newL, length $ splitContents !! newL)
                 | otherwise = (newL, newS)
            in now & _2 .~ sum (map length $ take (fst newPos) splitContents) + snd newPos

    appStartEvent :: InteractState c -> EventM InteractName (InteractState c)
    appStartEvent = return

    appAttrMap :: InteractState c -> AttrMap
    appAttrMap = const $ attrMap defAttr []

    appChooseCursor :: InteractState c -> [CursorLocation InteractName] -> Maybe (CursorLocation InteractName)
    appChooseCursor = focusRingCursor istFocus

prop :: forall st cfg m.
        ( MonadState st m
        , MonadReader cfg m
        , HasComplement st (Complement cfg)
        , HasEditLens cfg (StringEdits Natural Char) (StringEdits Natural Char)
        )
     => InteractDirection -> StringEdits Natural Char -> m (StringEdits Natural Char)
prop dir edits = do
  propD <- case dir of
    PropagateRight -> asks propR
    PropagateLeft  -> asks propL
  (c, res) <- propD . (, edits) <$> use complement
  unsafePerformIO . fmap return . appendFile "interact.log" . (<> "\n\n") $ show (edits, dir, res)
  res <$ assign complement c

doEdit :: forall m c.
          ( MonadState (InteractState c) m
          , MonadReader (InteractConfig c) m
          )
       => StringEdits Integer Char -> m ()
doEdit relativeEdit = void . runMaybeT $ do
  currentFocus <- MaybeT $ uses focus focusGetCurrent
  let direction
        | RightEditor <- currentFocus = PropagateLeft
        | otherwise                   = PropagateRight
      aL :: Lens' (InteractState c) (Last Validity, Last (Seq Char, Int), StringEdits Natural Char)
      aL | PropagateRight <- direction = left
         | PropagateLeft  <- direction = right
      bL :: Lens' (InteractState c) (Last Validity, Last (Seq Char, Int), StringEdits Natural Char)
      bL | PropagateRight <- direction = right
         | PropagateLeft  <- direction = left
      (aN, bN) = bool swap id (direction == PropagateRight) (LeftEditor, RightEditor)
  currentZipper <- use $ aL . _2 . _Wrapped
  let currentPos = currentZipper ^. _2
  absoluteEdit <- MaybeT . return $ do
    let minOffset = minimumOf (stringEdits . sePos) relativeEdit
    guard $ maybe True (\o -> 0 <= currentPos + fromIntegral o) minOffset
    return $ relativeEdit & stringEdits . sePos %~ (\n -> fromIntegral $ currentPos + fromIntegral n)
  newContent <- MaybeT . return $ view _1 currentZipper `apply` absoluteEdit
  let currentPos'
        | currentPos < 0 = 0
        | currentPos > length newContent = length newContent
        | otherwise = currentPos
  aL . _2 %= (<> Last (newContent, currentPos'))
  absoluteEdit' <- uses (aL . _3) (absoluteEdit `mappend`)
  bEdits <- prop direction absoluteEdit'
  bDom <- use $ bL . _2 . _Wrapped . _1
  case bDom `apply` bEdits of
    Nothing -> do
      bL . _1 %= (<> Last Invalid)
      aL . _3 .= absoluteEdit'
    Just bDom' -> do
      bL . _1 %= (<> Last Valid)
      bL . _2 . _Wrapped . _1 .= bDom'
      aL . _3 .= mempty