ihaskell-0.9.0.0: src/tests/IHaskell/Test/Completion.hs
{-# language NoImplicitPrelude, DoAndIfThenElse, OverloadedStrings, ExtendedDefaultRules #-}
{-# LANGUAGE CPP #-}
module IHaskell.Test.Completion (testCompletions) where
import Prelude
import Data.List (elemIndex)
import qualified Data.Text as T
import Control.Monad.IO.Class (liftIO)
import System.Environment (setEnv)
import System.Directory (setCurrentDirectory, getCurrentDirectory)
import GHC (getSessionDynFlags, setSessionDynFlags, DynFlags(..), GhcLink(..), setContext,
parseImportDecl, HscTarget(..), InteractiveImport(..))
import Test.Hspec
import Shelly (toTextIgnore, (</>), shelly, fromText, get_env_text, FilePath, cd, mkdir_p,
touchfile, withTmpDir)
import IHaskell.Eval.Evaluate (Interpreter, liftIO)
import IHaskell.Eval.Completion (complete, CompletionType(..), completionType,
completionTarget)
import IHaskell.Test.Util (replace, shouldBeAmong, ghc)
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
#endif
-- | @readCompletePrompt "xs*ys"@ return @(xs, i)@ where i is the location of
-- @'*'@ in the input string.
readCompletePrompt :: String -> (String, Int)
readCompletePrompt string =
case elemIndex '*' string of
Nothing -> error "Expected cursor written as '*'."
Just idx -> (replace "*" "" string, idx)
completionEvent :: String -> Interpreter (String, [String])
completionEvent string = complete newString cursorloc
where
(newString, cursorloc) =
case elemIndex '*' string of
Nothing -> error "Expected cursor written as '*'."
Just idx -> (replace "*" "" string, idx)
completionEventInDirectory :: String -> IO (String, [String])
completionEventInDirectory string = withHsDirectory $ const $ completionEvent string
shouldHaveCompletionsInDirectory :: String -> [String] -> IO ()
shouldHaveCompletionsInDirectory string expected = do
(matched, completions) <- completionEventInDirectory string
let existsInCompletion = (`elem` completions)
unmatched = filter (not . existsInCompletion) expected
expected `shouldBeAmong` completions
completionHas string expected = do
(matched, completions) <- ghc $ do
initCompleter
completionEvent string
let existsInCompletion = (`elem` completions)
unmatched = filter (not . existsInCompletion) expected
expected `shouldBeAmong` completions
initCompleter :: Interpreter ()
initCompleter = do
flags <- getSessionDynFlags
setSessionDynFlags $ flags { hscTarget = HscInterpreted, ghcLink = LinkInMemory }
-- Import modules.
imports <- mapM parseImportDecl
[ "import Prelude"
, "import qualified Control.Monad"
, "import qualified Data.List as List"
, "import IHaskell.Display"
, "import Data.Maybe as Maybe"
]
setContext $ map IIDecl imports
completes :: String -> [String] -> IO ()
completes string expected = completionTarget newString cursorloc `shouldBe` expected
where
(newString, cursorloc) = readCompletePrompt string
testCompletions :: Spec
testCompletions = do
testIdentifierCompletion
testCommandCompletion
testIdentifierCompletion :: Spec
testIdentifierCompletion = describe "Completion" $ do
it "correctly gets the completion identifier without dots" $ do
"hello*" `completes` ["hello"]
"hello aa*bb goodbye" `completes` ["aa"]
"hello aabb* goodbye" `completes` ["aabb"]
"aacc* goodbye" `completes` ["aacc"]
"hello *aabb goodbye" `completes` []
"*aabb goodbye" `completes` []
it "correctly gets the completion identifier with dots" $ do
"hello test.aa*bb goodbye" `completes` ["test", "aa"]
"Test.*" `completes` ["Test", ""]
"Test.Thing*" `completes` ["Test", "Thing"]
"Test.Thing.*" `completes` ["Test", "Thing", ""]
"Test.Thing.*nope" `completes` ["Test", "Thing", ""]
it "correctly gets the completion type" $ do
completionType "import Data." 12 ["Data", ""] `shouldBe` ModuleName "Data" ""
completionType "import Prel" 11 ["Prel"] `shouldBe` ModuleName "" "Prel"
completionType "import D.B.M" 12 ["D", "B", "M"] `shouldBe` ModuleName "D.B" "M"
completionType " import A." 10 ["A", ""] `shouldBe` ModuleName "A" ""
completionType "import a.x" 10 ["a", "x"] `shouldBe` Identifier "x"
completionType "A.x" 3 ["A", "x"] `shouldBe` Qualified "A" "x"
completionType "a.x" 3 ["a", "x"] `shouldBe` Identifier "x"
completionType "pri" 3 ["pri"] `shouldBe` Identifier "pri"
completionType ":load A" 7 ["A"] `shouldBe` HsFilePath ":load A" "A"
completionType ":! cd " 6 [""] `shouldBe` FilePath ":! cd " ""
it "properly completes identifiers" $ do
"pri*" `completionHas` ["print"]
"ma*" `completionHas` ["map"]
"hello ma*" `completionHas` ["map"]
"print $ catMa*" `completionHas` ["catMaybes"]
it "properly completes qualified identifiers" $ do
"Control.Monad.liftM*" `completionHas` [ "Control.Monad.liftM"
, "Control.Monad.liftM2"
, "Control.Monad.liftM5"
]
"print $ List.intercal*" `completionHas` ["List.intercalate"]
"print $ Data.Maybe.cat*" `completionHas` []
"print $ Maybe.catM*" `completionHas` ["Maybe.catMaybes"]
it "properly completes imports" $ do
"import Data.*" `completionHas` ["Data.Maybe", "Data.List"]
"import Data.M*" `completionHas` ["Data.Maybe"]
"import Prel*" `completionHas` ["Prelude"]
testCommandCompletion :: Spec
testCommandCompletion = describe "Completes commands" $ do
it "properly completes haskell file paths on :load directive" $ do
let loading xs = ":load " ++ T.unpack (toTextIgnore xs)
paths = map (T.unpack . toTextIgnore)
testInDirectory start comps = loading start `shouldHaveCompletionsInDirectory` paths comps
testInDirectory ("dir" </> "file*") ["dir" </> "file2.hs", "dir" </> "file2.lhs"]
testInDirectory ("" </> "file1*") ["" </> "file1.hs", "" </> "file1.lhs"]
testInDirectory ("" </> "file1*") ["" </> "file1.hs", "" </> "file1.lhs"]
testInDirectory ("" </> "./*") ["./" </> "dir/", "./" </> "file1.hs", "./" </> "file1.lhs"]
testInDirectory ("" </> "./*") ["./" </> "dir/", "./" </> "file1.hs", "./" </> "file1.lhs"]
it "provides path completions on empty shell cmds " $
":! cd *" `shouldHaveCompletionsInDirectory` map (T.unpack . toTextIgnore)
[ "" </> "dir/"
, "" </> "file1.hs"
, "" </> "file1.lhs"
]
let withHsHome action = withHsDirectory $ \dirPath -> do
home <- shelly $ Shelly.get_env_text "HOME"
setHomeEvent dirPath
result <- action
setHomeEvent $ Shelly.fromText home
return result
setHomeEvent path = liftIO $ setEnv "HOME" (T.unpack $ toTextIgnore path)
it "correctly interprets ~ as the environment HOME variable" $ do
let shouldHaveCompletions :: String -> [String] -> IO ()
shouldHaveCompletions string expected = do
(matched, completions) <- withHsHome $ completionEvent string
let existsInCompletion = (`elem` completions)
unmatched = filter (not . existsInCompletion) expected
expected `shouldBeAmong` completions
":! cd ~/*" `shouldHaveCompletions` ["~/dir/"]
":! ~/*" `shouldHaveCompletions` ["~/dir/"]
":load ~/*" `shouldHaveCompletions` ["~/dir/"]
":l ~/*" `shouldHaveCompletions` ["~/dir/"]
let shouldHaveMatchingText :: String -> String -> IO ()
shouldHaveMatchingText string expected = do
matchText <- withHsHome $ fst <$> uncurry complete (readCompletePrompt string)
matchText `shouldBe` expected
setHomeEvent path = liftIO $ setEnv "HOME" (T.unpack $ toTextIgnore path)
it "generates the correct matchingText on `:! cd ~/*` " $
":! cd ~/*" `shouldHaveMatchingText` ("~/" :: String)
it "generates the correct matchingText on `:load ~/*` " $
":load ~/*" `shouldHaveMatchingText` ("~/" :: String)
it "generates the correct matchingText on `:l ~/*` " $
":l ~/*" `shouldHaveMatchingText` ("~/" :: String)
inDirectory :: [Shelly.FilePath] -- ^ directories relative to temporary directory
-> [Shelly.FilePath] -- ^ files relative to temporary directory
-> (Shelly.FilePath -> Interpreter a)
-> IO a
-- | Run an Interpreter action, but first make a temporary directory
-- with some files and folder and cd to it.
inDirectory dirs files action = shelly $ withTmpDir $ \dirPath -> do
cd dirPath
mapM_ mkdir_p dirs
mapM_ touchfile files
liftIO $ ghc $ wrap (T.unpack $ toTextIgnore dirPath) (action dirPath)
where
cdEvent path = liftIO $ setCurrentDirectory path
wrap :: String -> Interpreter a -> Interpreter a
wrap path action = do
initCompleter
pwd <- IHaskell.Eval.Evaluate.liftIO getCurrentDirectory
cdEvent path -- change to the temporary directory
out <- action -- run action
cdEvent pwd -- change back to the original directory
return out
withHsDirectory :: (Shelly.FilePath -> Interpreter a) -> IO a
withHsDirectory = inDirectory [p "" </> p "dir", p "dir" </> p "dir1"]
[ p "" </> p "file1.hs"
, p "dir" </> p "file2.hs"
, p "" </> p "file1.lhs"
, p "dir" </> p "file2.lhs"
]
where
p = id