@@ -20,24 +20,33 @@ import qualified Data.ByteString as BS
20
20
import Data.Hashable
21
21
import Data.HashMap.Strict (HashMap )
22
22
import qualified Data.HashMap.Strict as HashMap
23
+ import qualified Data.List as List
23
24
import qualified Data.List.NonEmpty as NE
25
+ import Data.Map (Map )
26
+ import qualified Data.Map as Map
27
+ import qualified Data.Text as T
24
28
import qualified Data.Text.Encoding as Encoding
29
+ import qualified Data.Text.Utf16.Rope as Rope
25
30
import Data.Typeable
26
31
import Development.IDE as D
27
32
import Development.IDE.Core.Shake (restartShakeSession )
28
33
import qualified Development.IDE.Core.Shake as Shake
29
34
import Development.IDE.Graph (alwaysRerun )
35
+ import Distribution.Compat.Lens ((^.) )
30
36
import GHC.Generics
31
37
import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
32
38
import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
33
39
import qualified Ide.Plugin.Cabal.Parse as Parse
34
40
import Ide.Plugin.Config (Config )
35
41
import Ide.Types
36
- import Language.LSP.Server ( LspM )
42
+ import qualified Language.LSP.Server as LSP
37
43
import Language.LSP.Types
44
+ import qualified Language.LSP.Types as J
38
45
import qualified Language.LSP.Types as LSP
46
+ import qualified Language.LSP.Types.Lens as JL
47
+ import Language.LSP.VFS (VirtualFile )
39
48
import qualified Language.LSP.VFS as VFS
40
-
49
+ import qualified Text.Fuzzy.Parallel as Fuzzy
41
50
data Log
42
51
= LogModificationTime NormalizedFilePath FileVersion
43
52
| LogShake Shake. Log
@@ -69,6 +78,7 @@ descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeSta
69
78
descriptor recorder plId = (defaultCabalPluginDescriptor plId)
70
79
{ pluginRules = cabalRules recorder
71
80
, pluginHandlers = mkPluginHandler STextDocumentCodeAction licenseSuggestCodeAction
81
+ <> mkPluginHandler J. STextDocumentCompletion completion
72
82
, pluginNotificationHandlers = mconcat
73
83
[ mkPluginNotificationHandler LSP. STextDocumentDidOpen $
74
84
\ ide vfs _ (DidOpenTextDocumentParams TextDocumentItem {_uri,_version}) -> liftIO $ do
@@ -181,7 +191,7 @@ licenseSuggestCodeAction
181
191
:: IdeState
182
192
-> PluginId
183
193
-> CodeActionParams
184
- -> LspM Config (Either ResponseError (ResponseResult 'TextDocumentCodeAction))
194
+ -> LSP. LspM Config (Either ResponseError (ResponseResult 'TextDocumentCodeAction))
185
195
licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext {_diagnostics= List diags}) =
186
196
pure $ Right $ List $ diags >>= (fmap InR . (LicenseSuggest. licenseErrorAction uri))
187
197
@@ -255,3 +265,206 @@ deleteFileOfInterest recorder state f = do
255
265
log' Debug $ LogFOI files
256
266
where
257
267
log' = logWith recorder
268
+
269
+ -- ----------------------------------------------------------------
270
+ -- Completion
271
+ -- ----------------------------------------------------------------
272
+
273
+ completion :: PluginMethodHandler IdeState 'J.TextDocumentCompletion
274
+ completion _ide _ complParams = do
275
+ let (J. TextDocumentIdentifier uri) = complParams ^. JL. textDocument
276
+ position = complParams ^. JL. position
277
+ contents <- LSP. getVirtualFile $ toNormalizedUri uri
278
+ fmap (Right . J. InL ) $ case (contents, uriToFilePath' uri) of
279
+ (Just cnts, Just _path) -> do
280
+ pref <- VFS. getCompletionPrefix position cnts
281
+ return $ result pref cnts
282
+ _ -> return $ J. List []
283
+ where
284
+ result :: Maybe VFS. PosPrefixInfo -> VirtualFile -> J. List CompletionItem
285
+ result Nothing _ = J. List []
286
+ result (Just pfix) cnts
287
+ | (VFS. cursorPos pfix) ^. JL. line == 0 = J. List [buildCompletion cabalVersionKeyword]
288
+ | Stanza s <- findCurrentLevel (getPreviousLines pfix cnts) =
289
+ case (Map. lookup s stanzaKeywordMap) of
290
+ Nothing ->
291
+ J. List $
292
+ makeCompletionItems pfix topLevelKeywords
293
+ Just l -> J. List $ (makeCompletionItems pfix l) ++ (makeCompletionItems pfix $ Map. keys stanzaKeywordMap)
294
+ | otherwise =
295
+ J. List $
296
+ makeCompletionItems pfix topLevelKeywords
297
+ where
298
+ topLevelKeywords = cabalKeywords ++ Map. keys stanzaKeywordMap
299
+
300
+ -- | Takes info about the current cursor position and a set of possible keywords
301
+ -- and creates completion suggestions that fit the current input from the given list
302
+ makeCompletionItems :: VFS. PosPrefixInfo -> [T. Text ] -> [CompletionItem ]
303
+ makeCompletionItems pfix l =
304
+ map
305
+ (buildCompletion . Fuzzy. original)
306
+ (Fuzzy. simpleFilter 1000 10 (VFS. prefixText pfix) l)
307
+
308
+ -- | Parse the given set of lines (starting before current cursor position
309
+ -- up to the start of the file) to find the nearest stanza declaration,
310
+ -- if none is found we are in the top level
311
+ findCurrentLevel :: [T. Text ] -> Context
312
+ findCurrentLevel [] = TopLevel
313
+ findCurrentLevel (cur : xs)
314
+ | Just s <- stanza = Stanza s
315
+ | otherwise = findCurrentLevel xs
316
+ where
317
+ stanza = List. find (`T.isPrefixOf` cur) (Map. keys stanzaKeywordMap)
318
+
319
+ -- | Get all lines before the given cursor position in the given file
320
+ -- and reverse them since we want to traverse starting from our current position
321
+ getPreviousLines :: VFS. PosPrefixInfo -> VirtualFile -> [T. Text ]
322
+ getPreviousLines pos cont = reverse $ take (fromIntegral currentLine) allLines
323
+ where
324
+ allLines = Rope. lines $ cont ^. VFS. file_text
325
+ currentLine = (VFS. cursorPos pos) ^. JL. line
326
+
327
+
328
+ data Context
329
+ = TopLevel
330
+ -- ^ top level context in a cabal file such as 'author'
331
+ | Stanza T. Text
332
+ -- ^ nested context in a cabal file, such as 'library', which has nested keywords, specific to the stanza
333
+ deriving (Eq )
334
+
335
+ -- | Keyword for cabal version required to be the top line in a cabal file
336
+ cabalVersionKeyword :: T. Text
337
+ cabalVersionKeyword = " cabal-version:"
338
+
339
+ -- | Top level keywords of a cabal file
340
+ cabalKeywords :: [T. Text ]
341
+ cabalKeywords =
342
+ [
343
+ " name:" ,
344
+ " version:" ,
345
+ " build-type:" ,
346
+ " license:" ,
347
+ " license-file:" ,
348
+ " license-files:" ,
349
+ " copyright:" ,
350
+ " author:" ,
351
+ " maintainer:" ,
352
+ " stability:" ,
353
+ " homepage:" ,
354
+ " bug-reports:" ,
355
+ " package-url:" ,
356
+ " synopsis:" ,
357
+ " description:" ,
358
+ " category:" ,
359
+ " tested-with:" ,
360
+ " data-files:" ,
361
+ " data-dir:" ,
362
+ " data-dir:" ,
363
+ " extra-doc-files:" ,
364
+ " extra-tmp-files:"
365
+ ]
366
+
367
+ -- | Map, containing all stanzas in a cabal file as keys and lists of their possible nested keywords as values
368
+ stanzaKeywordMap :: Map T. Text [T. Text ]
369
+ stanzaKeywordMap = Map. fromList [(" library" , [
370
+ " exposed-modules:" ,
371
+ " virtual-modules:" ,
372
+ " exposed:" ,
373
+ " visibility:" ,
374
+ " reexported-modules:" ,
375
+ " signatures:"
376
+ ])]
377
+
378
+
379
+ -- TODO move out toplevel commands i.e. test-suite
380
+ -- cabalTestKeywords :: [T.Text]
381
+ -- cabalTestKeywords =
382
+ -- [
383
+ -- "test-suite",
384
+ -- "type:",
385
+ -- "main-is:",
386
+ -- "test-module:",
387
+ -- "benchmark",
388
+ -- "main-is:",
389
+ -- "foreign-library",
390
+ -- "type:",
391
+ -- "options:",
392
+ -- "mod-def-file:",
393
+ -- "lib-version-info:",
394
+ -- "lib-version-linux:",
395
+ -- "build-depends:",
396
+ -- "other-modules:",
397
+ -- "hs-source-dir:",
398
+ -- "hs-source-dirs:",
399
+ -- "default-extensions:",
400
+ -- "other-extensions:",
401
+ -- "default-language:",
402
+ -- "other-languages:",
403
+ -- "extensions:",
404
+ -- "build-tool-depends:",
405
+ -- "build-tools:",
406
+ -- "buildable:",
407
+ -- "ghc-options:",
408
+ -- "ghc-prof-options:",
409
+ -- "ghc-shared-options:",
410
+ -- "ghcjs-options:",
411
+ -- "ghcjs-prof-options:",
412
+ -- "ghcjs-shared-options:",
413
+ -- "includes:",
414
+ -- "install-includes:",
415
+ -- ("include-dirs:", "directory list"),
416
+ -- ("c-sources:", "filename list"),
417
+ -- ("cxx-sources:", "filename list"),
418
+ -- ("asm-sources:", "filename list"),
419
+ -- ("cmm-sources:", "filename list"),
420
+ -- ("js-sources:", "filename list"),
421
+ -- ("extra-libraries:", "token list"),
422
+ -- ("extra-libraries-static:", "token list"),
423
+ -- ("extra-ghci-libraries:", "token list"),
424
+ -- ("extra-bundled-libraries:", "token list"),
425
+ -- ("extra-lib-dirs:", "directory list")
426
+ -- ("extra-lib-dirs-static:", "directory list"),
427
+ -- ("extra-library-flavours:", "notsure"),
428
+ -- ("extra-dynamic-library-flavours:", "notsure"),
429
+ -- ("cc-options:", "token list"),
430
+ -- ("cpp-options:", "token list"),
431
+ -- ("cxx-options:", "token list"),
432
+ -- ("cmm-options:", "token list"),
433
+ -- ("asm-options:", "token list"),
434
+ -- ("ld-options:", "token list"),
435
+ -- ("hsc2hs-options:", "token list"),
436
+ -- ("pkgconfig-depends:", "package list"),
437
+ -- ("frameworks:", "token list"),
438
+ -- ("extra-framework-dirs:", "directory list"),
439
+ -- ("mixins:", "mixin list")
440
+ -- ]
441
+
442
+ -- cabalFlagKeywords :: [(T.Text, T.Text)]
443
+ -- cabalFlagKeywords =
444
+ -- [
445
+ -- ("flag", "name"),
446
+ -- ("description:", "freeform"),
447
+ -- ("default:", "boolean"),
448
+ -- ("manual:", "boolean")
449
+ -- ]
450
+
451
+ -- cabalStanzaKeywords :: [(T.Text, T.Text)]
452
+ -- cabalStanzaKeywords =
453
+ -- [
454
+ -- ("common", "name"),
455
+ -- ("import:", "token-list")
456
+ -- ]
457
+
458
+ -- cabalSourceRepoKeywords :: [(T.Text, T.Text)]
459
+ -- cabalSourceRepoKeywords =
460
+ -- [
461
+ -- ("source-repository", ""),
462
+ -- ("type:", "token"),
463
+ -- ("location:", "URL")
464
+ -- ]
465
+
466
+ buildCompletion :: T. Text -> J. CompletionItem
467
+ buildCompletion label =
468
+ J. CompletionItem label (Just J. CiKeyword ) Nothing Nothing
469
+ Nothing Nothing Nothing Nothing Nothing Nothing Nothing
470
+ Nothing Nothing Nothing Nothing Nothing Nothing
0 commit comments