2
2
3
3
module Ide.Plugin.Cabal.Completions where
4
4
5
+ import Control.Monad (filterM )
5
6
import qualified Data.List as List
6
7
import qualified Data.List.Extra as Extra
7
8
import Data.Map (Map )
8
9
import qualified Data.Map as Map
9
10
import qualified Data.Text as T
10
11
import Development.IDE as D
12
+ import Distribution.CabalSpecVersion (CabalSpecVersion (CabalSpecV1_2 ),
13
+ showCabalSpecVersion )
11
14
import Distribution.Compat.Lens ((^.) )
15
+ import Distribution.PackageDescription (GenericPackageDescription )
12
16
import Language.LSP.Types
13
17
import qualified Language.LSP.Types as J
14
18
import qualified Language.LSP.Types.Lens as JL
15
19
import qualified Language.LSP.VFS as VFS
16
20
import System.Console.Haskeline.Completion as Haskeline
21
+ import System.Directory (doesDirectoryExist )
22
+ import System.FilePath
17
23
import qualified Text.Fuzzy.Parallel as Fuzzy
18
24
19
25
20
- type Completer = IO [T. Text ]
26
+
27
+ type Completer = T. Text -> IO [T. Text ]
21
28
22
29
-- | The context a cursor can be in within a cabal file,
23
30
-- we can be in stanzas or the top level,
@@ -45,72 +52,50 @@ data KeyWordContext
45
52
-- right before the current position
46
53
deriving (Eq , Show )
47
54
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
-
69
55
-- ----------------------------------------------------------------
70
56
-- Public API for Completions
71
57
-- ----------------------------------------------------------------
72
58
73
- contextToCompleter :: Context -> Completer
59
+ contextToCompleter :: T. Text -> Context -> Completer
74
60
-- if we are in the top level of the cabal file and not in a keyword context,
75
61
-- 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
78
64
-- if we are in a keyword context in the top level,
79
65
-- 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
82
68
Nothing -> noopCompleter
83
69
Just l -> l
84
70
-- if we are in a stanza and not in a keyword context,
85
71
-- we can write any of the stanza's keywords or a stanza declaration
86
- contextToCompleter (Stanza s, None ) =
72
+ contextToCompleter _dir (Stanza s, None ) =
87
73
case Map. lookup s stanzaKeywordMap of
88
74
Nothing -> noopCompleter
89
- Just l -> pure $ Map. keys l ++ Map. keys stanzaKeywordMap
75
+ Just l -> constantCompleter $ Map. keys l ++ Map. keys stanzaKeywordMap
90
76
-- 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) =
92
78
case Map. lookup s stanzaKeywordMap of
93
79
Nothing -> noopCompleter
94
80
Just m -> case Map. lookup kw m of
95
81
Nothing -> noopCompleter
96
82
Just l -> l
97
83
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
99
86
-- 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
105
89
106
90
-- | Takes a position and a list of lines (representing a file)
107
91
-- and returns the context of the current position
108
92
-- can return Nothing if an error occurs
93
+ -- TODO: first line can only have cabal-version: keyword
109
94
getContext :: Position -> [T. Text ] -> Maybe Context
110
95
getContext pos ls =
111
96
case lvlContext of
112
97
TopLevel -> do
113
- kwContext <- getKeyWordContext pos ls (uncurry Map. insert cabalVersionKeyword cabalKeywords)
98
+ kwContext <- getKeyWordContext pos ls (cabalVersionKeyword <> cabalKeywords " " )
114
99
pure (TopLevel , kwContext)
115
100
Stanza s ->
116
101
case Map. lookup s stanzaKeywordMap of
@@ -169,6 +154,20 @@ getPreviousLines pos ls = reverse $ take (fromIntegral currentLine) ls
169
154
where
170
155
currentLine = pos ^. JL. line
171
156
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
+
172
171
buildCompletion :: T. Text -> J. CompletionItem
173
172
buildCompletion label =
174
173
J. CompletionItem label (Just J. CiKeyword ) Nothing Nothing
@@ -180,39 +179,51 @@ buildCompletion label =
180
179
-- ----------------------------------------------------------------
181
180
182
181
noopCompleter :: Completer
183
- noopCompleter = pure []
182
+ noopCompleter _ = pure []
184
183
185
184
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
187
188
189
+ -- | returns all possible files and directories reachable
190
+ -- from the given filepath
188
191
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)
191
194
pure $ map (T. pack . Haskeline. replacement) completions
192
195
196
+ -- | returns all possible directories reachable
197
+ -- from the given filepath
193
198
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
195
204
196
205
-- ----------------------------------------------------------------
197
206
-- Completion Data
198
207
-- ----------------------------------------------------------------
199
208
200
209
-- | 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 ]
203
214
204
215
-- todo: we could add file path completion for file path fields
205
216
-- we could add descriptions of field values and then show them when inside the field's context
206
217
-- | 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' =
209
220
Map. fromList [
210
221
(" name:" , noopCompleter), -- TODO: should complete to filename, needs meta info
211
222
(" version:" , noopCompleter),
212
223
(" build-type:" , constantCompleter [" Simple" , " Custom" , " Configure" , " Make" ]),
213
224
(" 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
216
227
(" copyright:" , noopCompleter),
217
228
(" author:" , noopCompleter),
218
229
(" maintainer:" , noopCompleter), -- email address, use git config
@@ -224,12 +235,14 @@ cabalKeywords =
224
235
(" description:" , noopCompleter),
225
236
(" category:" , noopCompleter),
226
237
(" 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
232
243
]
244
+ where
245
+ rootDir = T. unpack rootDir'
233
246
234
247
235
248
-- | Map, containing all stanzas in a cabal file as keys and lists of their possible nested keywords as values
@@ -291,13 +304,12 @@ stanzaKeywordMap =
291
304
where
292
305
libExecTestBenchCommons =
293
306
[ (" 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),
301
313
(" build-tool-depends:" , noopCompleter),
302
314
(" buildable:" , constantCompleter [" True" , " False" ]),
303
315
(" ghc-options:" , noopCompleter), -- todo: maybe there is a list of possible ghc options somewhere
@@ -306,18 +318,18 @@ stanzaKeywordMap =
306
318
(" ghcjs-options:" , noopCompleter),
307
319
(" ghcjs-prof-options:" , noopCompleter),
308
320
(" 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
317
329
(" extra-libraries:" , noopCompleter),
318
330
(" extra-ghci-libraries:" , noopCompleter),
319
331
(" extra-bundled-libraries:" , noopCompleter),
320
- (" extra-lib-dirs:" , noopCompleter ), -- list of directories
332
+ (" extra-lib-dirs:" , directoryCompleter " " ), -- list of directories
321
333
(" cc-options:" , noopCompleter),
322
334
(" cpp-options:" , noopCompleter),
323
335
(" cxx-options:" , noopCompleter),
@@ -326,7 +338,7 @@ stanzaKeywordMap =
326
338
(" ld-options:" , noopCompleter),
327
339
(" pkgconfig-depends:" , noopCompleter),
328
340
(" frameworks:" , noopCompleter),
329
- (" extra-framework-dirs:" , noopCompleter ), -- list of directories
341
+ (" extra-framework-dirs:" , directoryCompleter " " ), -- list of directories
330
342
(" mixins:" , noopCompleter)
331
343
]
332
344
0 commit comments