Skip to content

Commit 46ad7dd

Browse files
committed
Refactor context search to use readFields
Instead of custom parsing of the cabal file, we use `readFields` to parse the cabal file, as accurately as cabal supports. This allows us to additionally benefit from future improvements to the cabal lexer. Then, we traverse the fields and find the most likely location of the cursor in the cabal file. Based on these results, we can then establish the context accurately.
1 parent f8379bb commit 46ad7dd

File tree

12 files changed

+481
-248
lines changed

12 files changed

+481
-248
lines changed

Diff for: ghcide/src/Development/IDE/Plugin/Completions/Logic.hs

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module Development.IDE.Plugin.Completions.Logic (
1212
, getCompletions
1313
, fromIdentInfo
1414
, getCompletionPrefix
15+
, getCompletionPrefixFromRope
1516
) where
1617

1718
import Control.Applicative
@@ -898,7 +899,10 @@ mergeListsBy cmp all_lists = merge_lists all_lists
898899

899900
-- |From the given cursor position, gets the prefix module or record for autocompletion
900901
getCompletionPrefix :: Position -> VFS.VirtualFile -> PosPrefixInfo
901-
getCompletionPrefix pos@(Position l c) (VFS.VirtualFile _ _ ropetext) =
902+
getCompletionPrefix pos (VFS.VirtualFile _ _ ropetext) = getCompletionPrefixFromRope pos ropetext
903+
904+
getCompletionPrefixFromRope :: Position -> Rope.Rope -> PosPrefixInfo
905+
getCompletionPrefixFromRope pos@(Position l c) ropetext =
902906
fromMaybe (PosPrefixInfo "" "" "" pos) $ do -- Maybe monad
903907
let headMaybe = listToMaybe
904908
lastMaybe = headMaybe . reverse

Diff for: haskell-language-server.cabal

+1
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ library hls-cabal-plugin
238238
Ide.Plugin.Cabal.Completion.Data
239239
Ide.Plugin.Cabal.Completion.Types
240240
Ide.Plugin.Cabal.LicenseSuggest
241+
Ide.Plugin.Cabal.Orphans
241242
Ide.Plugin.Cabal.Parse
242243

243244

Diff for: hls-test-utils/hls-test-utils.cabal

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ library
4646
, lens
4747
, lsp-test ^>=0.17
4848
, lsp-types ^>=2.1
49+
, neat-interpolation
4950
, safe-exceptions
5051
, tasty
5152
, tasty-expected-failure
@@ -54,6 +55,7 @@ library
5455
, tasty-rerun
5556
, temporary
5657
, text
58+
, text-rope
5759
, row-types
5860
ghc-options: -Wall -Wunused-packages
5961

Diff for: hls-test-utils/src/Test/Hls/Util.hs

+76-18
Original file line numberDiff line numberDiff line change
@@ -45,38 +45,50 @@ module Test.Hls.Util
4545
, withCurrentDirectoryInTmp
4646
, withCurrentDirectoryInTmp'
4747
, withCanonicalTempDir
48+
-- * Extract positions from input file.
49+
, extractCursorPositions
50+
, trimming
4851
)
4952
where
5053

51-
import Control.Applicative.Combinators (skipManyTill, (<|>))
52-
import Control.Exception (catch, throwIO)
53-
import Control.Lens (_Just, (&), (.~), (?~), (^.))
54+
import Control.Applicative.Combinators (skipManyTill, (<|>))
55+
import Control.Exception (catch, throwIO)
56+
import Control.Lens (_Just, (&), (.~),
57+
(?~), (^.))
5458
import Control.Monad
5559
import Control.Monad.IO.Class
56-
import qualified Data.Aeson as A
57-
import Data.Bool (bool)
60+
import qualified Data.Aeson as A
61+
import Data.Bool (bool)
5862
import Data.Default
59-
import Data.List.Extra (find)
63+
import Data.List.Extra (find)
6064
import Data.Proxy
6165
import Data.Row
62-
import qualified Data.Set as Set
63-
import qualified Data.Text as T
64-
import Development.IDE (GhcVersion (..), ghcVersion)
65-
import qualified Language.LSP.Protocol.Lens as L
66+
import qualified Data.Set as Set
67+
import qualified Data.Text as T
68+
import Development.IDE (GhcVersion (..),
69+
ghcVersion)
70+
import qualified Language.LSP.Protocol.Lens as L
6671
import Language.LSP.Protocol.Message
6772
import Language.LSP.Protocol.Types
68-
import qualified Language.LSP.Test as Test
73+
import qualified Language.LSP.Test as Test
6974
import System.Directory
7075
import System.FilePath
71-
import System.Info.Extra (isMac, isWindows)
76+
import System.Info.Extra (isMac, isWindows)
7277
import qualified System.IO.Extra
7378
import System.IO.Temp
74-
import System.Time.Extra (Seconds, sleep)
75-
import Test.Tasty (TestTree)
76-
import Test.Tasty.ExpectedFailure (expectFailBecause,
77-
ignoreTestBecause)
78-
import Test.Tasty.HUnit (Assertion, assertFailure,
79-
(@?=))
79+
import System.Time.Extra (Seconds, sleep)
80+
import Test.Tasty (TestTree)
81+
import Test.Tasty.ExpectedFailure (expectFailBecause,
82+
ignoreTestBecause)
83+
import Test.Tasty.HUnit (Assertion,
84+
assertFailure, (@?=))
85+
86+
import qualified Data.List as List
87+
import qualified Data.Text.Internal.Search as T
88+
import qualified Data.Text.Utf16.Rope.Mixed as Rope
89+
import Development.IDE.Plugin.Completions.Logic (getCompletionPrefixFromRope)
90+
import Development.IDE.Plugin.Completions.Types (PosPrefixInfo)
91+
import NeatInterpolation (trimming)
8092

8193
noLiteralCaps :: ClientCapabilities
8294
noLiteralCaps = def & L.textDocument ?~ textDocumentCaps
@@ -348,3 +360,49 @@ withCanonicalTempDir :: (FilePath -> IO a) -> IO a
348360
withCanonicalTempDir f = System.IO.Extra.withTempDir $ \dir -> do
349361
dir' <- canonicalizePath dir
350362
f dir'
363+
364+
-- ----------------------------------------------------------------------------
365+
-- Extract Position data from the source file itself.
366+
-- ----------------------------------------------------------------------------
367+
368+
data FoldState = FoldState
369+
{ foldStateRows :: !Int
370+
, foldStatePositions :: ![Position]
371+
, foldStateFinalText :: ![T.Text]
372+
}
373+
374+
emptyFoldState :: FoldState
375+
emptyFoldState = FoldState
376+
{ foldStateRows = 0
377+
, foldStatePositions = []
378+
, foldStateFinalText = []
379+
}
380+
381+
foldStateToText :: FoldState -> T.Text
382+
foldStateToText state = T.unlines $ reverse $ foldStateFinalText state
383+
384+
addTextCursor :: FoldState -> Int -> FoldState
385+
addTextCursor state col = state
386+
{ foldStatePositions = Position (fromIntegral (foldStateRows state) - 1) (fromIntegral col) : foldStatePositions state
387+
}
388+
389+
addTextLine :: FoldState -> T.Text -> FoldState
390+
addTextLine state l = state
391+
{ foldStateFinalText = l : foldStateFinalText state
392+
, foldStateRows = foldStateRows state + 1
393+
}
394+
395+
extractCursorPositions :: T.Text -> (T.Text, [PosPrefixInfo])
396+
extractCursorPositions t =
397+
let
398+
textLines = T.lines t
399+
foldState = List.foldl' go emptyFoldState textLines
400+
finalText = foldStateToText foldState
401+
in
402+
(finalText, fmap (\pos -> getCompletionPrefixFromRope pos (Rope.fromText finalText)) $ foldStatePositions foldState)
403+
404+
where
405+
go foldState l = case T.indices "^" l of
406+
[] -> addTextLine foldState l
407+
xs -> List.foldl' addTextCursor foldState xs
408+

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

+74-38
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Control.DeepSeq
1212
import Control.Lens ((^.))
1313
import Control.Monad.Extra
1414
import Control.Monad.IO.Class
15-
import Control.Monad.Trans.Class (lift)
15+
import Control.Monad.Trans.Class
1616
import Control.Monad.Trans.Maybe (runMaybeT)
1717
import qualified Data.ByteString as BS
1818
import Data.Hashable
@@ -27,12 +27,17 @@ import qualified Development.IDE.Core.Shake as Shake
2727
import Development.IDE.Graph (alwaysRerun)
2828
import qualified Development.IDE.Plugin.Completions.Logic as Ghcide
2929
import qualified Development.IDE.Plugin.Completions.Types as Ghcide
30+
import qualified Distribution.Fields as Syntax
31+
import qualified Distribution.Parsec.Position as Syntax
3032
import GHC.Generics
3133
import qualified Ide.Plugin.Cabal.Completion.Completer.Types as CompleterTypes
3234
import qualified Ide.Plugin.Cabal.Completion.Completions as Completions
35+
import Ide.Plugin.Cabal.Completion.Types (ParseCabalFields (..),
36+
ParseCabalFile (..))
3337
import qualified Ide.Plugin.Cabal.Completion.Types as Types
3438
import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
3539
import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
40+
import Ide.Plugin.Cabal.Orphans ()
3641
import qualified Ide.Plugin.Cabal.Parse as Parse
3742
import Ide.Types
3843
import qualified Language.LSP.Protocol.Lens as JL
@@ -70,7 +75,7 @@ instance Pretty Log where
7075
"Set files of interest to:" <+> viaShow files
7176
LogCompletionContext context position ->
7277
"Determined completion context:"
73-
<+> viaShow context
78+
<+> pretty context
7479
<+> "for cursor position:"
7580
<+> pretty position
7681
LogCompletions logs -> pretty logs
@@ -144,30 +149,55 @@ cabalRules recorder plId = do
144149
-- Make sure we initialise the cabal files-of-interest.
145150
ofInterestRules recorder
146151
-- Rule to produce diagnostics for cabal files.
147-
define (cmapWithPrio LogShake recorder) $ \Types.GetCabalDiagnostics file -> do
152+
define (cmapWithPrio LogShake recorder) $ \ParseCabalFields file -> do
148153
config <- getPluginConfigAction plId
149154
if not (plcGlobalOn config && plcDiagnosticsOn config)
150-
then pure ([], Nothing)
151-
else do
152-
-- whenever this key is marked as dirty (e.g., when a user writes stuff to it),
153-
-- we rerun this rule because this rule *depends* on GetModificationTime.
154-
(t, mCabalSource) <- use_ GetFileContents file
155-
log' Debug $ LogModificationTime file t
156-
contents <- case mCabalSource of
157-
Just sources ->
158-
pure $ Encoding.encodeUtf8 sources
159-
Nothing -> do
160-
liftIO $ BS.readFile $ fromNormalizedFilePath file
161-
162-
(pWarnings, pm) <- liftIO $ Parse.parseCabalFileContents contents
163-
let warningDiags = fmap (Diagnostics.warningDiagnostic file) pWarnings
164-
case pm of
165-
Left (_cabalVersion, pErrorNE) -> do
166-
let errorDiags = NE.toList $ NE.map (Diagnostics.errorDiagnostic file) pErrorNE
167-
allDiags = errorDiags <> warningDiags
168-
pure (allDiags, Nothing)
169-
Right gpd -> do
170-
pure (warningDiags, Just gpd)
155+
then pure ([], Nothing)
156+
else do
157+
-- whenever this key is marked as dirty (e.g., when a user writes stuff to it),
158+
-- we rerun this rule because this rule *depends* on GetModificationTime.
159+
(t, mCabalSource) <- use_ GetFileContents file
160+
log' Debug $ LogModificationTime file t
161+
contents <- case mCabalSource of
162+
Just sources ->
163+
pure $ Encoding.encodeUtf8 sources
164+
Nothing -> do
165+
liftIO $ BS.readFile $ fromNormalizedFilePath file
166+
167+
case Parse.readCabalFields file contents of
168+
Left _ ->
169+
pure ([], Nothing)
170+
Right fields ->
171+
pure ([], Just fields)
172+
173+
define (cmapWithPrio LogShake recorder) $ \ParseCabalFile file -> do
174+
config <- getPluginConfigAction plId
175+
if not (plcGlobalOn config && plcDiagnosticsOn config)
176+
then pure ([], Nothing)
177+
else do
178+
-- whenever this key is marked as dirty (e.g., when a user writes stuff to it),
179+
-- we rerun this rule because this rule *depends* on GetModificationTime.
180+
(t, mCabalSource) <- use_ GetFileContents file
181+
log' Debug $ LogModificationTime file t
182+
contents <- case mCabalSource of
183+
Just sources ->
184+
pure $ Encoding.encodeUtf8 sources
185+
Nothing -> do
186+
liftIO $ BS.readFile $ fromNormalizedFilePath file
187+
188+
-- Instead of fully reparsing the sources to get a 'GenericPackageDescription',
189+
-- we would much rather re-use the already parsed results of 'ParseCabalFields'.
190+
-- Unfortunately, Cabal-syntax doesn't expose the function 'parseGenericPackageDescription''
191+
-- which allows us to resume the parsing pipeline with '[Field Position]'.
192+
(pWarnings, pm) <- liftIO $ Parse.parseCabalFileContents contents
193+
let warningDiags = fmap (Diagnostics.warningDiagnostic file) pWarnings
194+
case pm of
195+
Left (_cabalVersion, pErrorNE) -> do
196+
let errorDiags = NE.toList $ NE.map (Diagnostics.errorDiagnostic file) pErrorNE
197+
allDiags = errorDiags <> warningDiags
198+
pure (allDiags, Nothing)
199+
Right gpd -> do
200+
pure (warningDiags, Just gpd)
171201

172202
action $ do
173203
-- Run the cabal kick. This code always runs when 'shakeRestart' is run.
@@ -187,7 +217,7 @@ function invocation.
187217
kick :: Action ()
188218
kick = do
189219
files <- HashMap.keys <$> getCabalFilesOfInterestUntracked
190-
void $ uses Types.GetCabalDiagnostics files
220+
void $ uses Types.ParseCabalFile files
191221

192222
-- ----------------------------------------------------------------
193223
-- Code Actions
@@ -278,24 +308,31 @@ completion :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'LSP.M
278308
completion recorder ide _ complParams = do
279309
let (TextDocumentIdentifier uri) = complParams ^. JL.textDocument
280310
position = complParams ^. JL.position
281-
contents <- lift $ getVirtualFile $ toNormalizedUri uri
282-
case (contents, uriToFilePath' uri) of
283-
(Just cnts, Just path) -> do
284-
let pref = Ghcide.getCompletionPrefix position cnts
285-
let res = result pref path cnts
286-
liftIO $ fmap InL res
287-
_ -> pure . InR $ InR Null
311+
mVf <- lift $ getVirtualFile $ toNormalizedUri uri
312+
case (,) <$> mVf <*> uriToFilePath' uri of
313+
Just (cnts, path) -> do
314+
mFields <- liftIO $ runIdeAction "cabal-plugin.fields" (shakeExtras ide) $ useWithStaleFast ParseCabalFields $ toNormalizedFilePath path
315+
case mFields of
316+
Nothing ->
317+
pure . InR $ InR Null
318+
Just (fields, _) -> do
319+
let pref = Ghcide.getCompletionPrefix position cnts
320+
let res = produceCompletions pref path fields
321+
liftIO $ fmap InL res
322+
Nothing -> pure . InR $ InR Null
288323
where
289-
result :: Ghcide.PosPrefixInfo -> FilePath -> VFS.VirtualFile -> IO [CompletionItem]
290-
result prefix fp cnts = do
291-
runMaybeT context >>= \case
324+
completerRecorder = cmapWithPrio LogCompletions recorder
325+
326+
produceCompletions :: Ghcide.PosPrefixInfo -> FilePath -> [Syntax.Field Syntax.Position] -> IO [CompletionItem]
327+
produceCompletions prefix fp fields = do
328+
runMaybeT (context fields) >>= \case
292329
Nothing -> pure []
293330
Just ctx -> do
294331
logWith recorder Debug $ LogCompletionContext ctx pos
295332
let completer = Completions.contextToCompleter ctx
296333
let completerData = CompleterTypes.CompleterData
297334
{ getLatestGPD = do
298-
mGPD <- runIdeAction "cabal-plugin.modulesCompleter.gpd" (shakeExtras ide) $ useWithStaleFast Types.GetCabalDiagnostics $ toNormalizedFilePath fp
335+
mGPD <- runIdeAction "cabal-plugin.modulesCompleter.gpd" (shakeExtras ide) $ useWithStaleFast ParseCabalFile $ toNormalizedFilePath fp
299336
pure $ fmap fst mGPD
300337
, cabalPrefixInfo = prefInfo
301338
, stanzaName =
@@ -306,7 +343,6 @@ completion recorder ide _ complParams = do
306343
completions <- completer completerRecorder completerData
307344
pure completions
308345
where
309-
completerRecorder = cmapWithPrio LogCompletions recorder
310346
pos = Ghcide.cursorPos prefix
311-
context = Completions.getContext completerRecorder prefInfo (cnts ^. VFS.file_text)
347+
context fields = Completions.getContext completerRecorder prefInfo fields
312348
prefInfo = Completions.getCabalPrefixInfo fp prefix

0 commit comments

Comments
 (0)