forked from purescript/purescript
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMatcher.hs
More file actions
123 lines (105 loc) · 4.33 KB
/
Matcher.hs
File metadata and controls
123 lines (105 loc) · 4.33 KB
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
-----------------------------------------------------------------------------
--
-- Module : Language.PureScript.Ide.Matcher
-- Description : Matchers for psc-ide commands
-- Copyright : Christoph Hegemann 2016
-- License : MIT (http://opensource.org/licenses/MIT)
--
-- Maintainer : Christoph Hegemann <christoph.hegemann1337@gmail.com>
-- Stability : experimental
--
-- |
-- Matchers for psc-ide commands
-----------------------------------------------------------------------------
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Language.PureScript.Ide.Matcher
( Matcher
, runMatcher
-- for tests
, flexMatcher
) where
import Protolude
import Data.Aeson
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Language.PureScript.Ide.Types
import Language.PureScript.Ide.Util
import Text.EditDistance
import Text.Regex.TDFA ((=~))
type ScoredMatch a = (Match a, Double)
newtype Matcher a = Matcher (Endo [Match a]) deriving (Monoid)
instance FromJSON (Matcher IdeDeclarationAnn) where
parseJSON = withObject "matcher" $ \o -> do
(matcher :: Maybe Text) <- o .:? "matcher"
case matcher of
Just "flex" -> do
params <- o .: "params"
flexMatcher <$> params .: "search"
Just "distance" -> do
params <- o .: "params"
distanceMatcher
<$> params .: "search"
<*> params .: "maximumDistance"
Just _ -> mzero
Nothing -> return mempty
-- | Matches any occurence of the search string with intersections
--
-- The scoring measures how far the matches span the string where
-- closer is better.
-- Examples:
-- flMa matches flexMatcher. Score: 14.28
-- sons matches sortCompletions. Score: 6.25
flexMatcher :: Text -> Matcher IdeDeclarationAnn
flexMatcher p = mkMatcher (flexMatch p)
distanceMatcher :: Text -> Int -> Matcher IdeDeclarationAnn
distanceMatcher q maxDist = mkMatcher (distanceMatcher' q maxDist)
distanceMatcher' :: Text -> Int -> [Match IdeDeclarationAnn] -> [ScoredMatch IdeDeclarationAnn]
distanceMatcher' q maxDist = mapMaybe go
where
go m = let d = dist (T.unpack y)
y = identifierFromIdeDeclaration (discardAnn (unwrapMatch m))
in if d <= maxDist
then Just (m, 1 / fromIntegral d)
else Nothing
dist = levenshteinDistance defaultEditCosts (T.unpack q)
mkMatcher :: ([Match a] -> [ScoredMatch a]) -> Matcher a
mkMatcher matcher = Matcher . Endo $ fmap fst . sortCompletions . matcher
runMatcher :: Matcher a -> [Match a] -> [Match a]
runMatcher (Matcher m)= appEndo m
sortCompletions :: [ScoredMatch a] -> [ScoredMatch a]
sortCompletions = sortBy (flip compare `on` snd)
flexMatch :: Text -> [Match IdeDeclarationAnn] -> [ScoredMatch IdeDeclarationAnn]
flexMatch = mapMaybe . flexRate
flexRate :: Text -> Match IdeDeclarationAnn -> Maybe (ScoredMatch IdeDeclarationAnn)
flexRate p c = do
score <- flexScore p (identifierFromIdeDeclaration (discardAnn (unwrapMatch c)))
return (c, score)
-- FlexMatching ala Sublime.
-- Borrowed from: http://cdewaka.com/2013/06/fuzzy-pattern-matching-in-haskell/
--
-- By string =~ pattern we'll get the start of the match and the length of
-- the matchas a (start, length) tuple if there's a match.
-- If match fails then it would be (-1,0)
flexScore :: Text -> Text -> Maybe Double
flexScore pat str =
case T.uncons pat of
Nothing -> Nothing
Just (first', p) ->
case TE.encodeUtf8 str =~ TE.encodeUtf8 pat' :: (Int, Int) of
(-1,0) -> Nothing
(start,len) -> Just $ calcScore start (start + len)
where
escapedPattern :: [Text]
escapedPattern = map escape (T.unpack p)
-- escape prepends a backslash to "regexy" characters to prevent the
-- matcher from crashing when trying to build the regex
escape :: Char -> Text
escape c = if c `elem` T.unpack "[\\^$.|?*+(){}"
then T.pack ['\\', c]
else T.singleton c
-- This just interleaves the search pattern with .*
-- abcd[*] -> a.*b.*c.*d.*[*]
pat' = escape first' <> foldMap (<> ".*") escapedPattern
calcScore start end =
100.0 / fromIntegral ((1 + start) * (end - start + 1))