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 | ||