{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_HADDOCK show-extensions #-} -- | -- Module : Yi.Completion -- License : GPL-2 -- Maintainer : yi-devel@googlegroups.com -- Stability : experimental -- Portability : portable -- -- Collection of functions for completion and matching. module Yi.Completion ( completeInList, completeInList' , completeInListCustomShow , commonPrefix , prefixMatch, infixUptoEndMatch , subsequenceMatch, subsequenceTextMatch , containsMatch', containsMatch, containsMatchCaseInsensitive , isCasePrefixOf ) where import Data.Function (on) import Data.List (find, nub) import Data.Maybe (catMaybes) import Data.Monoid ((<>)) import Data.Text (Text) import qualified Data.Text as T (Text, breakOn, isPrefixOf, length, null, tails, toCaseFold, splitAt) import Yi.Editor (EditorM, printMsg, printMsgs) import Yi.String (commonTPrefix', showT) import Yi.Utils (commonPrefix) ------------------------------------------- -- General completion -- | Like usual 'T.isPrefixOf' but user can specify case sensitivity. -- See 'T.toCaseFold' for exotic unicode gotchas. isCasePrefixOf :: Bool -- ^ Is case-sensitive? -> T.Text -> T.Text -> Bool isCasePrefixOf True = T.isPrefixOf isCasePrefixOf False = T.isPrefixOf `on` T.toCaseFold -- | Prefix matching function, for use with 'completeInList' prefixMatch :: T.Text -> T.Text -> Maybe T.Text prefixMatch prefix s = if prefix `T.isPrefixOf` s then Just s else Nothing -- | Text from the match up to the end, for use with 'completeInList' infixUptoEndMatch :: T.Text -> T.Text -> Maybe T.Text infixUptoEndMatch "" haystack = Just haystack infixUptoEndMatch needle haystack = case T.breakOn needle haystack of (_, t) -> if T.null t then Nothing else Just t -- | A simple fuzzy match algorithm. Example: "abc" matches "a1b2c" subsequenceMatch :: String -> String -> Bool subsequenceMatch needle haystack = go needle haystack where go (n:ns) (h:hs) | n == h = go ns hs go (n:ns) (h:hs) | n /= h = go (n:ns) hs go [] _ = True go _ [] = False go _ _ = False -- | A simple fuzzy match algorithm. Example: "abc" matches "a1b2c" subsequenceTextMatch :: Text -> Text -> Bool subsequenceTextMatch needle haystack | T.null needle = True | T.null haystack = False | n == h = subsequenceTextMatch ns hs | n /= h = subsequenceTextMatch needle hs | otherwise = False where n,ns,h,hs :: Text (n,ns) = T.splitAt 1 needle (h,hs) = T.splitAt 1 haystack -- | TODO: this is a terrible function, isn't this just -- case-insensitive infix? – Fūzetsu containsMatch' :: Bool -> T.Text -> T.Text -> Maybe T.Text containsMatch' caseSensitive pattern str = const str <$> find (pattern `tstPrefix`) (T.tails str) where tstPrefix = isCasePrefixOf caseSensitive containsMatch :: T.Text -> T.Text -> Maybe T.Text containsMatch = containsMatch' True containsMatchCaseInsensitive :: T.Text -> T.Text -> Maybe T.Text containsMatchCaseInsensitive = containsMatch' False -- | Complete a string given a user input string, a matching function -- and a list of possibilites. Matching function should return the -- part of the string that matches the user string. completeInList :: T.Text -- ^ Input to match on -> (T.Text -> Maybe T.Text) -- ^ matcher function -> [T.Text] -- ^ items to match against -> EditorM T.Text completeInList = completeInListCustomShow id -- | Same as 'completeInList', but maps @showFunction@ on possible -- matches when printing completeInListCustomShow :: (T.Text -> T.Text) -- ^ Show function -> T.Text -- ^ Input to match on -> (T.Text -> Maybe T.Text) -- ^ matcher function -> [T.Text] -- ^ items to match against -> EditorM T.Text completeInListCustomShow showFunction s match possibilities | null filtered = printMsg "No match" >> return s | prefix /= s = return prefix | isSingleton filtered = printMsg "Sole completion" >> return s | prefix `elem` filtered = printMsg ("Complete, but not unique: " <> showT filtered) >> return s | otherwise = printMsgs (map showFunction filtered) >> return (bestMatch filtered s) where prefix = commonTPrefix' filtered filtered = filterMatches match possibilities completeInList' :: T.Text -> (T.Text -> Maybe T.Text) -> [T.Text] -> EditorM T.Text completeInList' s match l = case filtered of [] -> printMsg "No match" >> return s [x] | s == x -> printMsg "Sole completion" >> return s | otherwise -> return x _ -> printMsgs filtered >> return (bestMatch filtered s) where filtered = filterMatches match l -- | This function attempts to provide a better tab completion result in -- cases where more than one file matches our prefix. Consider directory with -- following files: @["Main.hs", "Main.hi", "Main.o", "Test.py", "Foo.hs"]@. -- -- After inserting @Mai@ into the minibuffer and attempting to complete, the -- possible matches will be filtered in 'completeInList'' to -- @["Main.hs", "Main.hi", "Main.o"]@ however because of multiple matches, -- the buffer will not be updated to say @Main.@ but will instead stay at @Mai@. -- -- This is extremely tedious when trying to complete filenames in directories -- with many files so here we try to catch common prefixes of filtered files and -- if the result is longer than what we have, we use it instead. bestMatch :: [T.Text] -> T.Text -> T.Text bestMatch fs s = let p = commonTPrefix' fs in if T.length p > T.length s then p else s filterMatches :: Eq a => (b -> Maybe a) -> [b] -> [a] filterMatches match = nub . catMaybes . fmap match -- Not really necessary but a bit faster than @(length l) == 1@ isSingleton :: [a] -> Bool isSingleton [_] = True isSingleton _ = False