hls-cabal-plugin-2.2.0.0: src/Ide/Plugin/Cabal/Completion/Completer/FilePath.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Ide.Plugin.Cabal.Completion.Completer.FilePath where
import Control.Exception (evaluate, try)
import Control.Monad (filterM)
import Control.Monad.Extra (concatForM, forM)
import qualified Data.Text as T
import Distribution.PackageDescription (GenericPackageDescription)
import Ide.Logger
import Ide.Plugin.Cabal.Completion.Completer.Paths
import Ide.Plugin.Cabal.Completion.Completer.Simple
import Ide.Plugin.Cabal.Completion.Completer.Types
import Ide.Plugin.Cabal.Completion.Types
import System.Directory (doesDirectoryExist,
doesFileExist,
listDirectory)
import qualified System.FilePath as FP
import qualified System.FilePath.Posix as Posix
import qualified Text.Fuzzy.Parallel as Fuzzy
-- | Completer to be used when a file path can be completed for a field.
-- Completes file paths as well as directories.
filePathCompleter :: Completer
filePathCompleter recorder cData = do
let prefInfo = cabalPrefixInfo cData
complInfo = pathCompletionInfoFromCabalPrefixInfo "" prefInfo
filePathCompletions <- listFileCompletions recorder complInfo
let scored =
Fuzzy.simpleFilter
Fuzzy.defChunkSize
Fuzzy.defMaxResults
(pathSegment complInfo)
(map T.pack filePathCompletions)
forM
scored
( \compl' -> do
let compl = Fuzzy.original compl'
fullFilePath <- mkFilePathCompletion complInfo compl
pure $ mkCompletionItem (completionRange prefInfo) fullFilePath fullFilePath
)
mainIsCompleter :: (Maybe StanzaName -> GenericPackageDescription -> [FilePath]) -> Completer
mainIsCompleter extractionFunction recorder cData = do
mGPD <- getLatestGPD cData
case mGPD of
Just gpd -> do
let srcDirs = extractionFunction sName gpd
concatForM srcDirs
(\dir' -> do
let dir = FP.normalise dir'
let pathInfo = pathCompletionInfoFromCabalPrefixInfo dir prefInfo
completions <- listFileCompletions recorder pathInfo
let scored = Fuzzy.simpleFilter
Fuzzy.defChunkSize
Fuzzy.defMaxResults
(pathSegment pathInfo)
(map T.pack completions)
forM
scored
( \compl' -> do
let compl = Fuzzy.original compl'
fullFilePath <- mkFilePathCompletion pathInfo compl
pure $ mkCompletionItem (completionRange prefInfo) fullFilePath fullFilePath
)
)
Nothing -> do
logWith recorder Debug LogUseWithStaleFastNoResult
pure []
where
sName = stanzaName cData
prefInfo = cabalPrefixInfo cData
-- | Completer to be used when a directory can be completed for the field.
-- Only completes directories.
directoryCompleter :: Completer
directoryCompleter recorder cData = do
let prefInfo = cabalPrefixInfo cData
complInfo = pathCompletionInfoFromCabalPrefixInfo "" prefInfo
directoryCompletions <- listDirectoryCompletions recorder complInfo
let scored =
Fuzzy.simpleFilter
Fuzzy.defChunkSize
Fuzzy.defMaxResults
(pathSegment complInfo)
(map T.pack directoryCompletions)
forM
scored
( \compl' -> do
let compl = Fuzzy.original compl'
let fullDirPath = mkPathCompletionDir complInfo compl
pure $ mkCompletionItem (completionRange prefInfo) fullDirPath fullDirPath
)
{- Note [Using correct file path separators]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Since cabal files only allow for posix style file paths
we need to be careful to use the correct path separators
whenever we work with file paths in cabal files.
Thus we are using two different kinds of imports.
We use "FP" for platform-compatible file paths with which
we can query files independently of the platform.
We use "Posix" for the posix syntax paths which need to
be used for file path completions to be written to the cabal file.
-}
-- | Takes a PathCompletionInfo and returns the list of files and directories
-- in the directory which match the path completion info in posix style.
--
-- The directories end with a posix trailing path separator.
-- Since this is used for completions to be written to the cabal file,
-- we use posix separators here.
-- See Note [Using correct file path separators].
listFileCompletions :: Recorder (WithPriority Log) -> PathCompletionInfo -> IO [FilePath]
listFileCompletions recorder complInfo = do
let complDir = mkCompletionDirectory complInfo
try (evaluate =<< listDirectory complDir) >>= \case
Right dirs -> do
forM dirs $ \d -> do
isDir <- doesDirectoryExist $ mkDirFromCWD complInfo d
pure $ if isDir then Posix.addTrailingPathSeparator d else d
Left (err :: IOError) -> do
logWith recorder Warning $ LogFilePathCompleterIOError complDir err
pure []
-- | Returns a list of all (and only) directories in the
-- directory described by path completion info.
listDirectoryCompletions :: Recorder (WithPriority Log) -> PathCompletionInfo -> IO [FilePath]
listDirectoryCompletions recorder complInfo = do
filepaths <- listFileCompletions recorder complInfo
filterM (doesDirectoryExist . mkDirFromCWD complInfo) filepaths
-- | Returns the directory where files and directories can be queried from
-- for the passed PathCompletionInfo.
--
-- Returns the full path to the directory pointed to by the path prefix
-- by combining it with the working directory.
--
-- Since this is used for querying paths we use platform
-- compatible separators here.
-- See Note [Using correct file path separators].
mkCompletionDirectory :: PathCompletionInfo -> FilePath
mkCompletionDirectory complInfo =
FP.addTrailingPathSeparator $
workingDirectory complInfo FP.</> (FP.normalise $ queryDirectory complInfo)
-- | Returns the full path for the given path segment
-- by combining the working directory with the path prefix
-- and the path segment.
--
-- Since this is used for querying paths we use platform
-- compatible separators here.
-- See Note [Using correct file path separators].
mkDirFromCWD :: PathCompletionInfo -> FilePath -> FilePath
mkDirFromCWD complInfo fp =
FP.addTrailingPathSeparator $
mkCompletionDirectory complInfo FP.</> FP.normalise fp
-- | Takes a PathCompletionInfo and a directory and
-- returns the complete cabal path to be written on completion action
-- by combining the previously written path prefix and the completed
-- path segment.
--
-- Since this is used for completions we use posix separators here.
-- See Note [Using correct file path separators].
mkPathCompletionDir :: PathCompletionInfo -> T.Text -> T.Text
mkPathCompletionDir complInfo completion =
T.pack $
queryDirectory complInfo Posix.</> T.unpack completion
-- | Takes a PathCompletionInfo and a completed path segment and
-- generates the whole filepath to be completed.
--
-- The returned text combines the completion with a relative path
-- generated from a possible previously written path prefix and
-- is relative to the cabal file location.
--
-- If the completion results in a filepath, we know this is a
-- completed path and can thus apply wrapping of apostrophes if needed.
mkFilePathCompletion :: PathCompletionInfo -> T.Text -> IO T.Text
mkFilePathCompletion complInfo completion = do
let combinedPath = mkPathCompletionDir complInfo completion
isFilePath <- doesFileExist $ T.unpack combinedPath
let completedPath = if isFilePath then applyStringNotation (isStringNotationPath complInfo) combinedPath else combinedPath
pure completedPath