Skip to content

Commit 0ec6fe4

Browse files
committed
Implement filepath completion closing opened apostrophes
Implement tests for functionality Refactor completion text representation into a type that can: * carry information on suffix to be added on completion * carry separate text to be displayed in suggestion box, * and text to be inserted on completion action
1 parent 22e2d18 commit 0ec6fe4

File tree

3 files changed

+732
-613
lines changed

3 files changed

+732
-613
lines changed

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

Lines changed: 147 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,40 @@
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
1616
import Control.DeepSeq
1717
import Control.Monad.Extra
1818
import Control.Monad.IO.Class
19-
import qualified Data.ByteString as BS
19+
import qualified Data.ByteString as BS
2020
import Data.Hashable
21-
import Data.HashMap.Strict (HashMap)
22-
import qualified Data.HashMap.Strict as HashMap
23-
import qualified Data.List.NonEmpty as NE
24-
import qualified Data.Text as T
25-
import qualified Data.Text.Encoding as Encoding
26-
import qualified Data.Text.Utf16.Rope as Rope
21+
import Data.HashMap.Strict (HashMap)
22+
import qualified Data.HashMap.Strict as HashMap
23+
import qualified Data.List.NonEmpty as NE
24+
import qualified Data.Text.Encoding as Encoding
25+
import qualified Data.Text.Utf16.Rope as Rope
2726
import Data.Typeable
28-
import Development.IDE as D
29-
import Development.IDE.Core.Shake (restartShakeSession)
30-
import qualified Development.IDE.Core.Shake as Shake
31-
import Development.IDE.Graph (alwaysRerun)
32-
import Distribution.Compat.Lens ((^.))
33-
import Distribution.Simple.PackageDescription (readGenericPackageDescription)
34-
import Distribution.Verbosity (silent)
27+
import Development.IDE as D
28+
import Development.IDE.Core.Shake (restartShakeSession)
29+
import qualified Development.IDE.Core.Shake as Shake
30+
import Development.IDE.Graph (alwaysRerun)
31+
import Distribution.Compat.Lens ((^.))
3532
import GHC.Generics
3633
import Ide.Plugin.Cabal.Completions
37-
import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
38-
import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
39-
import qualified Ide.Plugin.Cabal.Parse as Parse
34+
import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
35+
import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
36+
import qualified Ide.Plugin.Cabal.Parse as Parse
4037
import Ide.Types
41-
import qualified Language.LSP.Server as LSP
38+
import qualified Language.LSP.Server as LSP
4239
import Language.LSP.Types
43-
import qualified Language.LSP.Types as J
44-
import qualified Language.LSP.Types as LSP
45-
import qualified Language.LSP.Types.Lens as JL
46-
import Language.LSP.VFS (VirtualFile)
47-
import qualified Language.LSP.VFS as VFS
40+
import qualified Language.LSP.Types as J
41+
import qualified Language.LSP.Types as LSP
42+
import qualified Language.LSP.Types.Lens as JL
43+
import Language.LSP.VFS (VirtualFile)
44+
import qualified Language.LSP.VFS as VFS
45+
4846
data Log
4947
= LogModificationTime NormalizedFilePath FileVersion
5048
| LogShake Shake.Log
@@ -53,12 +51,12 @@ data Log
5351
| LogDocSaved Uri
5452
| LogDocClosed Uri
5553
| LogFOI (HashMap NormalizedFilePath FileOfInterestStatus)
56-
deriving Show
54+
deriving (Show)
5755

5856
instance Pretty Log where
5957
pretty = \case
6058
LogShake log' -> pretty log'
61-
LogModificationTime nfp modTime ->
59+
LogModificationTime nfp modTime ->
6260
"Modified:" <+> pretty (fromNormalizedFilePath nfp) <+> pretty (show modTime)
6361
LogDocOpened uri ->
6462
"Opened text document:" <+> pretty (getUri uri)
@@ -71,56 +69,56 @@ instance Pretty Log where
7169
LogFOI files ->
7270
"Set files of interest to:" <+> viaShow files
7371

74-
7572
descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState
76-
descriptor recorder plId = (defaultCabalPluginDescriptor plId)
77-
{ pluginRules = cabalRules recorder
78-
, pluginHandlers = mkPluginHandler STextDocumentCodeAction licenseSuggestCodeAction
79-
<> mkPluginHandler J.STextDocumentCompletion completion
80-
, pluginNotificationHandlers = mconcat
81-
[ mkPluginNotificationHandler LSP.STextDocumentDidOpen $
82-
\ide vfs _ (DidOpenTextDocumentParams TextDocumentItem{_uri,_version}) -> liftIO $ do
83-
whenUriFile _uri $ \file -> do
84-
log' Debug $ LogDocOpened _uri
85-
addFileOfInterest recorder ide file Modified{firstOpen=True}
86-
restartCabalShakeSession (shakeExtras ide) vfs file "(opened)"
87-
88-
, mkPluginNotificationHandler LSP.STextDocumentDidChange $
89-
\ide vfs _ (DidChangeTextDocumentParams VersionedTextDocumentIdentifier{_uri} _) -> liftIO $ do
90-
whenUriFile _uri $ \file -> do
91-
log' Debug $ LogDocModified _uri
92-
addFileOfInterest recorder ide file Modified{firstOpen=False}
93-
restartCabalShakeSession (shakeExtras ide) vfs file "(changed)"
94-
95-
, mkPluginNotificationHandler LSP.STextDocumentDidSave $
96-
\ide vfs _ (DidSaveTextDocumentParams TextDocumentIdentifier{_uri} _) -> liftIO $ do
97-
whenUriFile _uri $ \file -> do
98-
log' Debug $ LogDocSaved _uri
99-
addFileOfInterest recorder ide file OnDisk
100-
restartCabalShakeSession (shakeExtras ide) vfs file "(saved)"
101-
102-
, mkPluginNotificationHandler LSP.STextDocumentDidClose $
103-
\ide vfs _ (DidCloseTextDocumentParams TextDocumentIdentifier{_uri}) -> liftIO $ do
104-
whenUriFile _uri $ \file -> do
105-
log' Debug $ LogDocClosed _uri
106-
deleteFileOfInterest recorder ide file
107-
restartCabalShakeSession (shakeExtras ide) vfs file "(closed)"
108-
]
109-
}
110-
where
111-
log' = logWith recorder
112-
113-
whenUriFile :: Uri -> (NormalizedFilePath -> IO ()) -> IO ()
114-
whenUriFile uri act = whenJust (LSP.uriToFilePath uri) $ act . toNormalizedFilePath'
115-
116-
-- | Helper function to restart the shake session, specifically for modifying .cabal files.
117-
-- No special logic, just group up a bunch of functions you need for the base
118-
-- Notification Handlers.
119-
--
120-
-- To make sure diagnostics are up to date, we need to tell shake that the file was touched and
121-
-- needs to be re-parsed. That's what we do when we record the dirty key that our parsing
122-
-- rule depends on.
123-
-- Then we restart the shake session, so that changes to our virtual files are actually picked up.
73+
descriptor recorder plId =
74+
(defaultCabalPluginDescriptor plId)
75+
{ pluginRules = cabalRules recorder
76+
, pluginHandlers =
77+
mkPluginHandler STextDocumentCodeAction licenseSuggestCodeAction
78+
<> mkPluginHandler J.STextDocumentCompletion completion
79+
, pluginNotificationHandlers =
80+
mconcat
81+
[ mkPluginNotificationHandler LSP.STextDocumentDidOpen $
82+
\ide vfs _ (DidOpenTextDocumentParams TextDocumentItem{_uri, _version}) -> liftIO $ do
83+
whenUriFile _uri $ \file -> do
84+
log' Debug $ LogDocOpened _uri
85+
addFileOfInterest recorder ide file Modified{firstOpen = True}
86+
restartCabalShakeSession (shakeExtras ide) vfs file "(opened)"
87+
, mkPluginNotificationHandler LSP.STextDocumentDidChange $
88+
\ide vfs _ (DidChangeTextDocumentParams VersionedTextDocumentIdentifier{_uri} _) -> liftIO $ do
89+
whenUriFile _uri $ \file -> do
90+
log' Debug $ LogDocModified _uri
91+
addFileOfInterest recorder ide file Modified{firstOpen = False}
92+
restartCabalShakeSession (shakeExtras ide) vfs file "(changed)"
93+
, mkPluginNotificationHandler LSP.STextDocumentDidSave $
94+
\ide vfs _ (DidSaveTextDocumentParams TextDocumentIdentifier{_uri} _) -> liftIO $ do
95+
whenUriFile _uri $ \file -> do
96+
log' Debug $ LogDocSaved _uri
97+
addFileOfInterest recorder ide file OnDisk
98+
restartCabalShakeSession (shakeExtras ide) vfs file "(saved)"
99+
, mkPluginNotificationHandler LSP.STextDocumentDidClose $
100+
\ide vfs _ (DidCloseTextDocumentParams TextDocumentIdentifier{_uri}) -> liftIO $ do
101+
whenUriFile _uri $ \file -> do
102+
log' Debug $ LogDocClosed _uri
103+
deleteFileOfInterest recorder ide file
104+
restartCabalShakeSession (shakeExtras ide) vfs file "(closed)"
105+
]
106+
}
107+
where
108+
log' = logWith recorder
109+
110+
whenUriFile :: Uri -> (NormalizedFilePath -> IO ()) -> IO ()
111+
whenUriFile uri act = whenJust (LSP.uriToFilePath uri) $ act . toNormalizedFilePath'
112+
113+
{- | Helper function to restart the shake session, specifically for modifying .cabal files.
114+
No special logic, just group up a bunch of functions you need for the base
115+
Notification Handlers.
116+
117+
To make sure diagnostics are up to date, we need to tell shake that the file was touched and
118+
needs to be re-parsed. That's what we do when we record the dirty key that our parsing
119+
rule depends on.
120+
Then we restart the shake session, so that changes to our virtual files are actually picked up.
121+
-}
124122
restartCabalShakeSession :: ShakeExtras -> VFS.VFS -> NormalizedFilePath -> String -> IO ()
125123
restartCabalShakeSession shakeExtras vfs file actionMsg = do
126124
join $ atomically $ Shake.recordDirtyKeys shakeExtras GetModificationTime [file]
@@ -131,9 +129,9 @@ restartCabalShakeSession shakeExtras vfs file actionMsg = do
131129
-- ----------------------------------------------------------------
132130

133131
data ParseCabal = ParseCabal
134-
deriving (Eq, Show, Typeable, Generic)
132+
deriving (Eq, Show, Typeable, Generic)
135133
instance Hashable ParseCabal
136-
instance NFData ParseCabal
134+
instance NFData ParseCabal
137135

138136
type instance RuleResult ParseCabal = ()
139137

@@ -168,15 +166,16 @@ cabalRules recorder = do
168166
-- Must be careful to not impede the performance too much. Crucial to
169167
-- a snappy IDE experience.
170168
kick
171-
where
172-
log' = logWith recorder
173-
174-
-- | This is the kick function for the cabal plugin.
175-
-- We run this action, whenever we shake session us run/restarted, which triggers
176-
-- actions to produce diagnostics for cabal files.
177-
--
178-
-- It is paramount that this kick-function can be run quickly, since it is a blocking
179-
-- function invocation.
169+
where
170+
log' = logWith recorder
171+
172+
{- | This is the kick function for the cabal plugin.
173+
We run this action, whenever we shake session us run/restarted, which triggers
174+
actions to produce diagnostics for cabal files.
175+
176+
It is paramount that this kick-function can be run quickly, since it is a blocking
177+
function invocation.
178+
-}
180179
kick :: Action ()
181180
kick = do
182181
files <- HashMap.keys <$> getCabalFilesOfInterestUntracked
@@ -186,84 +185,86 @@ kick = do
186185
-- Code Actions
187186
-- ----------------------------------------------------------------
188187

189-
licenseSuggestCodeAction
190-
:: IdeState
191-
-> PluginId
192-
-> CodeActionParams
193-
-> LSP.LspM Config (Either ResponseError (ResponseResult 'TextDocumentCodeAction))
194-
licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext{_diagnostics=List diags}) =
188+
licenseSuggestCodeAction ::
189+
IdeState ->
190+
PluginId ->
191+
CodeActionParams ->
192+
LSP.LspM Config (Either ResponseError (ResponseResult 'TextDocumentCodeAction))
193+
licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext{_diagnostics = List diags}) =
195194
pure $ Right $ List $ diags >>= (fmap InR . (LicenseSuggest.licenseErrorAction uri))
196195

197196
-- ----------------------------------------------------------------
198197
-- Cabal file of Interest rules and global variable
199198
-- ----------------------------------------------------------------
200199

201-
-- | Cabal files that are currently open in the lsp-client.
202-
-- Specific actions happen when these files are saved, closed or modified,
203-
-- such as generating diagnostics, re-parsing, etc...
204-
--
205-
-- We need to store the open files to parse them again if we restart the shake session.
206-
-- Restarting of the shake session happens whenever these files are modified.
200+
{- | Cabal files that are currently open in the lsp-client.
201+
Specific actions happen when these files are saved, closed or modified,
202+
such as generating diagnostics, re-parsing, etc...
203+
204+
We need to store the open files to parse them again if we restart the shake session.
205+
Restarting of the shake session happens whenever these files are modified.
206+
-}
207207
newtype OfInterestCabalVar = OfInterestCabalVar (Var (HashMap NormalizedFilePath FileOfInterestStatus))
208208

209209
instance Shake.IsIdeGlobal OfInterestCabalVar
210210

211211
data IsCabalFileOfInterest = IsCabalFileOfInterest
212-
deriving (Eq, Show, Typeable, Generic)
212+
deriving (Eq, Show, Typeable, Generic)
213213
instance Hashable IsCabalFileOfInterest
214-
instance NFData IsCabalFileOfInterest
214+
instance NFData IsCabalFileOfInterest
215215

216216
type instance RuleResult IsCabalFileOfInterest = CabalFileOfInterestResult
217217

218218
data CabalFileOfInterestResult = NotCabalFOI | IsCabalFOI FileOfInterestStatus
219219
deriving (Eq, Show, Typeable, Generic)
220220
instance Hashable CabalFileOfInterestResult
221-
instance NFData CabalFileOfInterestResult
221+
instance NFData CabalFileOfInterestResult
222222

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

242243
getCabalFilesOfInterestUntracked :: Action (HashMap NormalizedFilePath FileOfInterestStatus)
243244
getCabalFilesOfInterestUntracked = do
244-
OfInterestCabalVar var <- Shake.getIdeGlobalAction
245-
liftIO $ readVar var
245+
OfInterestCabalVar var <- Shake.getIdeGlobalAction
246+
liftIO $ readVar var
246247

247248
addFileOfInterest :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> FileOfInterestStatus -> IO ()
248249
addFileOfInterest recorder state f v = do
249-
OfInterestCabalVar var <- Shake.getIdeGlobalState state
250-
(prev, files) <- modifyVar var $ \dict -> do
251-
let (prev, new) = HashMap.alterF (, Just v) f dict
252-
pure (new, (prev, new))
253-
when (prev /= Just v) $ do
254-
join $ atomically $ Shake.recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
255-
log' Debug $ LogFOI files
256-
where
257-
log' = logWith recorder
250+
OfInterestCabalVar var <- Shake.getIdeGlobalState state
251+
(prev, files) <- modifyVar var $ \dict -> do
252+
let (prev, new) = HashMap.alterF (,Just v) f dict
253+
pure (new, (prev, new))
254+
when (prev /= Just v) $ do
255+
join $ atomically $ Shake.recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
256+
log' Debug $ LogFOI files
257+
where
258+
log' = logWith recorder
258259

259260
deleteFileOfInterest :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> IO ()
260261
deleteFileOfInterest recorder state f = do
261-
OfInterestCabalVar var <- Shake.getIdeGlobalState state
262-
files <- modifyVar' var $ HashMap.delete f
263-
join $ atomically $ Shake.recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
264-
log' Debug $ LogFOI files
265-
where
266-
log' = logWith recorder
262+
OfInterestCabalVar var <- Shake.getIdeGlobalState state
263+
files <- modifyVar' var $ HashMap.delete f
264+
join $ atomically $ Shake.recordDirtyKeys (shakeExtras state) IsFileOfInterest [f]
265+
log' Debug $ LogFOI files
266+
where
267+
log' = logWith recorder
267268

268269
-- ----------------------------------------------------------------
269270
-- Completion
@@ -279,22 +280,17 @@ completion _ide _ complParams = do
279280
pref <- VFS.getCompletionPrefix position cnts
280281
liftIO $ result pref path cnts
281282
_ -> return $ J.List []
282-
where
283-
result :: Maybe VFS.PosPrefixInfo -> FilePath -> VirtualFile -> IO (J.List CompletionItem)
284-
result Nothing _ _ = pure $ J.List []
285-
result (Just pfix) fp cnts
286-
| Just ctx <- context = do
283+
where
284+
result :: Maybe VFS.PosPrefixInfo -> FilePath -> VirtualFile -> IO (J.List CompletionItem)
285+
result Nothing _ _ = pure $ J.List []
286+
result (Just prefix) _fp cnts
287+
| Just ctx <- context = do
287288
let completer = contextToCompleter "" ctx
288-
completions <- completer filePathPfix
289+
completions <- completer completionContext
289290
-- genPkgDesc <- readGenericPackageDescription silent fp
290-
pure $ J.List $ makeCompletionItems editRange completions
291-
| otherwise = pure $ J.List []
292-
where
293-
(Position linePos charPos) = VFS.cursorPos pfix
294-
context = getContext (Position linePos charPos) (Rope.lines $ cnts ^. VFS.file_text)
295-
filePathPfix = getFilePathCursorPrefix pfix
296-
editRange =
297-
Range
298-
(Position linePos (fromIntegral charPos - fromIntegral (T.length filePathPfix)))
299-
(Position linePos charPos)
300-
291+
pure $ J.List $ makeCompletionItems completions
292+
| otherwise = pure $ J.List []
293+
where
294+
(Position linePos charPos) = VFS.cursorPos prefix
295+
context = getContext (Position linePos charPos) (Rope.lines $ cnts ^. VFS.file_text)
296+
completionContext = getFilePathCompletionContext prefix

0 commit comments

Comments
 (0)