Skip to content

Commit 00be14b

Browse files
committed
Use context in code actions for cabal files
1 parent 8fed630 commit 00be14b

File tree

4 files changed

+95
-70
lines changed

4 files changed

+95
-70
lines changed

haskell-language-server.cabal

-1
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,6 @@ library hls-cabal-plugin
264264
, lsp-types ^>=2.3
265265
, regex-tdfa ^>=1.3.1
266266
, text
267-
, text-rope
268267
, transformers
269268
, unordered-containers >=0.2.10.0
270269
, containers

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

+57-31
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import Data.Hashable
1818
import Data.HashMap.Strict (HashMap)
1919
import qualified Data.HashMap.Strict as HashMap
2020
import qualified Data.List.NonEmpty as NE
21+
import Data.Maybe (mapMaybe)
22+
import qualified Data.Text as T
2123
import qualified Data.Text.Encoding as Encoding
2224
import Data.Typeable
2325
import Development.IDE as D
@@ -36,9 +38,9 @@ import Ide.Plugin.Cabal.Completion.Types (ParseCabalFields (
3638
ParseCabalFile (..))
3739
import qualified Ide.Plugin.Cabal.Completion.Types as Types
3840
import qualified Ide.Plugin.Cabal.Diagnostics as Diagnostics
41+
import qualified Ide.Plugin.Cabal.FieldSuggest as FieldSuggest
3942
import qualified Ide.Plugin.Cabal.LicenseSuggest as LicenseSuggest
4043
import Ide.Plugin.Cabal.Orphans ()
41-
import qualified Ide.Plugin.Cabal.FieldSuggest as FieldSuggest
4244
import qualified Ide.Plugin.Cabal.Parse as Parse
4345
import Ide.Types
4446
import qualified Language.LSP.Protocol.Lens as JL
@@ -89,7 +91,7 @@ descriptor recorder plId =
8991
mconcat
9092
[ mkPluginHandler LSP.SMethod_TextDocumentCodeAction licenseSuggestCodeAction
9193
, mkPluginHandler LSP.SMethod_TextDocumentCompletion $ completion recorder
92-
, mkPluginHandler LSP.SMethod_TextDocumentCodeAction fieldSuggestCodeAction
94+
, mkPluginHandler LSP.SMethod_TextDocumentCodeAction $ fieldSuggestCodeAction recorder
9395
]
9496
, pluginNotificationHandlers =
9597
mconcat
@@ -230,9 +232,34 @@ licenseSuggestCodeAction :: PluginMethodHandler IdeState 'LSP.Method_TextDocumen
230232
licenseSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext{_diagnostics=diags}) =
231233
pure $ InL $ diags >>= (fmap InR . LicenseSuggest.licenseErrorAction uri)
232234

233-
fieldSuggestCodeAction :: PluginMethodHandler IdeState 'LSP.Method_TextDocumentCodeAction
234-
fieldSuggestCodeAction _ _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _range CodeActionContext{_diagnostics=diags}) =
235-
pure $ InL $ diags >>= (fmap InR . FieldSuggest.fieldErrorAction uri)
235+
-- | CodeActions for correcting field names with typos in them.
236+
--
237+
-- Provides CodeActions that fix typos in field names, in both stanzas and top-level field names.
238+
-- The suggestions are computed based on the completion context, where we "move" a fake cursor
239+
-- to the end of the field name and trigger cabal file completions. The completions are then
240+
-- suggested to the user.
241+
fieldSuggestCodeAction :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'LSP.Method_TextDocumentCodeAction
242+
fieldSuggestCodeAction recorder ide _ (CodeActionParams _ _ (TextDocumentIdentifier uri) _ CodeActionContext{_diagnostics=diags}) = do
243+
vfileM <- lift (getVirtualFile $ toNormalizedUri uri)
244+
case (,) <$> vfileM <*> uriToFilePath' uri of
245+
Nothing -> pure $ InL []
246+
Just (vfile, path) -> do
247+
mFields <- liftIO $ runIdeAction "cabal-plugin.fields" (shakeExtras ide) $ useWithStaleFast ParseCabalFields $ toNormalizedFilePath path
248+
case mFields of
249+
Nothing ->
250+
pure $ InL []
251+
Just (fields, _) -> do
252+
let errorFields = mapMaybe FieldSuggest.fieldErrorName diags
253+
results <- forM errorFields (getSuggestion fields vfile path)
254+
pure $ InL $ map InR $ concat results
255+
where
256+
getSuggestion fields vfile fp (field,Diagnostic{ _range=_range@(Range (Position lineNr col) _) })= do
257+
let -- Compute where we would anticipate the cursor to be.
258+
fakeLspCursorPosition = Position lineNr (col + fromIntegral (T.length field))
259+
lspPrefixInfo = Ghcide.getCompletionPrefix fakeLspCursorPosition vfile
260+
completions <- liftIO $ produceCompletions recorder lspPrefixInfo fp fields (shakeExtras ide)
261+
let completionTexts = (fmap (^. JL.label) completions)
262+
pure $ FieldSuggest.fieldErrorAction uri field completionTexts _range
236263

237264
-- ----------------------------------------------------------------
238265
-- Cabal file of Interest rules and global variable
@@ -326,32 +353,31 @@ completion recorder ide _ complParams = do
326353
pure . InR $ InR Null
327354
Just (fields, _) -> do
328355
let pref = Ghcide.getCompletionPrefix position cnts
329-
let res = produceCompletions pref path fields
356+
let res = produceCompletions recorder pref path fields (shakeExtras ide)
330357
liftIO $ fmap InL res
331358
Nothing -> pure . InR $ InR Null
332-
where
333-
completerRecorder = cmapWithPrio LogCompletions recorder
334359

335-
produceCompletions :: Ghcide.PosPrefixInfo -> FilePath -> [Syntax.Field Syntax.Position] -> IO [CompletionItem]
336-
produceCompletions prefix fp fields = do
337-
runMaybeT (context fields) >>= \case
338-
Nothing -> pure []
339-
Just ctx -> do
340-
logWith recorder Debug $ LogCompletionContext ctx pos
341-
let completer = Completions.contextToCompleter ctx
342-
let completerData = CompleterTypes.CompleterData
343-
{ getLatestGPD = do
344-
mGPD <- runIdeAction "cabal-plugin.modulesCompleter.gpd" (shakeExtras ide) $ useWithStaleFast ParseCabalFile $ toNormalizedFilePath fp
345-
pure $ fmap fst mGPD
346-
, cabalPrefixInfo = prefInfo
347-
, stanzaName =
348-
case fst ctx of
349-
Types.Stanza _ name -> name
350-
_ -> Nothing
351-
}
352-
completions <- completer completerRecorder completerData
353-
pure completions
354-
where
355-
pos = Ghcide.cursorPos prefix
356-
context fields = Completions.getContext completerRecorder prefInfo fields
357-
prefInfo = Completions.getCabalPrefixInfo fp prefix
360+
produceCompletions :: Recorder (WithPriority Log) -> Ghcide.PosPrefixInfo -> FilePath -> [Syntax.Field Syntax.Position] -> ShakeExtras -> IO [CompletionItem]
361+
produceCompletions recorder prefix fp fields extras = do
362+
runMaybeT (context fields) >>= \case
363+
Nothing -> pure []
364+
Just ctx -> do
365+
logWith recorder Debug $ LogCompletionContext ctx pos
366+
let completer = Completions.contextToCompleter ctx
367+
let completerData = CompleterTypes.CompleterData
368+
{ getLatestGPD = do
369+
mGPD <- runIdeAction "cabal-plugin.modulesCompleter.gpd" extras $ useWithStaleFast ParseCabalFile $ toNormalizedFilePath fp
370+
pure $ fmap fst mGPD
371+
, cabalPrefixInfo = prefInfo
372+
, stanzaName =
373+
case fst ctx of
374+
Types.Stanza _ name -> name
375+
_ -> Nothing
376+
}
377+
completions <- completer completerRecorder completerData
378+
pure completions
379+
where
380+
pos = Ghcide.cursorPos prefix
381+
context fields = Completions.getContext completerRecorder prefInfo fields
382+
prefInfo = Completions.getCabalPrefixInfo fp prefix
383+
completerRecorder = cmapWithPrio LogCompletions recorder

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

+37-37
Original file line numberDiff line numberDiff line change
@@ -3,65 +3,65 @@
33
{-# LANGUAGE FlexibleContexts #-}
44
{-# LANGUAGE LambdaCase #-}
55
{-# LANGUAGE OverloadedStrings #-}
6+
67
module Ide.Plugin.Cabal.FieldSuggest
7-
( fieldErrorSuggestion
8-
, fieldErrorAction
9-
-- * Re-exports
10-
, T.Text
11-
, Diagnostic(..)
12-
)
8+
( fieldErrorName,
9+
fieldErrorAction,
10+
-- * Re-exports
11+
T.Text,
12+
Diagnostic (..),
13+
)
1314
where
1415

1516
import qualified Data.Map.Strict as Map
1617
import qualified Data.Text as T
17-
import Language.LSP.Protocol.Types (CodeAction (CodeAction),
18+
import Language.LSP.Protocol.Types (CodeAction (..),
1819
CodeActionKind (..),
19-
Diagnostic (..),
20-
Position (Position),
21-
Range (Range),
22-
TextEdit (TextEdit), Uri,
23-
WorkspaceEdit (WorkspaceEdit))
20+
Diagnostic (..), Position (..),
21+
Range (..), TextEdit (..), Uri,
22+
WorkspaceEdit (..))
2423
import Text.Regex.TDFA
2524

26-
-- | Given a diagnostic returned by 'Ide.Plugin.Cabal.Diag.errorDiagnostic',
27-
-- if it represents an "Unknown field"-error along
28-
-- with a incorrect field, then return a 'CodeAction' for replacing the
29-
-- the incorrect field with the suggestion.
30-
-- It should be context sensitive, but for now it isn't
25+
-- | Generate all code action for given file, error field in position and suggestions
3126
fieldErrorAction
3227
:: Uri
3328
-- ^ File for which the diagnostic was generated
34-
-> Diagnostic
35-
-- ^ Output of 'Ide.Plugin.Cabal.Diag.errorDiagnostic'
36-
-> [CodeAction]
37-
fieldErrorAction uri diag =
38-
mkCodeAction <$> fieldErrorSuggestion diag
29+
-> T.Text
30+
-- ^ Original field
31+
-> [T.Text]
32+
-- ^ Suggestions
33+
-> Range
34+
-- ^ Location of diagnostic
35+
-> [CodeAction]
36+
fieldErrorAction uri original suggestions range =
37+
fmap mkCodeAction suggestions
3938
where
40-
mkCodeAction (original, suggestion) =
39+
mkCodeAction suggestion =
4140
let
4241
-- Range returned by cabal here represents fragment from start of
4342
-- offending identifier to end of line, we modify it to the end of identifier
44-
adjustRange (Range rangeFrom@(Position line col) _) =
45-
Range rangeFrom (Position line (col + fromIntegral (T.length original)))
46-
title = "Replace with " <> suggestion
47-
tedit = [TextEdit (adjustRange $ _range diag) suggestion]
43+
adjustRange (Range rangeFrom@(Position lineNr col) _) =
44+
Range rangeFrom (Position lineNr (col + fromIntegral (T.length original)))
45+
title = "Replace with " <> suggestion'
46+
tedit = [TextEdit (adjustRange range ) suggestion']
4847
edit = WorkspaceEdit (Just $ Map.singleton uri tedit) Nothing Nothing
4948
in CodeAction title (Just CodeActionKind_QuickFix) (Just []) Nothing Nothing (Just edit) Nothing Nothing
49+
where
50+
-- dropping colon from the end of suggestion
51+
suggestion' = T.dropEnd 1 suggestion
5052

5153
-- | Given a diagnostic returned by 'Ide.Plugin.Cabal.Diag.errorDiagnostic',
5254
-- if it represents an "Unknown field"- error with incorrect identifier
53-
-- then return the suggestion (for now placeholder "name")
54-
-- along with the incorrect identifier.
55-
--
56-
fieldErrorSuggestion
57-
:: Diagnostic
55+
-- then return the incorrect identifier together with original diagnostics.
56+
fieldErrorName ::
57+
Diagnostic ->
5858
-- ^ Output of 'Ide.Plugin.Cabal.Diag.errorDiagnostic'
59-
-> [(T.Text, T.Text)]
60-
-- ^ (Original (incorrect) license identifier, suggested replacement)
61-
fieldErrorSuggestion diag =
59+
Maybe (T.Text, Diagnostic)
60+
-- ^ Original (incorrect) field name with the suggested replacement
61+
fieldErrorName diag =
6262
mSuggestion (_message diag) >>= \case
63-
[original] -> [(original, "name")]
64-
_ -> []
63+
[original] -> Just (original, diag)
64+
_ -> Nothing
6565
where
6666
regex :: T.Text
6767
regex = "Unknown field: \"(.*)\""

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ licenseErrorSuggestion ::
7272
T.Text
7373
-- ^ Output of 'Ide.Plugin.Cabal.Diag.errorDiagnostic'
7474
-> [(T.Text, T.Text)]
75-
-- ^ (Original (incorrect) license identifier, suggested replacement)
75+
-- ^ Original (incorrect) license identifier with the suggested replacement
7676
licenseErrorSuggestion msg =
7777
(getMatch <$> msg =~~ regex) >>= \case
7878
[original] ->

0 commit comments

Comments
 (0)