packages feed

haskell-language-server-2.9.0.0: plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/Completer/Paths.hs

module Ide.Plugin.Cabal.Completion.Completer.Paths where

import qualified Data.List                         as List
import           Data.List.Extra                   (dropPrefix)
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.Plugin.Cabal.Completion.Types
import qualified System.FilePath                   as FP
import qualified System.FilePath.Posix             as Posix


{- | Information used to query and build path completions.

  Note that pathSegment  combined with queryDirectory  results in
  the original prefix.

  Example:
  When given the written prefix, @dir1\/dir2\/fi@, the
  resulting PathCompletionInfo would be:

  @
    pathSegment = "fi"
    queryDirectory  = "dir1\/dir2\/fi"
    ...
  @
-}
data PathCompletionInfo = PathCompletionInfo
  { pathSegment          :: T.Text,
    -- ^ Partly written segment of the next part of the path.
    queryDirectory       :: FilePath,
    -- ^ Written part of path, in posix format.
    workingDirectory     :: FilePath,
    -- ^ Directory relative to which relative paths are interpreted, platform dependent.
    isStringNotationPath :: Maybe Apostrophe
    -- ^ Did the completion happen in the context of a string notation,
    -- if yes, contains the state of the string notation.
  }
  deriving (Eq, Show)


{- | Posix.splitFileName modification, that drops trailing ./ if
  if wasn't present in the original path.

  Fix for the issue #3774
  Examples:

  >>> splitFileNameNoTrailingSlash ""
  ("", "")
  >>> splitFileNameNoTrailingSlash "./"
  ("./", "")
  >>> splitFileNameNoTrailingSlash "dir"
  ("", "dir")
  >>> splitFileNameNoTrailingSlash "./dir"
  ("./", "dir")
  >>> splitFileNameNoTrailingSlash "dir1/dir2"
  ("dir1/","dir2")
  >>> splitFileNameNoTrailingSlash "./dir1/dir2"
  ("./dir1/","dir2")
-}
splitFileNameNoTrailingSlash :: FilePath -> (String, String)
splitFileNameNoTrailingSlash prefix = rmTrailingSlash ("./" `List.isPrefixOf` prefix) (Posix.splitFileName prefix)
  where rmTrailingSlash hadTrailingSlash (queryDirectory', pathSegment')
                    | hadTrailingSlash = (queryDirectory', pathSegment')
                    | otherwise        = ("./" `dropPrefix` queryDirectory', pathSegment')

{- | Takes an optional source subdirectory and a prefix info
  and creates a path completion info accordingly.

  The source directory represents some subdirectory of the working directory such as a
  path from the field @hs-source-dirs@.

  If the source subdirectory is empty, then the working directory is simply set to
  the currently handled cabal file's directory.
-}
pathCompletionInfoFromCabalPrefixInfo :: FilePath -> CabalPrefixInfo -> PathCompletionInfo
pathCompletionInfoFromCabalPrefixInfo srcDir prefInfo =
  PathCompletionInfo
    { pathSegment = T.pack pathSegment',
      queryDirectory = queryDirectory',
      workingDirectory = completionWorkingDir prefInfo FP.</> srcDir,
      isStringNotationPath = isStringNotation prefInfo
    }
  where
    prefix = T.unpack $ completionPrefix prefInfo
    (queryDirectory', pathSegment') = splitFileNameNoTrailingSlash prefix

-- | 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