packages feed

hls-cabal-plugin-2.1.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 qualified Data.List                                      as List
import           Data.Maybe                                     (fromMaybe)
import qualified Data.Text                                      as T
import           Distribution.PackageDescription                (Benchmark (..),
                                                                 BuildInfo (..),
                                                                 CondTree (condTreeData),
                                                                 Executable (..),
                                                                 GenericPackageDescription (..),
                                                                 Library (..),
                                                                 UnqualComponentName,
                                                                 mkUnqualComponentName,
                                                                 testBuildInfo)
import           Distribution.Utils.Path                        (getSymbolicPath)
import           Ide.Logger                                     (Priority (..),
                                                                 Recorder,
                                                                 WithPriority,
                                                                 logWith)
import           Ide.Plugin.Cabal.Completion.Completer.FilePath (PathCompletionInfo (..),
                                                                 listFileCompletions,
                                                                 mkCompletionDirectory)
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

-- | Extracts the source directories of the library stanza.
sourceDirsExtractionLibrary :: Maybe StanzaName -> GenericPackageDescription -> [FilePath]
sourceDirsExtractionLibrary Nothing gpd =
  -- we use condLibrary to get the information contained in the library stanza
  -- since the library in PackageDescription is not populated by us
  case libM of
    Just lib -> do
      map getSymbolicPath $ hsSourceDirs $ libBuildInfo $ condTreeData lib
    Nothing -> []
  where
    libM = condLibrary gpd
sourceDirsExtractionLibrary name gpd = extractRelativeDirsFromStanza name gpd condSubLibraries libBuildInfo

-- | Extracts the source directories of the executable stanza with the given name.
sourceDirsExtractionExecutable :: Maybe StanzaName -> GenericPackageDescription -> [FilePath]
sourceDirsExtractionExecutable name gpd = extractRelativeDirsFromStanza name gpd condExecutables buildInfo

-- | Extracts the source directories of the test suite stanza with the given name.
sourceDirsExtractionTestSuite :: Maybe StanzaName -> GenericPackageDescription -> [FilePath]
sourceDirsExtractionTestSuite name gpd = extractRelativeDirsFromStanza name gpd condTestSuites testBuildInfo

-- | Extracts the source directories of benchmark stanza with the given name.
sourceDirsExtractionBenchmark :: Maybe StanzaName -> GenericPackageDescription -> [FilePath]
sourceDirsExtractionBenchmark name gpd = extractRelativeDirsFromStanza name gpd condBenchmarks benchmarkBuildInfo

-- | Takes a possible stanza name, a GenericPackageDescription,
--  a function to access the stanza information we are interested in
--  and a function to access the build info from the specific stanza.
--
--  Returns a list of relative source directory paths specified for the extracted stanza.
extractRelativeDirsFromStanza ::
  Maybe StanzaName ->
  GenericPackageDescription ->
  (GenericPackageDescription -> [(UnqualComponentName, CondTree b c a)]) ->
  (a -> BuildInfo) ->
  [FilePath]
extractRelativeDirsFromStanza Nothing _ _ _ = []
extractRelativeDirsFromStanza (Just name) gpd getStanza getBuildInfo
  | Just stanza <- stanzaM = map getSymbolicPath $ hsSourceDirs $ getBuildInfo stanza
  | otherwise = []
  where
    stanzaM = fmap (condTreeData . snd) res
    allStanzasM = getStanza gpd
    res =
      List.find
        ( \(n, _) ->
            n == mkUnqualComponentName (T.unpack name)
        )
        allStanzasM

-- | 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'
        let pInfo =
              PathCompletionInfo
                { isStringNotationPath = Nothing,
                  pathSegment = T.pack $ FP.takeFileName prefix,
                  queryDirectory = FP.addTrailingPathSeparator $ FP.takeDirectory prefix,
                  workingDirectory = completionWorkingDir prefInfo FP.</> dir
                }
        completions <- listFileCompletions recorder pInfo
        validExposedCompletions <- filterM (isValidExposedModulePath pInfo) completions
        let toMatch = pathSegment pInfo
            scored = Fuzzy.simpleFilter Fuzzy.defChunkSize Fuzzy.defMaxResults toMatch (map T.pack validExposedCompletions)
        forM
          scored
          ( \compl' -> do
              let compl = Fuzzy.original compl'
              fullFilePath <- mkExposedModulePathCompletion pInfo $ T.unpack compl
              pure fullFilePath
          )
    )
  where
    prefix =
      exposedModulePathToFp $
        completionPrefix prefInfo
    -- \| 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