Skip to content

Commit 9c3821f

Browse files
Jana ChadtVeryMilkyJoe
Jana Chadt
authored andcommitted
Add completion support for cabal files
Currently supports: * completions for keywords, sensitive to stanzas * value completions for constant values, licenses, files and directories
1 parent a918c02 commit 9c3821f

File tree

10 files changed

+1324
-190
lines changed

10 files changed

+1324
-190
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ library
2727
exposed-modules:
2828
Ide.Plugin.Cabal
2929
Ide.Plugin.Cabal.Diagnostics
30+
Ide.Plugin.Cabal.Completions
3031
Ide.Plugin.Cabal.LicenseSuggest
3132
Ide.Plugin.Cabal.Parse
3233

@@ -45,8 +46,10 @@ library
4546
-- This is a lot of work for almost zero benefit, so we just allow more versions here
4647
-- and we eventually completely drop support for building HLS with stack.
4748
, Cabal ^>=3.2 || ^>=3.4 || ^>=3.6 || ^>= 3.8 || ^>= 3.10
49+
, containers
4850
, deepseq
4951
, directory
52+
, filepath
5053
, extra >=1.7.4
5154
, ghcide == 2.1.0.0
5255
, hashable
@@ -57,6 +60,7 @@ library
5760
, regex-tdfa ^>=1.3.1
5861
, stm
5962
, text
63+
, text-rope
6064
, unordered-containers >=0.2.10.0
6165
, containers
6266
hs-source-dirs: src
@@ -71,11 +75,13 @@ test-suite tests
7175
build-depends:
7276
, base
7377
, bytestring
78+
, directory
7479
, filepath
7580
, ghcide
7681
, hls-cabal-plugin
7782
, hls-test-utils == 2.1.0.0
7883
, lens
84+
, lsp
7985
, lsp-types
8086
, tasty-hunit
8187
, text

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

+105-62
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
{-# LANGUAGE TupleSections #-}
1010
{-# LANGUAGE TypeFamilies #-}
1111

12-
module Ide.Plugin.Cabal (descriptor, Log(..)) where
12+
module Ide.Plugin.Cabal (descriptor, Log (..)) where
1313

1414
import Control.Concurrent.STM
1515
import Control.Concurrent.Strict
@@ -22,21 +22,23 @@ import Data.HashMap.Strict (HashMap)
2222
import qualified Data.HashMap.Strict as HashMap
2323
import qualified Data.List.NonEmpty as NE
2424
import qualified Data.Text.Encoding as Encoding
25+
import qualified Data.Text.Utf16.Rope as Rope
2526
import Data.Typeable
2627
import Development.IDE as D
2728
import Development.IDE.Core.Shake (restartShakeSession)
2829
import qualified Development.IDE.Core.Shake as Shake
2930
import Development.IDE.Graph (alwaysRerun)
31+
import Distribution.Compat.Lens ((^.))
3032
import GHC.Generics
33+
import qualified Ide.Plugin.Cabal.Completions as Completions
3134
import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
3235
import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
3336
import qualified Ide.Plugin.Cabal.Parse as Parse
34-
import Ide.Plugin.Config (Config)
3537
import Ide.Types
38+
import qualified Language.LSP.Protocol.Lens as JL
3639
import qualified Language.LSP.Protocol.Message as LSP
3740
import Language.LSP.Protocol.Types
38-
import qualified Language.LSP.Protocol.Types as LSP
39-
import Language.LSP.Server (LspM)
41+
import Language.LSP.Server (LspM, getVirtualFile)
4042
import qualified Language.LSP.VFS as VFS
4143

4244
data Log
@@ -47,12 +49,14 @@ data Log
4749
| LogDocSaved Uri
4850
| LogDocClosed Uri
4951
| LogFOI (HashMap NormalizedFilePath FileOfInterestStatus)
50-
deriving Show
52+
| LogCompletionContext Completions.Context Position
53+
| LogCompletions Completions.Log
54+
deriving (Show)
5155

5256
instance Pretty Log where
5357
pretty = \case
5458
LogShake log' -> pretty log'
55-
LogModificationTime nfp modTime ->
59+
LogModificationTime nfp modTime ->
5660
"Modified:" <+> pretty (fromNormalizedFilePath nfp) <+> pretty (show modTime)
5761
LogDocOpened uri ->
5862
"Opened text document:" <+> pretty (getUri uri)
@@ -64,12 +68,18 @@ instance Pretty Log where
6468
"Closed text document:" <+> pretty (getUri uri)
6569
LogFOI files ->
6670
"Set files of interest to:" <+> viaShow files
67-
71+
LogCompletionContext context position->
72+
"Determined completion context:" <+> viaShow context
73+
<+> "for cursor position:" <+> viaShow position
74+
LogCompletions logs -> pretty logs
6875

6976
descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState
7077
descriptor recorder plId = (defaultCabalPluginDescriptor plId)
7178
{ pluginRules = cabalRules recorder
72-
, pluginHandlers = mkPluginHandler LSP.SMethod_TextDocumentCodeAction licenseSuggestCodeAction
79+
, pluginHandlers = mconcat
80+
[ mkPluginHandler LSP.SMethod_TextDocumentCodeAction licenseSuggestCodeAction
81+
, mkPluginHandler LSP.SMethod_TextDocumentCompletion $ completion recorder
82+
]
7383
, pluginNotificationHandlers = mconcat
7484
[ mkPluginNotificationHandler LSP.SMethod_TextDocumentDidOpen $
7585
\ide vfs _ (DidOpenTextDocumentParams TextDocumentItem{_uri,_version}) -> liftIO $ do
@@ -104,7 +114,7 @@ descriptor recorder plId = (defaultCabalPluginDescriptor plId)
104114
log' = logWith recorder
105115

106116
whenUriFile :: Uri -> (NormalizedFilePath -> IO ()) -> IO ()
107-
whenUriFile uri act = whenJust (LSP.uriToFilePath uri) $ act . toNormalizedFilePath'
117+
whenUriFile uri act = whenJust (uriToFilePath uri) $ act . toNormalizedFilePath'
108118

109119
-- | Helper function to restart the shake session, specifically for modifying .cabal files.
110120
-- No special logic, just group up a bunch of functions you need for the base
@@ -124,9 +134,9 @@ restartCabalShakeSession shakeExtras vfs file actionMsg = do
124134
-- ----------------------------------------------------------------
125135

126136
data ParseCabal = ParseCabal
127-
deriving (Eq, Show, Typeable, Generic)
137+
deriving (Eq, Show, Typeable, Generic)
128138
instance Hashable ParseCabal
129-
instance NFData ParseCabal
139+
instance NFData ParseCabal
130140

131141
type instance RuleResult ParseCabal = ()
132142

@@ -141,7 +151,8 @@ cabalRules recorder = do
141151
(t, mCabalSource) <- use_ GetFileContents file
142152
log' Debug $ LogModificationTime file t
143153
contents <- case mCabalSource of
144-
Just sources -> pure $ Encoding.encodeUtf8 sources
154+
Just sources ->
155+
pure $ Encoding.encodeUtf8 sources
145156
Nothing -> do
146157
liftIO $ BS.readFile $ fromNormalizedFilePath file
147158

@@ -160,15 +171,16 @@ cabalRules recorder = do
160171
-- Must be careful to not impede the performance too much. Crucial to
161172
-- a snappy IDE experience.
162173
kick
163-
where
164-
log' = logWith recorder
174+
where
175+
log' = logWith recorder
165176

166-
-- | This is the kick function for the cabal plugin.
167-
-- We run this action, whenever we shake session us run/restarted, which triggers
168-
-- actions to produce diagnostics for cabal files.
169-
--
170-
-- It is paramount that this kick-function can be run quickly, since it is a blocking
171-
-- function invocation.
177+
{- | This is the kick function for the cabal plugin.
178+
We run this action, whenever we shake session us run/restarted, which triggers
179+
actions to produce diagnostics for cabal files.
180+
181+
It is paramount that this kick-function can be run quickly, since it is a blocking
182+
function invocation.
183+
-}
172184
kick :: Action ()
173185
kick = do
174186
files <- HashMap.keys <$> getCabalFilesOfInterestUntracked
@@ -190,69 +202,100 @@ licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri)
190202
-- Cabal file of Interest rules and global variable
191203
-- ----------------------------------------------------------------
192204

193-
-- | Cabal files that are currently open in the lsp-client.
194-
-- Specific actions happen when these files are saved, closed or modified,
195-
-- such as generating diagnostics, re-parsing, etc...
196-
--
197-
-- We need to store the open files to parse them again if we restart the shake session.
198-
-- Restarting of the shake session happens whenever these files are modified.
205+
{- | Cabal files that are currently open in the lsp-client.
206+
Specific actions happen when these files are saved, closed or modified,
207+
such as generating diagnostics, re-parsing, etc...
208+
209+
We need to store the open files to parse them again if we restart the shake session.
210+
Restarting of the shake session happens whenever these files are modified.
211+
-}
199212
newtype OfInterestCabalVar = OfInterestCabalVar (Var (HashMap NormalizedFilePath FileOfInterestStatus))
200213

201214
instance Shake.IsIdeGlobal OfInterestCabalVar
202215

203216
data IsCabalFileOfInterest = IsCabalFileOfInterest
204-
deriving (Eq, Show, Typeable, Generic)
217+
deriving (Eq, Show, Typeable, Generic)
205218
instance Hashable IsCabalFileOfInterest
206-
instance NFData IsCabalFileOfInterest
219+
instance NFData IsCabalFileOfInterest
207220

208221
type instance RuleResult IsCabalFileOfInterest = CabalFileOfInterestResult
209222

210223
data CabalFileOfInterestResult = NotCabalFOI | IsCabalFOI FileOfInterestStatus
211224
deriving (Eq, Show, Typeable, Generic)
212225
instance Hashable CabalFileOfInterestResult
213-
instance NFData CabalFileOfInterestResult
226+
instance NFData CabalFileOfInterestResult
214227

215-
-- | The rule that initialises the files of interest state.
216-
--
217-
-- Needs to be run on start-up.
228+
{- | The rule that initialises the files of interest state.
229+
230+
Needs to be run on start-up.
231+
-}
218232
ofInterestRules :: Recorder (WithPriority Log) -> Rules ()
219233
ofInterestRules recorder = do
220-
Shake.addIdeGlobal . OfInterestCabalVar =<< liftIO (newVar HashMap.empty)
221-
Shake.defineEarlyCutoff (cmapWithPrio LogShake recorder) $ RuleNoDiagnostics $ \IsCabalFileOfInterest f -> do
222-
alwaysRerun
223-
filesOfInterest <- getCabalFilesOfInterestUntracked
224-
let foi = maybe NotCabalFOI IsCabalFOI $ f `HashMap.lookup` filesOfInterest
225-
fp = summarize foi
226-
res = (Just fp, Just foi)
227-
return res
228-
where
229-
summarize NotCabalFOI = BS.singleton 0
230-
summarize (IsCabalFOI OnDisk) = BS.singleton 1
231-
summarize (IsCabalFOI (Modified False)) = BS.singleton 2
232-
summarize (IsCabalFOI (Modified True)) = BS.singleton 3
234+
Shake.addIdeGlobal . OfInterestCabalVar =<< liftIO (newVar HashMap.empty)
235+
Shake.defineEarlyCutoff (cmapWithPrio LogShake recorder) $ RuleNoDiagnostics $ \IsCabalFileOfInterest f -> do
236+
alwaysRerun
237+
filesOfInterest <- getCabalFilesOfInterestUntracked
238+
let foi = maybe NotCabalFOI IsCabalFOI $ f `HashMap.lookup` filesOfInterest
239+
fp = summarize foi
240+
res = (Just fp, Just foi)
241+
return res
242+
where
243+
summarize NotCabalFOI = BS.singleton 0
244+
summarize (IsCabalFOI OnDisk) = BS.singleton 1
245+
summarize (IsCabalFOI (Modified False)) = BS.singleton 2
246+
summarize (IsCabalFOI (Modified True)) = BS.singleton 3
233247

234248
getCabalFilesOfInterestUntracked :: Action (HashMap NormalizedFilePath FileOfInterestStatus)
235249
getCabalFilesOfInterestUntracked = do
236-
OfInterestCabalVar var <- Shake.getIdeGlobalAction
237-
liftIO $ readVar var
250+
OfInterestCabalVar var <- Shake.getIdeGlobalAction
251+
liftIO $ readVar var
238252

239253
addFileOfInterest :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> FileOfInterestStatus -> IO ()
240254
addFileOfInterest recorder state f v = do
241-
OfInterestCabalVar var <- Shake.getIdeGlobalState state
242-
(prev, files) <- modifyVar var $ \dict -> do
243-
let (prev, new) = HashMap.alterF (, Just v) f dict
244-
pure (new, (prev, new))
245-
when (prev /= Just v) $ do
246-
join $ atomically $ Shake.recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
247-
log' Debug $ LogFOI files
248-
where
249-
log' = logWith recorder
255+
OfInterestCabalVar var <- Shake.getIdeGlobalState state
256+
(prev, files) <- modifyVar var $ \dict -> do
257+
let (prev, new) = HashMap.alterF (,Just v) f dict
258+
pure (new, (prev, new))
259+
when (prev /= Just v) $ do
260+
join $ atomically $ Shake.recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
261+
log' Debug $ LogFOI files
262+
where
263+
log' = logWith recorder
250264

251265
deleteFileOfInterest :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> IO ()
252266
deleteFileOfInterest recorder state f = do
253-
OfInterestCabalVar var <- Shake.getIdeGlobalState state
254-
files <- modifyVar' var $ HashMap.delete f
255-
join $ atomically $ Shake.recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
256-
log' Debug $ LogFOI files
257-
where
258-
log' = logWith recorder
267+
OfInterestCabalVar var <- Shake.getIdeGlobalState state
268+
files <- modifyVar' var $ HashMap.delete f
269+
join $ atomically $ Shake.recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
270+
log' Debug $ LogFOI files
271+
where
272+
log' = logWith recorder
273+
274+
-- ----------------------------------------------------------------
275+
-- Completion
276+
-- ----------------------------------------------------------------
277+
278+
completion :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'LSP.Method_TextDocumentCompletion
279+
completion recorder _ide _ complParams = do
280+
let (TextDocumentIdentifier uri) = complParams ^. JL.textDocument
281+
position = complParams ^. JL.position
282+
contents <- getVirtualFile $ toNormalizedUri uri
283+
fmap (Right . InL) $ case (contents, uriToFilePath' uri) of
284+
(Just cnts, Just path) -> do
285+
pref <- VFS.getCompletionPrefix position cnts
286+
liftIO $ result pref path cnts
287+
_ -> return []
288+
where
289+
result :: Maybe VFS.PosPrefixInfo -> FilePath -> VFS.VirtualFile -> IO [CompletionItem]
290+
result Nothing _ _ = pure []
291+
result (Just prefix) fp cnts
292+
| Just ctx <- context = do
293+
logWith recorder Debug $ LogCompletionContext ctx pos
294+
let completer = Completions.contextToCompleter ctx
295+
completions <- completer (cmapWithPrio LogCompletions recorder) completionContext
296+
pure $ Completions.mkCompletionItems completions
297+
| otherwise = pure []
298+
where
299+
pos = VFS.cursorPos prefix
300+
context = Completions.getContext completionContext (Rope.lines $ cnts ^. VFS.file_text)
301+
completionContext = Completions.getCabalCompletionContext fp prefix

0 commit comments

Comments
 (0)