hls-cabal-plugin-2.2.0.0: src/Ide/Plugin/Cabal/Completion/Completer/Module.hs
{-# LANGUAGE OverloadedStrings #-}
module Ide.Plugin.Cabal.Completion.Completer.Module where
import Control.Monad (filterM)
import Control.Monad.Extra (concatForM,
forM)
import Data.List (stripPrefix)
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import Distribution.PackageDescription (GenericPackageDescription)
import Ide.Logger (Priority (..),
Recorder,
WithPriority,
logWith)
import Ide.Plugin.Cabal.Completion.Completer.FilePath (listFileCompletions,
mkCompletionDirectory)
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 (doesFileExist)
import qualified System.FilePath as FP
import qualified Text.Fuzzy.Parallel as Fuzzy
-- | Completer to be used when module paths can be completed for the field.
--
-- Takes an extraction function which extracts the source directories
-- to be used by the completer.
modulesCompleter :: (Maybe StanzaName -> GenericPackageDescription -> [FilePath]) -> Completer
modulesCompleter extractionFunction recorder cData = do
mGPD <- getLatestGPD cData
case mGPD of
Just gpd -> do
let sourceDirs = extractionFunction sName gpd
filePathCompletions <-
filePathsForExposedModules recorder sourceDirs prefInfo
pure $ map (\compl -> mkSimpleCompletionItem (completionRange prefInfo) compl) filePathCompletions
Nothing -> do
logWith recorder Debug LogUseWithStaleFastNoResult
pure []
where
sName = stanzaName cData
prefInfo = cabalPrefixInfo cData
-- | Takes a list of source directories and returns a list of path completions
-- relative to any of the passed source directories which fit the passed prefix info.
filePathsForExposedModules :: Recorder (WithPriority Log) -> [FilePath] -> CabalPrefixInfo -> IO [T.Text]
filePathsForExposedModules recorder srcDirs prefInfo = do
concatForM
srcDirs
( \dir' -> do
let dir = FP.normalise dir'
pathInfo = pathCompletionInfoFromCabalPrefixInfo dir modPrefInfo
completions <- listFileCompletions recorder pathInfo
validExposedCompletions <- filterM (isValidExposedModulePath pathInfo) completions
let toMatch = pathSegment pathInfo
scored = Fuzzy.simpleFilter
Fuzzy.defChunkSize
Fuzzy.defMaxResults
toMatch
(map T.pack validExposedCompletions)
forM
scored
( \compl' -> do
let compl = Fuzzy.original compl'
fullFilePath <- mkExposedModulePathCompletion pathInfo $ T.unpack compl
pure fullFilePath
)
)
where
prefix =
T.pack $ exposedModulePathToFp $
completionPrefix prefInfo
-- build completion info relative to the source dir,
-- we overwrite the prefix written in the cabal file with its translation
-- to filepath syntax, since it is in exposed module syntax
modPrefInfo = prefInfo{completionPrefix=prefix}
-- Takes a PathCompletionInfo and a path segment and checks whether
-- the path segment can be completed for an exposed module.
--
-- This is the case if the segment represents either a directory or a Haskell file.
isValidExposedModulePath :: PathCompletionInfo -> FilePath -> IO Bool
isValidExposedModulePath pInfo path = do
let dir = mkCompletionDirectory pInfo
fileExists <- doesFileExist (dir FP.</> path)
pure $ not fileExists || FP.takeExtension path `elem` [".hs", ".lhs"]
-- | Takes a pathCompletionInfo and a path segment and generates the whole
-- filepath to be written on completion including a possibly already written prefix;
-- using the cabal syntax for exposed modules.
--
-- Examples:
-- When the partial directory path `Dir.Dir2.` is stored in the PathCompletionInfo
-- and the completed file `HaskellFile.hs` is passed along with that PathCompletionInfo,
-- the result would be `Dir1.Dir2.HaskellFile`
--
-- When the partial directory path `Dir.` is stored in the PathCompletionInfo
-- and the completed directory `Dir2` is passed along with that PathCompletionInfo,
-- the result would be `Dir1.Dir2.`
mkExposedModulePathCompletion :: PathCompletionInfo -> FilePath -> IO T.Text
mkExposedModulePathCompletion complInfo completion = do
let combinedPath = queryDirectory complInfo FP.</> completion
isFilePath <- doesFileExist (workingDirectory complInfo FP.</> combinedPath)
let addTrailingDot modPath = if isFilePath then modPath else modPath <> "."
let exposedPath = FP.makeRelative "." combinedPath
pure $ addTrailingDot $ fpToExposedModulePath "" exposedPath
-- | Takes a source directory path and a module path and returns
-- the module path relative to the source directory
-- in exposed module syntax where the separators are '.'
-- and the file ending is removed.
--
-- Synopsis: @'fpToExposedModulePath' sourceDir modPath@.
fpToExposedModulePath :: FilePath -> FilePath -> T.Text
fpToExposedModulePath sourceDir modPath =
T.intercalate "." $ fmap T.pack $ FP.splitDirectories $ FP.dropExtension fp
where
fp = fromMaybe modPath $ stripPrefix sourceDir modPath
-- | Takes a path in the exposed module syntax and translates it to a platform-compatible file path.
exposedModulePathToFp :: T.Text -> FilePath
exposedModulePathToFp fp = T.unpack $ T.replace "." (T.singleton FP.pathSeparator) fp