Skip to content

Commit

Permalink
Fix main-is completion not being relative to source dirs
Browse files Browse the repository at this point in the history
  • Loading branch information
VeryMilkyJoe committed Aug 21, 2023
1 parent af6387c commit 379d385
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 145 deletions.
1 change: 1 addition & 0 deletions plugins/hls-cabal-plugin/hls-cabal-plugin.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ library
Ide.Plugin.Cabal.Diagnostics
Ide.Plugin.Cabal.Completion.Completer.FilePath
Ide.Plugin.Cabal.Completion.Completer.Module
Ide.Plugin.Cabal.Completion.Completer.Paths
Ide.Plugin.Cabal.Completion.Completer.Simple
Ide.Plugin.Cabal.Completion.Completer.Snippet
Ide.Plugin.Cabal.Completion.Completer.Types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ module Ide.Plugin.Cabal.Completion.Completer.FilePath where

import Control.Exception (evaluate, try)
import Control.Monad (filterM)
import Control.Monad.Extra (forM)
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
Expand All @@ -23,7 +25,7 @@ import qualified Text.Fuzzy.Parallel as Fuzzy
filePathCompleter :: Completer
filePathCompleter recorder cData = do
let prefInfo = cabalPrefixInfo cData
complInfo = pathCompletionInfoFromCabalPrefixInfo prefInfo
complInfo = pathCompletionInfoFromCabalPrefixInfo "" prefInfo
filePathCompletions <- listFileCompletions recorder complInfo
let scored =
Fuzzy.simpleFilter
Expand All @@ -39,12 +41,44 @@ filePathCompleter recorder cData = do
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
complInfo = pathCompletionInfoFromCabalPrefixInfo "" prefInfo
directoryCompletions <- listDirectoryCompletions recorder complInfo
let scored =
Fuzzy.simpleFilter
Expand Down Expand Up @@ -73,33 +107,6 @@ directoryCompleter recorder cData = do
be used for file path completions to be written to the cabal file.
-}

-- | 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
{ -- | partly written segment of the next part of the path
pathSegment :: T.Text,
-- | written part of path, platform dependent
queryDirectory :: FilePath,
-- | directory relative to which relative paths are interpreted, platform dependent
workingDirectory :: FilePath,
-- | Did the completion happen in the context of a string notation,
-- if yes, contains the state of the string notation
isStringNotationPath :: Maybe Apostrophe
}
deriving (Eq, Show)

-- | Takes a PathCompletionInfo and returns the list of files and directories
-- in the directory which match the path completion info in posix style.
--
Expand All @@ -126,18 +133,6 @@ listDirectoryCompletions recorder complInfo = do
filepaths <- listFileCompletions recorder complInfo
filterM (doesDirectoryExist . mkDirFromCWD complInfo) filepaths

pathCompletionInfoFromCabalPrefixInfo :: CabalPrefixInfo -> PathCompletionInfo
pathCompletionInfoFromCabalPrefixInfo ctx =
PathCompletionInfo
{ pathSegment = T.pack pathSegment',
queryDirectory = queryDirectory',
workingDirectory = completionWorkingDir ctx,
isStringNotationPath = isStringNotation ctx
}
where
prefix = T.unpack $ completionPrefix ctx
(queryDirectory', pathSegment') = Posix.splitFileName prefix

-- | Returns the directory where files and directories can be queried from
-- for the passed PathCompletionInfo.
--
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,16 @@ 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 Distribution.PackageDescription (GenericPackageDescription)
import Ide.Logger (Priority (..),
Recorder,
WithPriority,
logWith)
import Ide.Plugin.Cabal.Completion.Completer.FilePath (PathCompletionInfo (..),
listFileCompletions,
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
Expand Down Expand Up @@ -53,56 +43,6 @@ modulesCompleter extractionFunction recorder cData = do
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]
Expand All @@ -111,34 +51,36 @@ filePathsForExposedModules recorder srcDirs prefInfo = do
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)
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 pInfo $ T.unpack compl
fullFilePath <- mkExposedModulePathCompletion pathInfo $ T.unpack compl
pure fullFilePath
)
)
where
prefix =
exposedModulePathToFp $
T.pack $ exposedModulePathToFp $
completionPrefix prefInfo
-- \| Takes a PathCompletionInfo and a path segment and checks whether
-- 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
module Ide.Plugin.Cabal.Completion.Completer.Paths where

import qualified Data.List as List
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
{ -- | partly written segment of the next part of the path
pathSegment :: T.Text,
-- | written part of path, platform dependent
queryDirectory :: FilePath,
-- | directory relative to which relative paths are interpreted, platform dependent
workingDirectory :: FilePath,
-- | Did the completion happen in the context of a string notation,
-- if yes, contains the state of the string notation
isStringNotationPath :: Maybe Apostrophe
}
deriving (Eq, Show)

pathCompletionInfoFromCabalPrefixInfo :: FilePath -> CabalPrefixInfo -> PathCompletionInfo
pathCompletionInfoFromCabalPrefixInfo fp prefInfo =
PathCompletionInfo
{ pathSegment = T.pack pathSegment',
queryDirectory = queryDirectory',
workingDirectory = completionWorkingDir prefInfo FP.</> fp,
isStringNotationPath = isStringNotation prefInfo
}
where
prefix = T.unpack $ completionPrefix prefInfo
(queryDirectory', pathSegment') = Posix.splitFileName 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
Loading

0 comments on commit 379d385

Please sign in to comment.