@@ -1,7 +1,7 @@
1{-# LANGUAGE FlexibleContexts #-}
1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE LambdaCase #-}
2{-# LANGUAGE LambdaCase #-}
3{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE OverloadedStrings #-}
4−module Reflex.Vty.Widget.Text where
4+module Data.Text.Zipper where
6import Data.Char
6import Data.Char
7import Data.Map (Map)
7import Data.Map (Map)
@@ -11,10 +11,13 @@ import Control.Monad.State
12import Data.Text (Text)
12import Data.Text (Text)
13import qualified Data.Text as T
13import qualified Data.Text as T
14−import qualified Graphics.Vty as V
16−-- | A zipper of the logical text input contents. The lines before the line containing the cursor
15+-- | A zipper of the logical text input contents (the "document"). The lines
16+-- before the line containing the cursor are stored in reverse order.
17−-- are stored in reverse order. The cursor is logically _between_ the "before" and "after" text.
17+-- The cursor is logically between the "before" and "after" text.
18+-- A "logical" line of input is a line of input up until a user-entered newline
19+-- character (as compared to a "display" line, which is wrapped to fit within
20+-- a given viewport width).
18data TextZipper = TextZipper
21data TextZipper = TextZipper
19 { _textZipper_linesBefore :: [Text] -- reversed
22 { _textZipper_linesBefore :: [Text] -- reversed
20 , _textZipper_before :: Text
23 , _textZipper_before :: Text
@@ -23,6 +26,7 @@ data TextZipper = TextZipper
24 deriving (Show)
27 deriving (Show)
29+-- | Move the cursor left one character, if possible
26left :: TextZipper -> TextZipper
30left :: TextZipper -> TextZipper
27left z@(TextZipper lb b a la) = case T.unsnoc b of
31left z@(TextZipper lb b a la) = case T.unsnoc b of
28 Nothing -> case lb of
32 Nothing -> case lb of
@@ -30,6 +34,7 @@ left z@(TextZipper lb b a la) = case T.unsnoc b of
30 (l:ls) -> TextZipper ls l "" (a : la)
34 (l:ls) -> TextZipper ls l "" (a : la)
31 Just (b', c) -> TextZipper lb b' (T.cons c a) la
35 Just (b', c) -> TextZipper lb b' (T.cons c a) la
37+-- | Move the cursor right one character, if possible
33right :: TextZipper -> TextZipper
38right :: TextZipper -> TextZipper
34right z@(TextZipper lb b a la) = case T.uncons a of
39right z@(TextZipper lb b a la) = case T.uncons a of
35 Nothing -> case la of
40 Nothing -> case la of
@@ -37,6 +42,7 @@ right z@(TextZipper lb b a la) = case T.uncons a of
37 (l:ls) -> TextZipper (b : lb) "" l ls
42 (l:ls) -> TextZipper (b : lb) "" l ls
38 Just (c, a') -> TextZipper lb (T.snoc b c) a' la
43 Just (c, a') -> TextZipper lb (T.snoc b c) a' la
45+-- | Move the cursor up one logical line, if possible
40up :: TextZipper -> TextZipper
46up :: TextZipper -> TextZipper
41up z@(TextZipper lb b a la) = case lb of
47up z@(TextZipper lb b a la) = case lb of
@@ -44,6 +50,7 @@ up z@(TextZipper lb b a la) = case lb of
44 let (b', a') = T.splitAt (T.length b) l
50 let (b', a') = T.splitAt (T.length b) l
45 in TextZipper ls b' a' ((b <> a) : la)
51 in TextZipper ls b' a' ((b <> a) : la)
53+-- | Move the cursor down one logical line, if possible
47down :: TextZipper -> TextZipper
54down :: TextZipper -> TextZipper
48down z@(TextZipper lb b a la) = case la of
55down z@(TextZipper lb b a la) = case la of
@@ -51,17 +58,25 @@ down z@(TextZipper lb b a la) = case la of
51 let (b', a') = T.splitAt (T.length b) l
58 let (b', a') = T.splitAt (T.length b) l
52 in TextZipper ((b <> a) : lb) b' a' ls
59 in TextZipper ((b <> a) : lb) b' a' ls
61+-- | Move the cursor to the beginning of the current logical line
54home :: TextZipper -> TextZipper
62home :: TextZipper -> TextZipper
55home (TextZipper lb b a la) = TextZipper lb "" (b <> a) la
63home (TextZipper lb b a la) = TextZipper lb "" (b <> a) la
65+-- | Move the cursor to the end of the current logical line
57end :: TextZipper -> TextZipper
66end :: TextZipper -> TextZipper
58end (TextZipper lb b a la) = TextZipper lb (b <> a) "" la
67end (TextZipper lb b a la) = TextZipper lb (b <> a) "" la
69+-- | Move the cursor to the top of the document
60top :: TextZipper -> TextZipper
70top :: TextZipper -> TextZipper
61top (TextZipper lb b a la) = case reverse lb of
71top (TextZipper lb b a la) = case reverse lb of
62 [] -> TextZipper [] "" (b <> a) la
72 [] -> TextZipper [] "" (b <> a) la
63 (start:rest) -> TextZipper [] "" start (rest <> la)
73 (start:rest) -> TextZipper [] "" start (rest <> la)
75+-- | Insert a character at the current cursor position
76+insertChar :: Char -> TextZipper -> TextZipper
77+insertChar i = insert (T.singleton i)
79+-- | Insert text at the current cursor position
65insert :: Text -> TextZipper -> TextZipper
80insert :: Text -> TextZipper -> TextZipper
66insert i z@(TextZipper lb b a la) = case T.split (=='\n') i of
81insert i z@(TextZipper lb b a la) = case T.split (=='\n') i of
@@ -69,6 +84,7 @@ insert i z@(TextZipper lb b a la) = case T.split (=='\n') i of
69 [] -> TextZipper lb (b <> start) a la
84 [] -> TextZipper lb (b <> start) a la
70 (l:ls) -> TextZipper (ls <> [b <> start] <> lb) l a la
85 (l:ls) -> TextZipper (ls <> [b <> start] <> lb) l a la
87+-- | Delete the character to the left of the cursor
72deleteLeft :: TextZipper-> TextZipper
88deleteLeft :: TextZipper-> TextZipper
73deleteLeft z@(TextZipper lb b a la) = case T.unsnoc b of
89deleteLeft z@(TextZipper lb b a la) = case T.unsnoc b of
74 Nothing -> case lb of
90 Nothing -> case lb of
@@ -76,6 +92,7 @@ deleteLeft z@(TextZipper lb b a la) = case T.unsnoc b of
76 (l:ls) -> TextZipper ls l a la
92 (l:ls) -> TextZipper ls l a la
77 Just (b', _) -> TextZipper lb b' a la
93 Just (b', _) -> TextZipper lb b' a la
95+-- | Delete the character under/to the right of the cursor
79deleteRight :: TextZipper -> TextZipper
96deleteRight :: TextZipper -> TextZipper
80deleteRight z@(TextZipper lb b a la) = case T.uncons a of
97deleteRight z@(TextZipper lb b a la) = case T.uncons a of
81 Nothing -> case la of
98 Nothing -> case la of
@@ -83,6 +100,9 @@ deleteRight z@(TextZipper lb b a la) = case T.uncons a of
83 (l:ls) -> TextZipper lb b l ls
100 (l:ls) -> TextZipper lb b l ls
84 Just (_, a') -> TextZipper lb b a' la
101 Just (_, a') -> TextZipper lb b a' la
103+-- | Delete a word to the left of the cursor. Deletes all whitespace until it
104+-- finds a non-whitespace character, and then deletes contiguous non-whitespace
86deleteLeftWord :: TextZipper -> TextZipper
106deleteLeftWord :: TextZipper -> TextZipper
87deleteLeftWord (TextZipper lb b a la) =
107deleteLeftWord (TextZipper lb b a la) =
88 let b' = T.dropWhileEnd isSpace b
108 let b' = T.dropWhileEnd isSpace b
@@ -92,29 +112,30 @@ deleteLeftWord (TextZipper lb b a la) =
92 (l:ls) -> deleteLeftWord $ TextZipper ls l a la
112 (l:ls) -> deleteLeftWord $ TextZipper ls l a la
93 else TextZipper lb (T.dropWhileEnd (not . isSpace) b') a la
113 else TextZipper lb (T.dropWhileEnd (not . isSpace) b') a la
115+-- | The plain text contents of the zipper
95value :: TextZipper -> Text
116value :: TextZipper -> Text
96value (TextZipper lb b a la) = T.intercalate "\n" $ mconcat [ reverse lb
117value (TextZipper lb b a la) = T.intercalate "\n" $ mconcat [ reverse lb
97 , [b <> a]
118 , [b <> a]
122+-- | The empty zipper
101empty :: TextZipper
123empty :: TextZipper
102empty = TextZipper [] "" "" []
124empty = TextZipper [] "" "" []
126+-- | Constructs a zipper with the given contents. The cursor is placed after
104fromText :: Text -> TextZipper
128fromText :: Text -> TextZipper
105fromText = flip insert empty
129fromText = flip insert empty
107−-- | A span of text that makes up part of a display line
131+-- | A span of text tagged with some metadata that makes up part of a display
108−data Span = Span V.Attr Text
133+data Span tag = Span tag Text
109 deriving (Show)
134 deriving (Show)
111−-- | Default attributes for the text cursor
112−cursorAttributes :: V.Attr
113−cursorAttributes = V.withStyle V.defAttr V.reverseVideo
115-- | Information about the documents as it is displayed (i.e., post-wrapping)
136-- | Information about the documents as it is displayed (i.e., post-wrapping)
116−data DisplayLines = DisplayLines
137+data DisplayLines tag = DisplayLines
117− { _displayLines_spans :: [[Span]]
138+ { _displayLines_spans :: [[Span tag]]
118 , _displayLines_offsetMap :: Map Int Int
139 , _displayLines_offsetMap :: Map Int Int
119 , _displayLines_cursorY :: Int
140 , _displayLines_cursorY :: Int
@@ -127,9 +148,11 @@ data DisplayLines = DisplayLines
128displayLines
149displayLines
129 :: Int -- ^ Width, used for wrapping
150 :: Int -- ^ Width, used for wrapping
151+ -> tag -- ^ Metadata for normal characters
152+ -> tag -- ^ Metadata for the cursor
130 -> TextZipper -- ^ The text input contents and cursor state
153 -> TextZipper -- ^ The text input contents and cursor state
131− -> DisplayLines
154+ -> DisplayLines tag
132−displayLines width (TextZipper lb b a la) =
155+displayLines width tag cursorTag (TextZipper lb b a la) =
133 let linesBefore :: [[Text]]
156 let linesBefore :: [[Text]]
134 linesBefore = map (wrapWithOffset width 0) $ reverse lb
157 linesBefore = map (wrapWithOffset width 0) $ reverse lb
135 linesAfter :: [[Text]]
158 linesAfter :: [[Text]]
@@ -139,25 +162,19 @@ displayLines width (TextZipper lb b a la) =
139 , [wrapWithOffset width 0 $ b <> a]
162 , [wrapWithOffset width 0 $ b <> a]
140 , linesAfter
163 , linesAfter
142− spansBefore = map ((:[]) . Span V.defAttr) $ concat linesBefore
165+ spansBefore = map ((:[]) . Span tag) $ concat linesBefore
143− spansAfter = map ((:[]) . Span V.defAttr) $ concat linesAfter
166+ spansAfter = map ((:[]) . Span tag) $ concat linesAfter
144 (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
167 (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
145− initLast $ map ((:[]) . Span V.defAttr) (wrapWithOffset width 0 b)
168+ initLast $ map ((:[]) . Span tag) (wrapWithOffset width 0 b)
146 curLineOffset = spansLength spansCurLineBefore
169 curLineOffset = spansLength spansCurLineBefore
147 cursorAfterEOL = curLineOffset == width
170 cursorAfterEOL = curLineOffset == width
148− -- (cursor, spansCurLineAfter, spansCurrentAfter) = case T.uncons a of
149− -- Nothing -> (Span cursorAttributes " ", [], [])
150− -- Just (c, rest) ->
151− -- ( Span cursorAttributes (T.singleton c)
154 (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
171 (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
155 headTail $ case T.uncons a of
172 headTail $ case T.uncons a of
156− Nothing -> [[Span cursorAttributes " "]]
173+ Nothing -> [[Span cursorTag " "]]
157 Just (c, rest) ->
174 Just (c, rest) ->
158 let o = if cursorAfterEOL then 1 else curLineOffset + 1
175 let o = if cursorAfterEOL then 1 else curLineOffset + 1
159− cursor = Span cursorAttributes (T.singleton c)
176+ cursor = Span cursorTag (T.singleton c)
160− in case map ((:[]) . Span V.defAttr) (wrapWithOffset width o rest) of
177+ in case map ((:[]) . Span tag) (wrapWithOffset width o rest) of
161 [] -> [[cursor]]
178 [] -> [[cursor]]
162 (l:ls) -> (cursor : l) : ls
179 (l:ls) -> (cursor : l) : ls
163 in DisplayLines
180 in DisplayLines
@@ -178,7 +195,7 @@ displayLines width (TextZipper lb b a la) =
181− spansLength :: [Span] -> Int
198+ spansLength :: [Span tag] -> Int
182 spansLength = sum . map (\(Span _ t) -> T.length t)
199 spansLength = sum . map (\(Span _ t) -> T.length t)
183 initLast :: [a] -> Maybe ([a], a)
200 initLast :: [a] -> Maybe ([a], a)
184 initLast = \case
201 initLast = \case
@@ -191,6 +208,9 @@ displayLines width (TextZipper lb b a la) =
191 [] -> Nothing
208 [] -> Nothing
192 x:xs -> Just (x, xs)
209 x:xs -> Just (x, xs)
211+-- | Wraps a logical line of text to fit within the given width. The first
212+-- wrapped line is offset by the number of columns provided. Subsequent wrapped
194wrapWithOffset :: Int -> Int -> Text -> [Text]
214wrapWithOffset :: Int -> Int -> Text -> [Text]
195wrapWithOffset maxWidth _ _ | maxWidth <= 0 = []
215wrapWithOffset maxWidth _ _ | maxWidth <= 0 = []
196wrapWithOffset maxWidth n xs =
216wrapWithOffset maxWidth n xs =
@@ -232,34 +252,3 @@ offsetMap ts = evalState (offsetMap' ts) (0, 0)
232 (dl, o) <- get
252 (dl, o) <- get
233 put (dl, o + 1)
253 put (dl, o + 1)
234 return $ Map.insert dl (o + 1) $ Map.unions maps
254 return $ Map.insert dl (o + 1) $ Map.unions maps
236−-- click :: (Int, Int) -> Int -> DisplayLines -> TextZipper
237−-- click (x, y) scrollTop dlines = undefined
239−images :: [[Span]] -> [V.Image]
240−images = map (V.horizCat . map spanToImage)
242−image :: [[Span]] -> V.Image
243−image = V.vertCat . images
245−spanToImage :: Span -> V.Image
246−spanToImage (Span attrs t) = V.text' attrs t
248−updateTextZipper :: V.Event -> TextZipper -> TextZipper
249−updateTextZipper ev = case ev of
250− -- Regular characters
251− V.EvKey (V.KChar k) [] -> insert $ T.singleton k
253− V.EvKey V.KBS [] -> deleteLeft
254− V.EvKey V.KDel [] -> deleteRight
256− V.EvKey (V.KChar 'u') [V.MCtrl] -> const empty
257− V.EvKey (V.KChar 'w') [V.MCtrl] -> deleteLeftWord
259− V.EvKey V.KLeft [] -> left
260− V.EvKey V.KRight [] -> right
261− V.EvKey V.KUp [] -> up
262− V.EvKey V.KDown [] -> down
263− V.EvKey V.KHome [] -> home
264− V.EvKey V.KEnd [] -> end