Skip to content

Commit 4df79fc

Browse files
committed
Implement tests and functionality for filepath completion
Write function which parses the filepath occuring before the cursor in the cabal file
1 parent 2c325fb commit 4df79fc

File tree

4 files changed

+170
-108
lines changed

4 files changed

+170
-108
lines changed

plugins/hls-cabal-plugin/hls-cabal-plugin.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ library
4949
, containers
5050
, deepseq
5151
, directory
52+
, filepath
5253
, extra >=1.7.4
5354
, ghcide == 2.0.0.0
5455
, hashable
@@ -80,6 +81,7 @@ test-suite tests
8081
, hls-cabal-plugin
8182
, hls-test-utils == 2.0.0.0
8283
, lens
84+
, lsp
8385
, lsp-types
8486
, tasty-hunit
8587
, text

plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,35 @@ import Control.Concurrent.Strict
1616
import Control.DeepSeq
1717
import Control.Monad.Extra
1818
import Control.Monad.IO.Class
19-
import qualified Data.ByteString as BS
19+
import qualified Data.ByteString as BS
2020
import Data.Hashable
21-
import Data.HashMap.Strict (HashMap)
22-
import qualified Data.HashMap.Strict as HashMap
23-
import qualified Data.List.NonEmpty as NE
24-
import qualified Data.Text.Encoding as Encoding
25-
import qualified Data.Text.Utf16.Rope as Rope
21+
import Data.HashMap.Strict (HashMap)
22+
import qualified Data.HashMap.Strict as HashMap
23+
import qualified Data.List.NonEmpty as NE
24+
import qualified Data.Text.Encoding as Encoding
25+
import qualified Data.Text.Utf16.Rope as Rope
2626
import Data.Typeable
27-
import Development.IDE as D
28-
import Development.IDE.Core.Shake (restartShakeSession)
29-
import qualified Development.IDE.Core.Shake as Shake
30-
import Development.IDE.Graph (alwaysRerun)
31-
import Distribution.Compat.Lens ((^.))
27+
import Development.IDE as D
28+
import Development.IDE.Core.Shake (restartShakeSession)
29+
import qualified Development.IDE.Core.Shake as Shake
30+
import Development.IDE.Graph (alwaysRerun)
31+
import Distribution.Compat.Lens ((^.))
32+
import Distribution.Simple.PackageDescription (readGenericPackageDescription)
33+
import Distribution.Verbosity (silent)
3234
import GHC.Generics
3335
import Ide.Plugin.Cabal.Completions
34-
import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
35-
import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
36-
import qualified Ide.Plugin.Cabal.Parse as Parse
36+
import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
37+
import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
38+
import qualified Ide.Plugin.Cabal.Parse as Parse
3739
import Ide.Types
38-
import qualified Language.LSP.Server as LSP
40+
import qualified Language.LSP.Server as LSP
3941
import Language.LSP.Types
40-
import qualified Language.LSP.Types as J
41-
import qualified Language.LSP.Types as LSP
42-
import qualified Language.LSP.Types.Lens as JL
43-
import Language.LSP.VFS (VirtualFile)
44-
import qualified Language.LSP.VFS as VFS
42+
import qualified Language.LSP.Types as J
43+
import qualified Language.LSP.Types as LSP
44+
import qualified Language.LSP.Types.Lens as JL
45+
import Language.LSP.VFS (VirtualFile)
46+
import qualified Language.LSP.VFS as VFS
47+
import Debug.Trace (traceShowId)
4548

4649

4750
data Log
@@ -273,18 +276,21 @@ completion _ide _ complParams = do
273276
position = complParams ^. JL.position
274277
contents <- LSP.getVirtualFile $ toNormalizedUri uri
275278
fmap (Right . J.InL) $ case (contents, uriToFilePath' uri) of
276-
(Just cnts, Just _path) -> do
279+
(Just cnts, Just path) -> do
277280
pref <- VFS.getCompletionPrefix position cnts
278-
liftIO $ result pref cnts
281+
liftIO $ result pref path cnts
279282
_ -> return $ J.List []
280283
where
281-
result :: Maybe VFS.PosPrefixInfo -> VirtualFile -> IO (J.List CompletionItem)
282-
result Nothing _ = pure $ J.List []
283-
result (Just pfix) cnts
284+
result :: Maybe VFS.PosPrefixInfo -> FilePath -> VirtualFile -> IO (J.List CompletionItem)
285+
result Nothing _ _ = pure $ J.List []
286+
result (Just pfix) fp cnts
284287
| Just ctx <- context = do
285-
completions <- contextToCompleter ctx
286-
pure $ J.List $ makeCompletionItems pfix completions
288+
let completer = contextToCompleter "" ctx
289+
completions <- completer $ VFS.prefixText $ (traceShowId pfix)
290+
genPkgDesc <- readGenericPackageDescription silent fp
291+
pure $ J.List $ makeCompletionItems pfix genPkgDesc completions
287292
| otherwise = pure $ J.List []
288293
where
289294
pos = VFS.cursorPos pfix
290295
context = getContext pos (Rope.lines $ cnts ^. VFS.file_text)
296+

plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completions.hs

Lines changed: 82 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,29 @@
22

33
module Ide.Plugin.Cabal.Completions where
44

5+
import Control.Monad (filterM)
56
import qualified Data.List as List
67
import qualified Data.List.Extra as Extra
78
import Data.Map (Map)
89
import qualified Data.Map as Map
910
import qualified Data.Text as T
1011
import Development.IDE as D
12+
import Distribution.CabalSpecVersion (CabalSpecVersion (CabalSpecV1_2),
13+
showCabalSpecVersion)
1114
import Distribution.Compat.Lens ((^.))
15+
import Distribution.PackageDescription (GenericPackageDescription)
1216
import Language.LSP.Types
1317
import qualified Language.LSP.Types as J
1418
import qualified Language.LSP.Types.Lens as JL
1519
import qualified Language.LSP.VFS as VFS
1620
import System.Console.Haskeline.Completion as Haskeline
21+
import System.Directory (doesDirectoryExist)
22+
import System.FilePath
1723
import qualified Text.Fuzzy.Parallel as Fuzzy
1824

1925

20-
type Completer = IO [T.Text]
26+
27+
type Completer = T.Text -> IO [T.Text]
2128

2229
-- | The context a cursor can be in within a cabal file,
2330
-- we can be in stanzas or the top level,
@@ -45,72 +52,50 @@ data KeyWordContext
4552
-- right before the current position
4653
deriving (Eq, Show)
4754

48-
49-
-- | Describes the line at the current cursor position
50-
data PosPrefixInfo = PosPrefixInfo
51-
{ fullLine :: !T.Text
52-
-- ^ The full contents of the line the cursor is at
53-
54-
, prefixScope :: !T.Text
55-
-- ^ If any, the module name that was typed right before the cursor position.
56-
-- For example, if the user has typed "Data.Maybe.from", then this property
57-
-- will be "Data.Maybe"
58-
-- If OverloadedRecordDot is enabled, "Shape.rect.width" will be
59-
-- "Shape.rect"
60-
61-
, prefixText :: !T.Text
62-
-- ^ The word right before the cursor position, after removing the module part.
63-
-- For example if the user has typed "Data.Maybe.from",
64-
-- then this property will be "from"
65-
, cursorPos :: !J.Position
66-
-- ^ The cursor position
67-
} deriving (Show,Eq)
68-
6955
-- ----------------------------------------------------------------
7056
-- Public API for Completions
7157
-- ----------------------------------------------------------------
7258

73-
contextToCompleter :: Context -> Completer
59+
contextToCompleter :: T.Text ->Context -> Completer
7460
-- if we are in the top level of the cabal file and not in a keyword context,
7561
-- we can write any top level keywords or a stanza declaration
76-
contextToCompleter (TopLevel, None) =
77-
pure $ Map.keys cabalKeywords ++ Map.keys stanzaKeywordMap
62+
contextToCompleter dir (TopLevel, None) =
63+
constantCompleter $ Map.keys (cabalKeywords dir) ++ Map.keys stanzaKeywordMap
7864
-- if we are in a keyword context in the top level,
7965
-- we look up that keyword in the toplevel context and can complete its possible values
80-
contextToCompleter (TopLevel, KeyWord kw) =
81-
case Map.lookup kw cabalKeywords of
66+
contextToCompleter dir (TopLevel, KeyWord kw) =
67+
case Map.lookup kw (cabalKeywords dir) of
8268
Nothing -> noopCompleter
8369
Just l -> l
8470
-- if we are in a stanza and not in a keyword context,
8571
-- we can write any of the stanza's keywords or a stanza declaration
86-
contextToCompleter (Stanza s, None) =
72+
contextToCompleter _dir (Stanza s, None) =
8773
case Map.lookup s stanzaKeywordMap of
8874
Nothing -> noopCompleter
89-
Just l -> pure $ Map.keys l ++ Map.keys stanzaKeywordMap
75+
Just l -> constantCompleter $ Map.keys l ++ Map.keys stanzaKeywordMap
9076
-- if we are in a stanza's keyword's context we can complete possible values of that keyword
91-
contextToCompleter (Stanza s, KeyWord kw) =
77+
contextToCompleter _dir (Stanza s, KeyWord kw) =
9278
case Map.lookup s stanzaKeywordMap of
9379
Nothing -> noopCompleter
9480
Just m -> case Map.lookup kw m of
9581
Nothing -> noopCompleter
9682
Just l -> l
9783

98-
-- | Takes info about the current cursor position and a set of possible keywords
84+
-- | Takes info about the current cursor position, information
85+
-- about the handled cabal file and a set of possible keywords
9986
-- and creates completion suggestions that fit the current input from the given list
100-
makeCompletionItems :: VFS.PosPrefixInfo -> [T.Text] -> [CompletionItem]
101-
makeCompletionItems pfix l =
102-
map
103-
(buildCompletion . Fuzzy.original)
104-
(Fuzzy.simpleFilter 1000 10 (VFS.prefixText pfix) l)
87+
makeCompletionItems :: VFS.PosPrefixInfo -> GenericPackageDescription -> [T.Text] -> [CompletionItem]
88+
makeCompletionItems _pfix _pkgDesc l = map buildCompletion l
10589

10690
-- | Takes a position and a list of lines (representing a file)
10791
-- and returns the context of the current position
10892
-- can return Nothing if an error occurs
93+
-- TODO: first line can only have cabal-version: keyword
10994
getContext :: Position -> [T.Text] -> Maybe Context
11095
getContext pos ls =
11196
case lvlContext of
11297
TopLevel -> do
113-
kwContext <- getKeyWordContext pos ls (uncurry Map.insert cabalVersionKeyword cabalKeywords)
98+
kwContext <- getKeyWordContext pos ls (cabalVersionKeyword <> cabalKeywords "")
11499
pure (TopLevel, kwContext)
115100
Stanza s ->
116101
case Map.lookup s stanzaKeywordMap of
@@ -169,6 +154,20 @@ getPreviousLines pos ls = reverse $ take (fromIntegral currentLine) ls
169154
where
170155
currentLine = pos ^. JL.line
171156

157+
-- | Takes information about the current cursor position in the file
158+
-- and returns the filepath up to that cursor position
159+
getFilePathCursorPrefix :: VFS.PosPrefixInfo -> T.Text
160+
getFilePathCursorPrefix pfixInfo =
161+
T.takeWhileEnd (not . (`elem` stopConditionChars)) lineText
162+
where
163+
lineText = T.take cursorColumn $ VFS.fullLine pfixInfo
164+
cursorColumn = fromIntegral $ VFS.cursorPos pfixInfo ^. JL.character
165+
-- if the filepath is inside apostrophes, we parse until the apostrophe,
166+
-- otherwise space is a separator
167+
apostropheOrSpaceSeparator = if T.count "\"" lineText `mod` 2 == 1 then '\"' else ' '
168+
stopConditionChars = apostropheOrSpaceSeparator : [',']
169+
170+
172171
buildCompletion :: T.Text -> J.CompletionItem
173172
buildCompletion label =
174173
J.CompletionItem label (Just J.CiKeyword) Nothing Nothing
@@ -180,39 +179,51 @@ buildCompletion label =
180179
-- ----------------------------------------------------------------
181180

182181
noopCompleter :: Completer
183-
noopCompleter = pure []
182+
noopCompleter _ = pure []
184183

185184
constantCompleter :: [T.Text] -> Completer
186-
constantCompleter = pure
185+
constantCompleter completions pfix = do
186+
let scored = Fuzzy.simpleFilter 1000 10 pfix completions
187+
pure $ map Fuzzy.original scored
187188

189+
-- | returns all possible files and directories reachable
190+
-- from the given filepath
188191
filePathCompleter :: FilePath -> Completer
189-
filePathCompleter fp = do
190-
completions <- Haskeline.listFiles fp
192+
filePathCompleter fp pfix = do
193+
completions <- Haskeline.listFiles (fp </> T.unpack pfix)
191194
pure $ map (T.pack . Haskeline.replacement) completions
192195

196+
-- | returns all possible directories reachable
197+
-- from the given filepath
193198
directoryCompleter :: FilePath -> Completer
194-
directoryCompleter fp = undefined
199+
directoryCompleter fp pfix = do
200+
completions <- Haskeline.listFiles (fp </> T.unpack pfix)
201+
let filepathCompletions = fmap Haskeline.replacement completions
202+
directoryCompletions <- filterM doesDirectoryExist filepathCompletions
203+
pure $ map T.pack directoryCompletions
195204

196205
-- ----------------------------------------------------------------
197206
-- Completion Data
198207
-- ----------------------------------------------------------------
199208

200209
-- | Keyword for cabal version; required to be the top line in a cabal file
201-
cabalVersionKeyword :: (T.Text,Completer)
202-
cabalVersionKeyword = ("cabal-version:", constantCompleter["2.0", "2.2", "2.4", "3.0"])
210+
cabalVersionKeyword :: Map T.Text Completer
211+
cabalVersionKeyword = Map.singleton "cabal-version:" $
212+
constantCompleter $
213+
map (T.pack . showCabalSpecVersion) [CabalSpecV1_2 .. maxBound]
203214

204215
-- todo: we could add file path completion for file path fields
205216
-- we could add descriptions of field values and then show them when inside the field's context
206217
-- | Top level keywords of a cabal file
207-
cabalKeywords :: Map T.Text Completer
208-
cabalKeywords =
218+
cabalKeywords :: T.Text -> Map T.Text Completer
219+
cabalKeywords rootDir' =
209220
Map.fromList [
210221
("name:", noopCompleter), -- TODO: should complete to filename, needs meta info
211222
("version:", noopCompleter),
212223
("build-type:", constantCompleter ["Simple", "Custom", "Configure", "Make"]),
213224
("license:", constantCompleter ["NONE"]), -- TODO: add possible values, spdx
214-
("license-file:", filePathCompleter ""),
215-
("license-files:", noopCompleter), -- list of filenames
225+
("license-file:", filePathCompleter rootDir),
226+
("license-files:", filePathCompleter rootDir), -- list of filenames
216227
("copyright:", noopCompleter),
217228
("author:", noopCompleter),
218229
("maintainer:", noopCompleter), -- email address, use git config
@@ -224,12 +235,14 @@ cabalKeywords =
224235
("description:", noopCompleter),
225236
("category:", noopCompleter),
226237
("tested-with:", constantCompleter ["GHC"]), -- list of compilers, i.e. "GHC == 8.6.3, GHC == 8.4.4"
227-
("data-files:", noopCompleter), -- list of filenames
228-
("data-dir:", noopCompleter), -- directory
229-
("extra-source-files:", noopCompleter), -- filename list
230-
("extra-doc-files:", noopCompleter), -- filename list
231-
("extra-tmp-files:", noopCompleter) -- filename list
238+
("data-files:", filePathCompleter rootDir), -- list of filenames
239+
("data-dir:", directoryCompleter rootDir), -- directory
240+
("extra-source-files:", filePathCompleter rootDir), -- filename list
241+
("extra-doc-files:", filePathCompleter rootDir), -- filename list
242+
("extra-tmp-files:", filePathCompleter rootDir) -- filename list
232243
]
244+
where
245+
rootDir = T.unpack rootDir'
233246

234247

235248
-- | Map, containing all stanzas in a cabal file as keys and lists of their possible nested keywords as values
@@ -291,13 +304,12 @@ stanzaKeywordMap =
291304
where
292305
libExecTestBenchCommons =
293306
[ ("build-depends:", noopCompleter),
294-
("other-modules:", noopCompleter), -- list of identifiers
295-
("hs-source-dir:", constantCompleter ["."]), -- list of directories
296-
("hs-source-dirs:", constantCompleter ["."]), -- list of directories
297-
("default-extensions:", noopCompleter), -- list of identifiers
298-
("other-extensions:", noopCompleter), -- list of identifiers
299-
("default-language:", noopCompleter), -- identifier
300-
("other-languages:", noopCompleter), -- list of identifiers
307+
("other-modules:", noopCompleter),
308+
("hs-source-dirs:", directoryCompleter ""),
309+
("default-extensions:", noopCompleter),
310+
("other-extensions:", noopCompleter),
311+
("default-language:", noopCompleter),
312+
("other-languages:", noopCompleter),
301313
("build-tool-depends:", noopCompleter),
302314
("buildable:", constantCompleter ["True", "False"]),
303315
("ghc-options:", noopCompleter), -- todo: maybe there is a list of possible ghc options somewhere
@@ -306,18 +318,18 @@ stanzaKeywordMap =
306318
("ghcjs-options:", noopCompleter),
307319
("ghcjs-prof-options:", noopCompleter),
308320
("ghcjs-shared-options:", noopCompleter),
309-
("includes:", noopCompleter), -- list of filenames
310-
("install-includes:", noopCompleter), -- list of filenames
311-
("include-dirs:", noopCompleter), -- list of directories
312-
("c-sources:", noopCompleter), -- list of filenames
313-
("cxx-sources:", noopCompleter), -- list of filenames
314-
("asm-sources:", noopCompleter), -- list of filenames
315-
("cmm-sources:", noopCompleter), -- list of filenames
316-
("js-sources:", noopCompleter), -- list of filenames
321+
("includes:", filePathCompleter ""), -- list of filenames
322+
("install-includes:", filePathCompleter ""), -- list of filenames
323+
("include-dirs:", directoryCompleter ""), -- list of directories
324+
("c-sources:", filePathCompleter ""), -- list of filenames
325+
("cxx-sources:", filePathCompleter ""), -- list of filenames
326+
("asm-sources:", filePathCompleter ""), -- list of filenames
327+
("cmm-sources:", filePathCompleter ""), -- list of filenames
328+
("js-sources:", filePathCompleter ""), -- list of filenames
317329
("extra-libraries:", noopCompleter),
318330
("extra-ghci-libraries:", noopCompleter),
319331
("extra-bundled-libraries:", noopCompleter),
320-
("extra-lib-dirs:", noopCompleter), -- list of directories
332+
("extra-lib-dirs:", directoryCompleter ""), -- list of directories
321333
("cc-options:", noopCompleter),
322334
("cpp-options:", noopCompleter),
323335
("cxx-options:", noopCompleter),
@@ -326,7 +338,7 @@ stanzaKeywordMap =
326338
("ld-options:", noopCompleter),
327339
("pkgconfig-depends:", noopCompleter),
328340
("frameworks:", noopCompleter),
329-
("extra-framework-dirs:", noopCompleter), -- list of directories
341+
("extra-framework-dirs:", directoryCompleter ""), -- list of directories
330342
("mixins:", noopCompleter)
331343
]
332344

0 commit comments

Comments
 (0)