summaryrefslogtreecommitdiff
path: root/interactive-edit-lens/src/Interact.hs
blob: 662052b0b67ea0a60ac5d7f69c9db0a0c9ac2e30 (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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
{-# 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 qualified Data.Text.IO 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 Brick.Widgets.FileBrowser
import Graphics.Vty hiding (showCursor)

import Config.Dyre

import System.IO.Unsafe
import Debug.Trace

import System.CPUTime
import Text.Printf

import Control.Exception (evaluate)
import Control.DeepSeq

interactiveEditLens :: forall c. NFData c => (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. NFData 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 &?~
    
    (&?~), actOn :: 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)
      , istActive = True
      , istLoadBrowser = Nothing
      }

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

    appDraw :: InteractState c -> [Widget InteractName]
    appDraw InteractState{..} = [ editors ]
      where
        editors = vBox
          [ case istLoadBrowser of
              Nothing -> hBox
                [ mbInvalid (withFocusRing istFocus renderEditor') (istLeft `WithName` LeftEditor)
                , vBorder
                , mbInvalid (withFocusRing istFocus renderEditor') (istRight `WithName` RightEditor)
                ]
              Just lBrowser -> renderFileBrowser True lBrowser
          , hCenter . str $ bool "Inactive" "" istActive
          ]
        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)
      | Nothing <- istLoadBrowser = case ev of
          EvKey  KEsc        []      -> halt st
          EvKey (KChar 'c')  [MCtrl] -> halt st
          EvKey (KChar '\t') []      -> continue . actOn st . runMaybeT $ do
            guard =<< use active
            focus %= focusNext
          EvKey  KBackTab    []      -> continue . actOn st . runMaybeT $ do
            guard =<< use active
            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         []      -> 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       []      -> 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
          EvKey (KChar 'p')  [MCtrl]
            | istActive -> do
                void . liftIO . evaluate . force . ($ st) $ (,,) <$> view left <*> view right <*> view complement
                continue $ st & active .~ False
            | otherwise -> do
                let st' = actOn st $ do
                      active .= True
                      doEdit mempty
                before <- liftIO getCPUTime
                void . liftIO . evaluate . force . ($ st') $ (,,) <$> view left <*> view right <*> view complement
                after <- liftIO getCPUTime
                suspendAndResume $ do
                  printf "Resume took %.12fs\n" (fromInteger (after - before) * 1e-12 :: Double)
                  return st'
          EvKey (KChar 'o')  [MCtrl] -> do
            lBrowser <- liftIO $ newFileBrowser selectNonDirectories LoadBrowser Nothing
            continue $ st & loadBrowser .~ Just lBrowser
          other                      -> suspendAndResume $ do
            traceIO $ "Unhandled event:\n\t" ++ show other
            return st
          -- where
          --   editorMovement f = continue $ st & focused . _Just . editContentsL %~ f
      | Just lBrowser <- istLoadBrowser = do
          lBrowser' <- handleFileBrowserEvent ev lBrowser
          case fileBrowserSelection lBrowser' of
            [] -> continue $ st &?~ loadBrowser .= Just lBrowser'
            (FileInfo{..} : _) -> do
              insEdit <- divInit . view charseq <$> liftIO (Text.readFile fileInfoFilePath)
              let st' = actOn st $ do
                    doEdit $ (insEdit :: StringEdits Natural Char) & stringEdits . sePos %~ fromIntegral
                    loadBrowser .= Nothing
              continue st'
              
    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`)
  bRes <- runMaybeT $ do
    guard =<< use active
    (,) <$> use (bL . _2 . _Wrapped . _1) <*> prop direction absoluteEdit'
  case uncurry apply =<< bRes 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