Commit 1cda25e4 authored by Andrew Gibiansky's avatar Andrew Gibiansky

Merge pull request #103 from edechter/path_completion

Path completion
parents 92361c93 42365e54
...@@ -73,11 +73,12 @@ library ...@@ -73,11 +73,12 @@ library
directory, directory,
here, here,
system-filepath, system-filepath,
filemanip,
filepath, filepath,
cereal ==0.3.*, cereal ==0.3.*,
text >=0.11, text >=0.11,
mtl >= 2.1 mtl >= 2.1
transformers,
haskeline
exposed-modules: IHaskell.Display exposed-modules: IHaskell.Display
IHaskell.Eval.Completion IHaskell.Eval.Completion
IHaskell.Eval.Evaluate IHaskell.Eval.Evaluate
...@@ -145,11 +146,12 @@ executable IHaskell ...@@ -145,11 +146,12 @@ executable IHaskell
directory, directory,
here, here,
system-filepath, system-filepath,
filemanip,
filepath, filepath,
cereal ==0.3.*, cereal ==0.3.*,
text >=0.11, text >=0.11,
mtl >= 2.1 mtl >= 2.1,
transformers,
haskeline
Test-Suite hspec Test-Suite hspec
hs-source-dirs: src hs-source-dirs: src
...@@ -185,7 +187,9 @@ Test-Suite hspec ...@@ -185,7 +187,9 @@ Test-Suite hspec
filepath, filepath,
cereal ==0.3.*, cereal ==0.3.*,
text >=0.11, text >=0.11,
mtl >= 2.1 mtl >= 2.1,
transformers,
haskeline
extensions: DoAndIfThenElse extensions: DoAndIfThenElse
OverloadedStrings OverloadedStrings
ExtendedDefaultRules ExtendedDefaultRules
......
...@@ -75,10 +75,10 @@ completes string expected = completionTarget newString cursorloc `shouldBe` expe ...@@ -75,10 +75,10 @@ completes string expected = completionTarget newString cursorloc `shouldBe` expe
Nothing -> error "Expected cursor written as '!'." Nothing -> error "Expected cursor written as '!'."
Just idx -> (replace "!" "" string, idx) Just idx -> (replace "!" "" string, idx)
completionHas_ action string expected = do completionHas_ wrap string expected = do
(matched, completions) <- doGhc $ do (matched, completions) <- doGhc $ do
initCompleter action wrap $ do initCompleter
complete newString cursorloc complete newString cursorloc
let existsInCompletion = (`elem` completions) let existsInCompletion = (`elem` completions)
unmatched = filter (not . existsInCompletion) expected unmatched = filter (not . existsInCompletion) expected
unmatched `shouldBe` [] unmatched `shouldBe` []
...@@ -86,10 +86,12 @@ completionHas_ action string expected = do ...@@ -86,10 +86,12 @@ completionHas_ action string expected = do
Nothing -> error "Expected cursor written as '!'." Nothing -> error "Expected cursor written as '!'."
Just idx -> (replace "!" "" string, idx) Just idx -> (replace "!" "" string, idx)
completionHas = completionHas_ (return ()) completionHas = completionHas_ id
initCompleter :: GhcMonad m => m a -> m a initCompleter :: GhcMonad m => m ()
initCompleter action = do initCompleter = do
pwd <- Eval.liftIO $ getCurrentDirectory
--Eval.liftIO $ traceIO $ pwd
flags <- getSessionDynFlags flags <- getSessionDynFlags
setSessionDynFlags $ flags { hscTarget = HscInterpreted, ghcLink = LinkInMemory } setSessionDynFlags $ flags { hscTarget = HscInterpreted, ghcLink = LinkInMemory }
...@@ -99,7 +101,6 @@ initCompleter action = do ...@@ -99,7 +101,6 @@ initCompleter action = do
"import qualified Data.List as List", "import qualified Data.List as List",
"import Data.Maybe as Maybe"] "import Data.Maybe as Maybe"]
setContext $ map IIDecl imports setContext $ map IIDecl imports
action
withHsDirectory :: (FilePath -> Sh ()) -> IO () withHsDirectory :: (FilePath -> Sh ()) -> IO ()
withHsDirectory f = shelly $ withTmpDir $ \dirPath -> withHsDirectory f = shelly $ withTmpDir $ \dirPath ->
...@@ -141,7 +142,7 @@ completionTests = do ...@@ -141,7 +142,7 @@ completionTests = do
completionType "A.x" ["A", "x"] `shouldBe` Qualified "A" "x" completionType "A.x" ["A", "x"] `shouldBe` Qualified "A" "x"
completionType "a.x" ["a", "x"] `shouldBe` Identifier "x" completionType "a.x" ["a", "x"] `shouldBe` Identifier "x"
completionType "pri" ["pri"] `shouldBe` Identifier "pri" completionType "pri" ["pri"] `shouldBe` Identifier "pri"
completionType ":load A" [""] `shouldBe` HsFilePath "A" completionType ":load A" ["A"] `shouldBe` HsFilePath "A"
it "properly completes identifiers" $ do it "properly completes identifiers" $ do
"pri!" `completionHas` ["print"] "pri!" `completionHas` ["print"]
...@@ -166,16 +167,23 @@ completionTests = do ...@@ -166,16 +167,23 @@ completionTests = do
withHsDirectory $ \dirPath -> withHsDirectory $ \dirPath ->
let loading xs = ":load " ++ encodeString xs let loading xs = ":load " ++ encodeString xs
paths xs = map encodeString xs paths xs = map encodeString xs
completionHas' = completionHas_ $ completionHas' = completionHas_ fun
do Eval.evaluate defaultKernelState fun action = do pwd <- Eval.liftIO getCurrentDirectory
(":! cd " ++ dirPath) Eval.evaluate defaultKernelState
(\b d -> return ()) (":! cd " ++ dirPath)
(\b d -> return ())
out <- action
Eval.evaluate defaultKernelState
(":! cd " ++ pwd)
(\b d -> return ())
return out
in liftIO $ do in liftIO $ do
loading ("dir" </> "file!") `completionHas'` paths ["dir" </> "file2.hs", loading ("dir" </> "file!") `completionHas'` paths ["dir" </> "file2.hs",
"dir" </> "file2.lhs"] "dir" </> "file2.lhs"]
loading ("" </> "file1!") `completionHas'` paths ["" </> "file1.hs", loading ("" </> "file1!") `completionHas'` paths ["" </> "file1.hs",
"" </> "file1.lhs"] "" </> "file1.lhs"]
loading ("" </> "file1!") `completionHas'` paths ["" </> "file1.hs",
"" </> "file1.lhs"]
evalTests = do evalTests = do
describe "Code Evaluation" $ do describe "Code Evaluation" $ do
......
...@@ -27,27 +27,30 @@ import GhcMonad ...@@ -27,27 +27,30 @@ import GhcMonad
import PackageConfig import PackageConfig
import Outputable (showPpr) import Outputable (showPpr)
import qualified System.FilePath.Find as Find (find)
import System.FilePath.Find hiding (find)
import System.Directory import System.Directory
import System.FilePath.GlobPattern
import System.FilePath import System.FilePath
import MonadUtils (MonadIO) import MonadUtils (MonadIO)
import Control.Monad (filterM, mapM, liftM) import Control.Monad (filterM, mapM, liftM)
import System.Console.Haskeline.Completion
import qualified Control.Monad.IO.Class as MonadIO (MonadIO(), liftIO)
import IHaskell.Types import IHaskell.Types
import IHaskell.Eval.Evaluate (Interpreter)
data CompletionType data CompletionType
= Empty = Empty
| Identifier String | Identifier String
| Extension String | Extension String
| Qualified String String | Qualified String String
| ModuleName String String | ModuleName String String
| HsFilePath String | HsFilePath String
| FilePath String
deriving (Show, Eq) deriving (Show, Eq)
complete :: GHC.GhcMonad m => String -> Int -> m (String, [String]) complete :: String -> Int -> Interpreter (String, [String])
complete line pos = do complete line pos = do
flags <- getSessionDynFlags flags <- getSessionDynFlags
rdrNames <- map (showPpr flags) <$> getRdrNamesInScope rdrNames <- map (showPpr flags) <$> getRdrNamesInScope
...@@ -63,7 +66,7 @@ complete line pos = do ...@@ -63,7 +66,7 @@ complete line pos = do
let target = completionTarget line pos let target = completionTarget line pos
matchedText = intercalate "." target matchedText = intercalate "." target
options <- options <-
case completionType line target of case completionType line target of
Empty -> return [] Empty -> return []
...@@ -89,12 +92,13 @@ complete line pos = do ...@@ -89,12 +92,13 @@ complete line pos = do
nonames = map ("No" ++) names nonames = map ("No" ++) names
return $ filter (ext `isPrefixOf`) $ names ++ nonames return $ filter (ext `isPrefixOf`) $ names ++ nonames
HsFilePath path -> do pwd <- liftIO getCurrentDirectory HsFilePath path -> completePathWithExtensions [".hs", ".lhs"] path
completePath pwd (Just [".hs", ".lhs"]) path
FilePath path -> completePath path
return (matchedText, options) return (matchedText, options)
getTrueModuleName :: GhcMonad m => String -> m String getTrueModuleName :: String -> Interpreter String
getTrueModuleName name = do getTrueModuleName name = do
-- Only use the things that were actually imported -- Only use the things that were actually imported
let onlyImportDecl (IIDecl decl) = Just decl let onlyImportDecl (IIDecl decl) = Just decl
...@@ -109,20 +113,22 @@ getTrueModuleName name = do ...@@ -109,20 +113,22 @@ getTrueModuleName name = do
let qualifiedImports = filter (isJust . ideclAs) imports let qualifiedImports = filter (isJust . ideclAs) imports
hasName imp = name == (showPpr flags . fromJust . ideclAs) imp hasName imp = name == (showPpr flags . fromJust . ideclAs) imp
case find hasName qualifiedImports of case find hasName qualifiedImports of
Nothing -> return name Nothing -> return name
Just trueImp -> return $ showPpr flags $ unLoc $ ideclName trueImp Just trueImp -> return $ showPpr flags $ unLoc $ ideclName trueImp
completionType :: String -> [String] -> CompletionType completionType :: String -> [String] -> CompletionType
completionType line [] = Empty completionType line [] = Empty
completionType line target completionType line target
| startswith ":l" stripped | startswith ":! " stripped
= HsFilePath $ last $ splitOn " " stripped = FilePath complete_target
| startswith ":l" stripped
= HsFilePath complete_target
| startswith "import" stripped && isModName | startswith "import" stripped && isModName
= ModuleName dotted candidate = ModuleName dotted candidate
| isModName && (not . null . init) target | isModName && (not . null . init) target
= Qualified dotted candidate = Qualified dotted candidate
| startswith ":e" stripped | startswith ":e" stripped
= Extension candidate = Extension candidate
| otherwise | otherwise
= Identifier candidate = Identifier candidate
where stripped = strip line where stripped = strip line
...@@ -131,13 +137,14 @@ completionType line target ...@@ -131,13 +137,14 @@ completionType line target
dots = intercalate "." . init dots = intercalate "." . init
isModName = all isCapitalized (init target) isModName = all isCapitalized (init target)
isCapitalized = isUpper . head isCapitalized = isUpper . head
complete_target = intercalate "." target
-- | Get the word under a given cursor location. -- | Get the word under a given cursor location.
completionTarget :: String -> Int -> [String] completionTarget :: String -> Int -> [String]
completionTarget code cursor = expandCompletionPiece pieceToComplete completionTarget code cursor = expandCompletionPiece pieceToComplete
where where
pieceToComplete = map fst <$> find (elem cursor . map snd) pieces pieceToComplete = map fst <$> find (elem cursor . map snd) pieces
pieces = splitAlongCursor $ split splitter $ zip code [1 .. ] pieces = splitAlongCursor $ split splitter $ zip code [1 .. ]
splitter = defaultSplitter { splitter = defaultSplitter {
...@@ -153,7 +160,7 @@ completionTarget code cursor = expandCompletionPiece pieceToComplete ...@@ -153,7 +160,7 @@ completionTarget code cursor = expandCompletionPiece pieceToComplete
splitAlongCursor :: [[(Char, Int)]] -> [[(Char, Int)]] splitAlongCursor :: [[(Char, Int)]] -> [[(Char, Int)]]
splitAlongCursor [] = [] splitAlongCursor [] = []
splitAlongCursor (x:xs) = splitAlongCursor (x:xs) =
case elemIndex cursor $ map snd x of case elemIndex cursor $ map snd x of
Nothing -> x:splitAlongCursor xs Nothing -> x:splitAlongCursor xs
Just idx -> take (idx + 1) x:drop (idx + 1) x:splitAlongCursor xs Just idx -> take (idx + 1) x:drop (idx + 1) x:splitAlongCursor xs
...@@ -162,34 +169,26 @@ completionTarget code cursor = expandCompletionPiece pieceToComplete ...@@ -162,34 +169,26 @@ completionTarget code cursor = expandCompletionPiece pieceToComplete
neverIdent = " \n\t(),{}[]\\'\"`" neverIdent = " \n\t(),{}[]\\'\"`"
expandCompletionPiece Nothing = [] expandCompletionPiece Nothing = []
expandCompletionPiece (Just str) = splitOn "." str expandCompletionPiece (Just str) = splitOn "." str
completePath :: MonadIO m => completePathFilter :: (String -> Bool) -- ^ filter files
String -- ^ Current directory -> (String -> Bool) -- ^ filter directories
-> Maybe [String] -- ^ list of file extensions -> String -- ^ line contents left of cursor
-> String -- ^ prefix to be completed -> String -- ^ line contents right of cursor
-> m [String] -- ^ completions, that is, if prefix is "Mai" one completion might be "Main.hs" -> Interpreter [String]
completePath currDir exts prefix completePathFilter fileFilter dirFilter loc roc =
= let absolutePrefix = combine currDir prefix do (_, comps) <- MonadIO.liftIO $ (completeFilename (reverse loc, roc))
searchDir = dropFileName absolutePrefix let completions = map replacement comps
pattern = absolutePrefix ++ "*" dirs <- liftIO $ filterM doesDirectoryExist completions
completions = liftIO $ Find.find always (filePath ~~? pattern) searchDir files <- liftIO $ filterM (liftM not . doesDirectoryExist) completions
allFileCompletions = completions >>= liftIO . filterM (liftM not . doesDirectoryExist) let dirs' = filter dirFilter files
fileCompletions = case exts of files' = filter fileFilter dirs
Nothing -> allFileCompletions return $ filter (\x -> elem x $ dirs' ++ files') completions
Just exts -> do xs <- allFileCompletions
return $ filter (\s -> or [endswith ext s | ext <- exts]) xs completePath :: String -> Interpreter [String]
dirCompletions = completions completePath loc = completePathFilter (const True) (const True) loc ""
>>= liftIO . filterM doesDirectoryExist
>>= \xs -> do return $ [x ++ [pathSeparator] | x <- xs] completePathWithExtensions :: [String] -> String -> Interpreter [String]
relativeCompletions = do validSearchDir <- liftIO $ doesDirectoryExist searchDir completePathWithExtensions extensions loc =
if validSearchDir then completePathFilter (\s -> any (\x -> endswith x s) extensions) (const True) loc ""
do xs <- fileCompletions
ys <- dirCompletions
return $ map (cut $ currDir ++ [pathSeparator]) $ xs ++ ys
else return []
cut :: String -> String -> String
cut (x:xs) z@(y:ys) | x == y = cut xs ys
| otherwise = z
cut _ z = z
in relativeCompletions
{-# LANGUAGE DoAndIfThenElse, NoOverloadedStrings #-} {-# LANGUAGE DoAndIfThenElse, NoOverloadedStrings, TypeSynonymInstances #-}
{- | Description : Wrapper around GHC API, exposing a single `evaluate` interface that runs {- | Description : Wrapper around GHC API, exposing a single `evaluate` interface that runs
a statement, declaration, import, or directive. a statement, declaration, import, or directive.
...@@ -29,6 +30,8 @@ import Control.Monad (guard) ...@@ -29,6 +30,8 @@ import Control.Monad (guard)
import System.Process import System.Process
import System.Exit import System.Exit
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import qualified Control.Monad.IO.Class as MonadIO (MonadIO, liftIO)
import qualified MonadUtils as MonadUtils (MonadIO, liftIO)
import NameSet import NameSet
import Name import Name
...@@ -81,6 +84,9 @@ write x = when debug $ liftIO $ hPutStrLn stderr $ "DEBUG: " ++ x ...@@ -81,6 +84,9 @@ write x = when debug $ liftIO $ hPutStrLn stderr $ "DEBUG: " ++ x
type Interpreter = Ghc type Interpreter = Ghc
instance MonadIO.MonadIO Interpreter where
liftIO = MonadUtils.liftIO
globalImports :: [String] globalImports :: [String]
globalImports = globalImports =
[ "import IHaskell.Display" [ "import IHaskell.Display"
...@@ -406,8 +412,8 @@ evalCommand _ (Directive SetOpt option) state = do ...@@ -406,8 +412,8 @@ evalCommand _ (Directive SetOpt option) state = do
setOpt _ _ = Nothing setOpt _ _ = Nothing
evalCommand publish (Directive ShellCmd ('!':cmd)) state = wrapExecution state $ liftIO $ evalCommand publish (Directive ShellCmd ('!':cmd)) state = wrapExecution state $ liftIO $
case words cmd of case words cmd of
"cd":dirs -> "cd":dirs ->
let directory = unwords dirs in do let directory = unwords dirs in do
setCurrentDirectory directory setCurrentDirectory directory
...@@ -423,13 +429,13 @@ evalCommand publish (Directive ShellCmd ('!':cmd)) state = wrapExecution state $ ...@@ -423,13 +429,13 @@ evalCommand publish (Directive ShellCmd ('!':cmd)) state = wrapExecution state $
std_err = UseHandle handle std_err = UseHandle handle
} }
(_, _, _, process) <- createProcess procSpec (_, _, _, process) <- createProcess procSpec
-- Accumulate output from the process. -- Accumulate output from the process.
outputAccum <- liftIO $ newMVar "" outputAccum <- liftIO $ newMVar ""
-- Start a loop to publish intermediate results. -- Start a loop to publish intermediate results.
let let
-- Compute how long to wait between reading pieces of the output. -- Compute how long to wait between reading pieces of the output.
-- `threadDelay` takes an argument of microseconds. -- `threadDelay` takes an argument of microseconds.
ms = 1000 ms = 1000
delay = 100 * ms delay = 100 * ms
...@@ -458,7 +464,7 @@ evalCommand publish (Directive ShellCmd ('!':cmd)) state = wrapExecution state $ ...@@ -458,7 +464,7 @@ evalCommand publish (Directive ShellCmd ('!':cmd)) state = wrapExecution state $
if not computationDone if not computationDone
then do then do
-- Write to frontend and repeat. -- Write to frontend and repeat.
readMVar outputAccum >>= output readMVar outputAccum >>= output
loop loop
else do else do
out <- readMVar outputAccum out <- readMVar outputAccum
...@@ -471,7 +477,7 @@ evalCommand publish (Directive ShellCmd ('!':cmd)) state = wrapExecution state $ ...@@ -471,7 +477,7 @@ evalCommand publish (Directive ShellCmd ('!':cmd)) state = wrapExecution state $
html $ printf "<span class='mono'>%s</span>" out ++ htmlErr] html $ printf "<span class='mono'>%s</span>" out ++ htmlErr]
loop loop
-- This is taken largely from GHCi's info section in InteractiveUI. -- This is taken largely from GHCi's info section in InteractiveUI.
evalCommand _ (Directive GetHelp _) state = do evalCommand _ (Directive GetHelp _) state = do
...@@ -688,7 +694,7 @@ evalCommand _ (Declaration decl) state = wrapExecution state $ do ...@@ -688,7 +694,7 @@ evalCommand _ (Declaration decl) state = wrapExecution state $ do
names <- runDecls decl names <- runDecls decl
dflags <- getSessionDynFlags dflags <- getSessionDynFlags
let boundNames = map (replace ":Interactive." "" . showPpr dflags) names let boundNames = map (replace ":Interactive." "" . showPpr dflags) names
nonDataNames = filter (not . isUpper . head) boundNames nonDataNames = filter (not . isUpper . head) boundNames
-- Display the types of all bound names if the option is on. -- Display the types of all bound names if the option is on.
...@@ -982,7 +988,7 @@ formatType :: String -> [DisplayData] ...@@ -982,7 +988,7 @@ formatType :: String -> [DisplayData]
formatType typeStr = [plain typeStr, html $ formatGetType typeStr] formatType typeStr = [plain typeStr, html $ formatGetType typeStr]
displayError :: ErrMsg -> [DisplayData] displayError :: ErrMsg -> [DisplayData]
displayError msg = [plain . fixStdinError . typeCleaner $ msg, html $ formatError msg] displayError msg = [plain . fixStdinError . typeCleaner $ msg, html $ formatError msg]
fixStdinError :: ErrMsg -> ErrMsg fixStdinError :: ErrMsg -> ErrMsg
fixStdinError err = fixStdinError err =
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment