Skip to content

Commit aa47679

Browse files
author
Jana Chadt
committed
Add completion functionality for cabal keywords
1 parent 65ab7b1 commit aa47679

File tree

2 files changed

+218
-4
lines changed

2 files changed

+218
-4
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ library
3232
, base >=4.12 && <5
3333
, bytestring
3434
, Cabal ^>=3.2 || ^>=3.4 || ^>=3.6
35+
, containers
36+
, text-rope
3537
, deepseq
3638
, directory
3739
, extra >=1.7.4

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

Lines changed: 216 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,20 @@ import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
2929
import qualified Ide.Plugin.Cabal.Parse as Parse
3030
import Ide.Plugin.Config (Config)
3131
import Ide.Types
32-
import Language.LSP.Server (LspM)
32+
import qualified Language.LSP.Server as LSP
3333
import Language.LSP.Types
3434
import qualified Language.LSP.Types as LSP
3535
import qualified Language.LSP.VFS as VFS
36-
36+
import qualified Data.Text as T
37+
import qualified Language.LSP.Types.Lens as JL
38+
import qualified Language.LSP.Types as J
39+
import Distribution.Compat.Lens((^.))
40+
import qualified Text.Fuzzy.Parallel as Fuzzy
41+
import Data.Map (Map)
42+
import qualified Data.Map as Map
43+
import Language.LSP.VFS (VirtualFile)
44+
import qualified Data.Text.Utf16.Rope as Rope
45+
import qualified Data.List as List
3746
data Log
3847
= LogModificationTime NormalizedFilePath (Maybe FileVersion)
3948
| LogDiagnostics NormalizedFilePath [FileDiagnostic]
@@ -63,7 +72,8 @@ instance Pretty Log where
6372
descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState
6473
descriptor recorder plId = (defaultCabalPluginDescriptor plId)
6574
{ pluginRules = cabalRules recorder
66-
, pluginHandlers = mkPluginHandler STextDocumentCodeAction licenseSuggestCodeAction
75+
, pluginHandlers = mkPluginHandler STextDocumentCodeAction licenseSuggestCodeAction
76+
<> mkPluginHandler J.STextDocumentCompletion completion
6777
, pluginNotificationHandlers = mconcat
6878
[ mkPluginNotificationHandler LSP.STextDocumentDidOpen $
6979
\ide vfs _ (DidOpenTextDocumentParams TextDocumentItem{_uri,_version}) -> liftIO $ do
@@ -149,6 +159,208 @@ licenseSuggestCodeAction
149159
:: IdeState
150160
-> PluginId
151161
-> CodeActionParams
152-
-> LspM Config (Either ResponseError (ResponseResult 'TextDocumentCodeAction))
162+
-> LSP.LspM Config (Either ResponseError (ResponseResult 'TextDocumentCodeAction))
153163
licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext{_diagnostics=List diags}) =
154164
pure $ Right $ List $ mapMaybe (fmap InR . LicenseSuggest.licenseErrorAction uri) diags
165+
166+
-- ----------------------------------------------------------------
167+
-- Completion
168+
-- ----------------------------------------------------------------
169+
completion :: PluginMethodHandler IdeState 'J.TextDocumentCompletion
170+
completion _ide _ complParams = do
171+
let (J.TextDocumentIdentifier uri) = complParams ^. JL.textDocument
172+
position = complParams ^. JL.position
173+
contents <- LSP.getVirtualFile $ toNormalizedUri uri
174+
fmap (Right . J.InL) $ case (contents, uriToFilePath' uri) of
175+
(Just cnts, Just _path) -> do
176+
pref <- VFS.getCompletionPrefix position cnts
177+
return $ result pref cnts
178+
_ -> return $ J.List []
179+
where
180+
result :: Maybe VFS.PosPrefixInfo -> VirtualFile -> J.List CompletionItem
181+
result Nothing _ = J.List []
182+
result (Just pfix) cnts
183+
| (VFS.cursorPos pfix) ^. JL.line == 0 = J.List [buildCompletion cabalVersionKeyword]
184+
| Stanza s <- findCurrentLevel (getPreviousLines pfix cnts) =
185+
case (Map.lookup s stanzaKeywordMap) of
186+
Nothing ->
187+
J.List $
188+
makeCompletionItems pfix topLevelKeywords
189+
Just l -> J.List $ (makeCompletionItems pfix l) ++ (makeCompletionItems pfix $ Map.keys stanzaKeywordMap)
190+
| otherwise =
191+
J.List $
192+
makeCompletionItems pfix topLevelKeywords
193+
where
194+
topLevelKeywords = cabalKeywords ++ Map.keys stanzaKeywordMap
195+
196+
-- | Takes info about the current cursor position and a set of possible keywords
197+
-- and creates completion suggestions that fit the current input from the given list
198+
makeCompletionItems :: VFS.PosPrefixInfo -> [T.Text] -> [CompletionItem]
199+
makeCompletionItems pfix l =
200+
map
201+
(buildCompletion . Fuzzy.original)
202+
(Fuzzy.simpleFilter 1000 10 (VFS.prefixText pfix) l)
203+
204+
-- | Parse the given set of lines (starting before current cursor position
205+
-- up to the start of the file) to find the nearest stanza declaration,
206+
-- if none is found we are in the top level
207+
findCurrentLevel :: [T.Text] -> Context
208+
findCurrentLevel [] = TopLevel
209+
findCurrentLevel (cur : xs)
210+
| Just s <- stanza = Stanza s
211+
| otherwise = findCurrentLevel xs
212+
where
213+
stanza = List.find (`T.isPrefixOf` cur) (Map.keys stanzaKeywordMap)
214+
215+
-- | Get all lines before the given cursor position in the given file
216+
-- and reverse them since we want to traverse starting from our current position
217+
getPreviousLines :: VFS.PosPrefixInfo -> VirtualFile -> [T.Text]
218+
getPreviousLines pos cont = reverse $ take (fromIntegral currentLine) allLines
219+
where
220+
allLines = Rope.lines $ cont ^. VFS.file_text
221+
currentLine = (VFS.cursorPos pos) ^. JL.line
222+
223+
224+
data Context
225+
= TopLevel
226+
-- ^ top level context in a cabal file such as 'author'
227+
| Stanza T.Text
228+
-- ^ nested context in a cabal file, such as 'library', which has nested keywords, specific to the stanza
229+
deriving (Eq)
230+
231+
-- | Keyword for cabal version required to be the top line in a cabal file
232+
cabalVersionKeyword :: T.Text
233+
cabalVersionKeyword = "cabal-version:"
234+
235+
-- | Top level keywords of a cabal file
236+
cabalKeywords :: [T.Text]
237+
cabalKeywords =
238+
[
239+
"name:",
240+
"version:",
241+
"build-type:",
242+
"license:",
243+
"license-file:",
244+
"license-files:",
245+
"copyright:",
246+
"author:",
247+
"maintainer:",
248+
"stability:",
249+
"homepage:",
250+
"bug-reports:",
251+
"package-url:",
252+
"synopsis:",
253+
"description:",
254+
"category:",
255+
"tested-with:",
256+
"data-files:",
257+
"data-dir:",
258+
"data-dir:",
259+
"extra-doc-files:",
260+
"extra-tmp-files:"
261+
]
262+
263+
-- | Map, containing all stanzas in a cabal file as keys and lists of their possible nested keywords as values
264+
stanzaKeywordMap :: Map T.Text [T.Text]
265+
stanzaKeywordMap = Map.fromList [("library", [
266+
"exposed-modules:",
267+
"virtual-modules:",
268+
"exposed:",
269+
"visibility:",
270+
"reexported-modules:",
271+
"signatures:"
272+
])]
273+
274+
275+
-- TODO move out toplevel commands i.e. test-suite
276+
-- cabalTestKeywords :: [T.Text]
277+
-- cabalTestKeywords =
278+
-- [
279+
-- "test-suite",
280+
-- "type:",
281+
-- "main-is:",
282+
-- "test-module:",
283+
-- "benchmark",
284+
-- "main-is:",
285+
-- "foreign-library",
286+
-- "type:",
287+
-- "options:",
288+
-- "mod-def-file:",
289+
-- "lib-version-info:",
290+
-- "lib-version-linux:",
291+
-- "build-depends:",
292+
-- "other-modules:",
293+
-- "hs-source-dir:",
294+
-- "hs-source-dirs:",
295+
-- "default-extensions:",
296+
-- "other-extensions:",
297+
-- "default-language:",
298+
-- "other-languages:",
299+
-- "extensions:",
300+
-- "build-tool-depends:",
301+
-- "build-tools:",
302+
-- "buildable:",
303+
-- "ghc-options:",
304+
-- "ghc-prof-options:",
305+
-- "ghc-shared-options:",
306+
-- "ghcjs-options:",
307+
-- "ghcjs-prof-options:",
308+
-- "ghcjs-shared-options:",
309+
-- "includes:",
310+
-- "install-includes:",
311+
-- ("include-dirs:", "directory list"),
312+
-- ("c-sources:", "filename list"),
313+
-- ("cxx-sources:", "filename list"),
314+
-- ("asm-sources:", "filename list"),
315+
-- ("cmm-sources:", "filename list"),
316+
-- ("js-sources:", "filename list"),
317+
-- ("extra-libraries:", "token list"),
318+
-- ("extra-libraries-static:", "token list"),
319+
-- ("extra-ghci-libraries:", "token list"),
320+
-- ("extra-bundled-libraries:", "token list"),
321+
-- ("extra-lib-dirs:", "directory list")
322+
-- ("extra-lib-dirs-static:", "directory list"),
323+
-- ("extra-library-flavours:", "notsure"),
324+
-- ("extra-dynamic-library-flavours:", "notsure"),
325+
-- ("cc-options:", "token list"),
326+
-- ("cpp-options:", "token list"),
327+
-- ("cxx-options:", "token list"),
328+
-- ("cmm-options:", "token list"),
329+
-- ("asm-options:", "token list"),
330+
-- ("ld-options:", "token list"),
331+
-- ("hsc2hs-options:", "token list"),
332+
-- ("pkgconfig-depends:", "package list"),
333+
-- ("frameworks:", "token list"),
334+
-- ("extra-framework-dirs:", "directory list"),
335+
-- ("mixins:", "mixin list")
336+
-- ]
337+
338+
-- cabalFlagKeywords :: [(T.Text, T.Text)]
339+
-- cabalFlagKeywords =
340+
-- [
341+
-- ("flag", "name"),
342+
-- ("description:", "freeform"),
343+
-- ("default:", "boolean"),
344+
-- ("manual:", "boolean")
345+
-- ]
346+
347+
-- cabalStanzaKeywords :: [(T.Text, T.Text)]
348+
-- cabalStanzaKeywords =
349+
-- [
350+
-- ("common", "name"),
351+
-- ("import:", "token-list")
352+
-- ]
353+
354+
-- cabalSourceRepoKeywords :: [(T.Text, T.Text)]
355+
-- cabalSourceRepoKeywords =
356+
-- [
357+
-- ("source-repository", ""),
358+
-- ("type:", "token"),
359+
-- ("location:", "URL")
360+
-- ]
361+
362+
buildCompletion :: T.Text -> J.CompletionItem
363+
buildCompletion label =
364+
J.CompletionItem label (Just J.CiKeyword) Nothing Nothing
365+
Nothing Nothing Nothing Nothing Nothing Nothing Nothing
366+
Nothing Nothing Nothing Nothing Nothing Nothing

0 commit comments

Comments
 (0)