-
Notifications
You must be signed in to change notification settings - Fork 199
/
Search.hs
441 lines (380 loc) · 15.8 KB
/
Search.hs
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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK show-extensions #-}
-- |
-- Module : Yi.Search
-- License : GPL-2
-- Maintainer : yi-devel@googlegroups.com
-- Stability : experimental
-- Portability : portable
--
-- Search/Replace functions
module Yi.Search (
setRegexE, -- :: SearchExp -> EditorM ()
resetRegexE, -- :: EditorM ()
getRegexE, -- :: EditorM (Maybe SearchExp)
SearchMatch,
SearchResult(..),
SearchOption(..),
doSearch, -- :: (Maybe String) -> [SearchOption]
-- -> Direction -> YiM ()
searchInit, -- :: String
-- -> [SearchOption]
-- -> IO SearchExp
continueSearch, -- :: SearchExp
-- -> IO SearchResult
makeSimpleSearch,
-- * Batch search-replace
searchReplaceRegionB,
searchReplaceSelectionB,
replaceString,
searchAndRepRegion,
searchAndRepRegion0,
searchAndRepUnit, -- :: String -> String -> Bool -> TextUnit -> EditorM Bool
-- * Incremental Search
isearchInitE,
isearchIsEmpty,
isearchAddE,
isearchPrevE,
isearchNextE,
isearchWordE,
isearchHistory,
isearchDelE,
isearchCancelE,
isearchFinishE,
isearchCancelWithE,
isearchFinishWithE,
-- * Replace
qrNext,
qrReplaceAll,
qrReplaceOne,
qrFinish
) where
import Control.Applicative ((<$>))
import Control.Lens (assign)
import Control.Monad (void, when)
import Data.Binary (Binary, get, put)
import Data.Char (isAlpha, isUpper)
import Data.Default (Default, def)
import Data.Maybe (listToMaybe)
import Data.Monoid ((<>))
import qualified Data.Text as T (Text, any, break, empty, length, null, takeWhile, unpack)
import qualified Data.Text.Encoding as E (decodeUtf8, encodeUtf8)
import Data.Typeable (Typeable)
import Yi.Buffer
import Yi.Editor
import Yi.History (historyFinishGen, historyMoveGen, historyStartGen)
import Yi.Regex
import qualified Yi.Rope as R (YiString, null, toString, toText)
import Yi.Search.Internal (getRegexE, resetRegexE, setRegexE)
import Yi.String (showT)
import Yi.Types (YiVariable)
import Yi.Utils (fst3)
import Yi.Window (Window)
-- ---------------------------------------------------------------------
--
-- | Global searching. Search for regex and move point to that position.
-- @Nothing@ means reuse the last regular expression. @Just s@ means use
-- @s@ as the new regular expression. Direction of search can be
-- specified as either @Backward@ or @Forward@ (forwards in the buffer).
-- Arguments to modify the compiled regular expression can be supplied
-- as well.
--
type SearchMatch = Region
data SearchResult = PatternFound
| PatternNotFound
| SearchWrapped
deriving Eq
doSearch :: Maybe String -- ^ @Nothing@ means used previous
-- pattern, if any. Complain otherwise.
-- Use getRegexE to check for previous patterns
-> [SearchOption] -- ^ Flags to modify the compiled regex
-> Direction -- ^ @Backward@ or @Forward@
-> EditorM SearchResult
doSearch (Just re) fs d = searchInit re d fs >>= withCurrentBuffer . continueSearch
doSearch Nothing _ d = do
mre <- getRegexE
case mre of
Nothing -> fail "No previous search pattern" -- NB
Just r -> withCurrentBuffer (continueSearch (r,d))
-- | Set up a search.
searchInit :: String -> Direction -> [SearchOption] -> EditorM (SearchExp, Direction)
searchInit re d fs = do
let Right c_re = makeSearchOptsM fs re
setRegexE c_re
assign searchDirectionA d
return (c_re,d)
-- | Do a search, placing cursor at first char of pattern, if found.
-- Keymaps may implement their own regex language. How do we provide for this?
-- Also, what's happening with ^ not matching sol?
continueSearch :: (SearchExp, Direction) -> BufferM SearchResult
continueSearch (c_re, dir) = do
mp <- savingPointB $ do
moveB Character dir -- start immed. after cursor
rs <- regexB dir c_re
moveB Document (reverseDir dir) -- wrap around
ls <- regexB dir c_re
return $ listToMaybe $ fmap Right rs ++ fmap Left ls
maybe (return ()) (moveTo . regionStart . either id id) mp
return $ f mp
where
f (Just (Right _)) = PatternFound
f (Just (Left _)) = SearchWrapped
f Nothing = PatternNotFound
------------------------------------------------------------------------
-- Batch search and replace
--
-- | Search and Replace all within the current region.
-- Note the region is the final argument since we might perform
-- the same search and replace over multiple regions however we are
-- unlikely to perform several search and replaces over the same region
-- since the first such may change the bounds of the region.
searchReplaceRegionB :: R.YiString -- ^ The string to search for
-> R.YiString -- ^ The string to replace it with
-> Region -- ^ The region to perform this over
-> BufferM Int
searchReplaceRegionB from to =
searchAndRepRegion0 (makeSimpleSearch from) to True
-- | Peform a search and replace on the selection
searchReplaceSelectionB :: R.YiString -- ^ text to search for
-> R.YiString -- ^ text to replace it with
-> BufferM Int
searchReplaceSelectionB from to =
getSelectRegionB >>= searchReplaceRegionB from to
-- | Replace a string by another everywhere in the document
replaceString :: R.YiString -> R.YiString -> BufferM Int
replaceString a b = regionOfB Document >>= searchReplaceRegionB a b
------------------------------------------------------------------------
-- | Search and replace in the given region.
--
-- If the input boolean is True, then the replace is done globally,
-- otherwise only the first match is replaced. Returns the number of
-- replacements done.
searchAndRepRegion0 :: SearchExp -> R.YiString -> Bool -> Region -> BufferM Int
searchAndRepRegion0 c_re str globally region = do
mp <- (if globally then id else take 1) <$> regexRegionB c_re region -- find the regex
-- mp' is a maybe not reversed version of mp, the goal
-- is to avoid replaceRegionB to mess up the next regions.
-- So we start from the end.
let mp' = mayReverse (reverseDir $ regionDirection region) mp
mapM_ (`replaceRegionB` str) mp'
return (length mp)
searchAndRepRegion :: R.YiString -> R.YiString -> Bool -> Region -> EditorM Bool
searchAndRepRegion s str globally region = case R.null s of
False -> return False
True -> do
let c_re = makeSimpleSearch s
setRegexE c_re -- store away for later use
assign searchDirectionA Forward
withCurrentBuffer $ (/= 0) <$> searchAndRepRegion0 c_re str globally region
------------------------------------------------------------------------
-- | Search and replace in the region defined by the given unit.
-- The rest is as in 'searchAndRepRegion'.
searchAndRepUnit :: R.YiString -> R.YiString -> Bool -> TextUnit -> EditorM Bool
searchAndRepUnit re str g unit =
withCurrentBuffer (regionOfB unit) >>= searchAndRepRegion re str g
--------------------------
-- Incremental search
newtype Isearch = Isearch [(T.Text, Region, Direction)]
deriving (Typeable, Show)
instance Binary Isearch where
put (Isearch ts) = put (map3 E.encodeUtf8 ts)
get = Isearch . map3 E.decodeUtf8 <$> get
map3 :: (a -> d) -> [(a, b, c)] -> [(d, b, c)]
map3 _ [] = []
map3 f ((a, b, c):xs) = (f a, b, c) : map3 f xs
-- This contains: (string currently searched, position where we
-- searched it, direction, overlay for highlighting searched text)
-- Note that this info cannot be embedded in the Keymap state: the state
-- modification can depend on the state of the editor.
instance Default Isearch where
def = Isearch []
instance YiVariable Isearch
isearchInitE :: Direction -> EditorM ()
isearchInitE dir = do
historyStartGen iSearch
p <- withCurrentBuffer pointB
resetRegexE
putEditorDyn (Isearch [(T.empty ,mkRegion p p, dir)])
printMsg "I-search: "
isearchIsEmpty :: EditorM Bool
isearchIsEmpty = do
Isearch s <- getEditorDyn
return . not . T.null . fst3 $ head s
isearchAddE :: T.Text -> EditorM ()
isearchAddE inc = isearchFunE (<> inc)
-- | Create a SearchExp that matches exactly its argument
makeSimpleSearch :: R.YiString -> SearchExp
makeSimpleSearch s = se
where Right se = makeSearchOptsM [QuoteRegex] (R.toString s)
makeISearch :: T.Text -> SearchExp
makeISearch s = case makeSearchOptsM opts (T.unpack s) of
Left _ -> SearchExp (T.unpack s) emptyRegex emptyRegex []
Right search -> search
where opts = QuoteRegex : if T.any isUpper s then [] else [IgnoreCase]
isearchFunE :: (T.Text -> T.Text) -> EditorM ()
isearchFunE fun = do
Isearch s <- getEditorDyn
let (previous,p0,direction) = head s
current = fun previous
srch = makeISearch current
printMsg $ "I-search: " <> current
setRegexE srch
prevPoint <- withCurrentBuffer pointB
matches <- withCurrentBuffer $ do
moveTo $ regionStart p0
when (direction == Backward) $
moveN $ T.length current
regexB direction srch
let onSuccess p = do withCurrentBuffer $ moveTo (regionEnd p)
putEditorDyn $ Isearch ((current, p, direction) : s)
case matches of
(p:_) -> onSuccess p
[] -> do matchesAfterWrap <- withCurrentBuffer $ do
case direction of
Forward -> moveTo 0
Backward -> do
bufferLength <- sizeB
moveTo bufferLength
regexB direction srch
case matchesAfterWrap of
(p:_) -> onSuccess p
[] -> do withCurrentBuffer $ moveTo prevPoint -- go back to where we were
putEditorDyn $ Isearch ((current, p0, direction) : s)
printMsg $ "Failing I-search: " <> current
isearchDelE :: EditorM ()
isearchDelE = do
Isearch s <- getEditorDyn
case s of
(_:(text,p,dir):rest) -> do
withCurrentBuffer $
moveTo $ regionEnd p
putEditorDyn $ Isearch ((text,p,dir):rest)
setRegexE $ makeISearch text
printMsg $ "I-search: " <> text
_ -> return () -- if the searched string is empty, don't try to remove chars from it.
isearchHistory :: Int -> EditorM ()
isearchHistory delta = do
Isearch ((current,_p0,_dir):_) <- getEditorDyn
h <- historyMoveGen iSearch delta (return current)
isearchFunE (const h)
isearchPrevE :: EditorM ()
isearchPrevE = isearchNext0 Backward
isearchNextE :: EditorM ()
isearchNextE = isearchNext0 Forward
isearchNext0 :: Direction -> EditorM ()
isearchNext0 newDir = do
Isearch ((current,_p0,_dir):_rest) <- getEditorDyn
if T.null current
then isearchHistory 1
else isearchNext newDir
isearchNext :: Direction -> EditorM ()
isearchNext direction = do
Isearch ((current, p0, _dir) : rest) <- getEditorDyn
withCurrentBuffer $ moveTo (regionStart p0 + startOfs)
mp <- withCurrentBuffer $
regexB direction (makeISearch current)
case mp of
[] -> do
endPoint <- withCurrentBuffer $ do
moveTo (regionEnd p0) -- revert to offset we were before.
sizeB
printMsg "isearch: end of document reached"
let wrappedOfs = case direction of
Forward -> mkRegion 0 0
Backward -> mkRegion endPoint endPoint
putEditorDyn $ Isearch ((current,wrappedOfs,direction):rest) -- prepare to wrap around.
(p:_) -> do
withCurrentBuffer $
moveTo (regionEnd p)
printMsg $ "I-search: " <> current
putEditorDyn $ Isearch ((current,p,direction):rest)
where startOfs = case direction of
Forward -> 1
Backward -> -1
isearchWordE :: EditorM ()
isearchWordE = do
-- add maximum 32 chars at a time.
text <- R.toText <$> withCurrentBuffer (pointB >>= nelemsB 32)
let (prefix, rest) = T.break isAlpha text
word = T.takeWhile isAlpha rest
isearchAddE $ prefix <> word
-- | Succesfully finish a search. Also see 'isearchFinishWithE'.
isearchFinishE :: EditorM ()
isearchFinishE = isearchEnd True
-- | Cancel a search. Also see 'isearchCancelWithE'.
isearchCancelE :: EditorM ()
isearchCancelE = isearchEnd False
-- | Wrapper over 'isearchEndWith' that passes through the action and
-- accepts the search as successful (i.e. when the user wants to stay
-- at the result).
isearchFinishWithE :: EditorM a -> EditorM ()
isearchFinishWithE act = isearchEndWith act True
-- | Wrapper over 'isearchEndWith' that passes through the action and
-- marks the search as unsuccessful (i.e. when the user wants to
-- jump back to where the search started).
isearchCancelWithE :: EditorM a -> EditorM ()
isearchCancelWithE act = isearchEndWith act False
iSearch :: T.Text
iSearch = "isearch"
-- | Editor action describing how to end finish incremental search.
-- The @act@ parameter allows us to specify an extra action to run
-- before finishing up the search. For Vim, we don't want to do
-- anything so we use 'isearchEnd' which just does nothing. For emacs,
-- we want to cancel highlighting and stay where we are.
isearchEndWith :: EditorM a -> Bool -> EditorM ()
isearchEndWith act accept = getEditorDyn >>= \case
Isearch [] -> return ()
Isearch s@((lastSearched, _, dir):_) -> do
let (_,p0,_) = last s
historyFinishGen iSearch (return lastSearched)
assign searchDirectionA dir
if accept
then do void act
withCurrentBuffer $ setSelectionMarkPointB $ regionStart p0
printMsg "Quit"
else do resetRegexE
withCurrentBuffer $ moveTo $ regionStart p0
-- | Specialised 'isearchEndWith' to do nothing as the action.
isearchEnd :: Bool -> EditorM ()
isearchEnd = isearchEndWith (return ())
-----------------
-- Query-Replace
-- | Find the next match and select it.
-- Point is end, mark is beginning.
qrNext :: Window -> BufferRef -> SearchExp -> EditorM ()
qrNext win b what = do
mp <- withGivenBufferAndWindow win b $ regexB Forward what
case mp of
[] -> do
printMsg "String to search not found"
qrFinish
(r:_) -> withGivenBufferAndWindow win b $ setSelectRegionB r
-- | Replace all the remaining occurrences.
qrReplaceAll :: Window -> BufferRef -> SearchExp -> R.YiString -> EditorM ()
qrReplaceAll win b what replacement = do
n <- withGivenBufferAndWindow win b $ do
exchangePointAndMarkB -- so we replace the current occurence too
searchAndRepRegion0 what replacement True =<< regionOfPartB Document Forward
printMsg $ "Replaced " <> showT n <> " occurrences"
qrFinish
-- | Exit from query/replace.
qrFinish :: EditorM ()
qrFinish = do
assign currentRegexA Nothing
closeBufferAndWindowE -- the minibuffer.
-- | We replace the currently selected match and then move to the next
-- match.
qrReplaceOne :: Window -> BufferRef -> SearchExp -> R.YiString -> EditorM ()
qrReplaceOne win b reg replacement = do
qrReplaceCurrent win b replacement
qrNext win b reg
-- | This may actually be a bit more general it replaces the current
-- selection with the given replacement string in the given window and
-- buffer.
qrReplaceCurrent :: Window -> BufferRef -> R.YiString -> EditorM ()
qrReplaceCurrent win b replacement =
withGivenBufferAndWindow win b $
flip replaceRegionB replacement =<< getRawestSelectRegionB