Skip to content

Commit b4da571

Browse files
Jana ChadtVeryMilkyJoe
Jana Chadt
authored andcommitted
Add completion functionality for cabal keywords
1 parent 5540213 commit b4da571

File tree

2 files changed

+218
-3
lines changed

2 files changed

+218
-3
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ library
4545
-- This is a lot of work for almost zero benefit, so we just allow more versions here
4646
-- and we eventually completely drop support for building HLS with stack.
4747
, Cabal ^>=3.2 || ^>=3.4 || ^>=3.6 || ^>= 3.8 || ^>= 3.10
48+
, containers
4849
, deepseq
4950
, directory
5051
, extra >=1.7.4
@@ -57,6 +58,7 @@ library
5758
, regex-tdfa ^>=1.3.1
5859
, stm
5960
, text
61+
, text-rope
6062
, unordered-containers >=0.2.10.0
6163

6264
hs-source-dirs: src

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

Lines changed: 216 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,33 @@ import qualified Data.ByteString as BS
2020
import Data.Hashable
2121
import Data.HashMap.Strict (HashMap)
2222
import qualified Data.HashMap.Strict as HashMap
23+
import qualified Data.List as List
2324
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
2428
import qualified Data.Text.Encoding as Encoding
29+
import qualified Data.Text.Utf16.Rope as Rope
2530
import Data.Typeable
2631
import Development.IDE as D
2732
import Development.IDE.Core.Shake (restartShakeSession)
2833
import qualified Development.IDE.Core.Shake as Shake
2934
import Development.IDE.Graph (alwaysRerun)
35+
import Distribution.Compat.Lens ((^.))
3036
import GHC.Generics
3137
import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
3238
import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
3339
import qualified Ide.Plugin.Cabal.Parse as Parse
3440
import Ide.Plugin.Config (Config)
3541
import Ide.Types
36-
import Language.LSP.Server (LspM)
42+
import qualified Language.LSP.Server as LSP
3743
import Language.LSP.Types
44+
import qualified Language.LSP.Types as J
3845
import qualified Language.LSP.Types as LSP
46+
import qualified Language.LSP.Types.Lens as JL
47+
import Language.LSP.VFS (VirtualFile)
3948
import qualified Language.LSP.VFS as VFS
40-
49+
import qualified Text.Fuzzy.Parallel as Fuzzy
4150
data Log
4251
= LogModificationTime NormalizedFilePath FileVersion
4352
| LogShake Shake.Log
@@ -69,6 +78,7 @@ descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeSta
6978
descriptor recorder plId = (defaultCabalPluginDescriptor plId)
7079
{ pluginRules = cabalRules recorder
7180
, pluginHandlers = mkPluginHandler STextDocumentCodeAction licenseSuggestCodeAction
81+
<> mkPluginHandler J.STextDocumentCompletion completion
7282
, pluginNotificationHandlers = mconcat
7383
[ mkPluginNotificationHandler LSP.STextDocumentDidOpen $
7484
\ide vfs _ (DidOpenTextDocumentParams TextDocumentItem{_uri,_version}) -> liftIO $ do
@@ -181,7 +191,7 @@ licenseSuggestCodeAction
181191
:: IdeState
182192
-> PluginId
183193
-> CodeActionParams
184-
-> LspM Config (Either ResponseError (ResponseResult 'TextDocumentCodeAction))
194+
-> LSP.LspM Config (Either ResponseError (ResponseResult 'TextDocumentCodeAction))
185195
licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext{_diagnostics=List diags}) =
186196
pure $ Right $ List $ diags >>= (fmap InR . (LicenseSuggest.licenseErrorAction uri))
187197

@@ -255,3 +265,206 @@ deleteFileOfInterest recorder state f = do
255265
log' Debug $ LogFOI files
256266
where
257267
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

Comments
 (0)