Skip to content

Commit 6f775e9

Browse files
Resolve 1: Support for resolve in overloaded-record-dot (#3658)
* resolve for overloaded-record-dot (checkpoint) * resolve support works on VSCode (tests need to be redone) * Tests for both resolve and non resolve variants * Added more tests * Fix merge mistakes; move function to hls-test-utils * Remove codeLens resolve Fix codeActionResolve combine responses * Don't use partial functions * Implement michaelpj's suggestions * Make owned resolve data transparent to the plugins * Improve ord's resolve handler's error handling * Oh well, if only we had MonadFail * Generic support for resolve in hls packages * Add a new code action resolve helper that falls backs to commands * add resolve capability set to hls-test-utils * use caps defined at hls-test-utils * Add code lens resolve support * Improve comments * remove Benchmark as it wasn't that useful and triggered a lsp-test bug --------- Co-authored-by: Michael Peyton Jones <[email protected]>
1 parent 90b18ee commit 6f775e9

File tree

3 files changed

+144
-44
lines changed

3 files changed

+144
-44
lines changed

plugins/hls-overloaded-record-dot-plugin/hls-overloaded-record-dot-plugin.cabal

+5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ library
3030
exposed-modules: Ide.Plugin.OverloadedRecordDot
3131
build-depends:
3232
, base >=4.16 && <5
33+
, aeson
3334
, ghcide
3435
, hls-plugin-api
3536
, lsp
@@ -58,8 +59,12 @@ test-suite tests
5859
build-depends:
5960
, base
6061
, filepath
62+
, ghcide
6163
, text
6264
, hls-overloaded-record-dot-plugin
65+
, lens
6366
, lsp-test
67+
, lsp-types
68+
, row-types
6469
, hls-test-utils
6570

plugins/hls-overloaded-record-dot-plugin/src/Ide/Plugin/OverloadedRecordDot.hs

+84-29
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,24 @@ module Ide.Plugin.OverloadedRecordDot
1313

1414
-- based off of Berk Okzuturk's hls-explicit-records-fields-plugin
1515

16-
import Control.Lens ((^.))
16+
import Control.Lens (_Just, (^.), (^?))
17+
import Control.Monad (replicateM)
1718
import Control.Monad.IO.Class (MonadIO, liftIO)
18-
import Control.Monad.Trans.Except (ExceptT)
19+
import Control.Monad.Trans.Class (lift)
20+
import Control.Monad.Trans.Except (ExceptT, throwE)
21+
import Data.Aeson (FromJSON, Result (..),
22+
ToJSON, fromJSON, toJSON)
1923
import Data.Generics (GenericQ, everything,
2024
everythingBut, mkQ)
25+
import qualified Data.IntMap.Strict as IntMap
2126
import qualified Data.Map as Map
22-
import Data.Maybe (mapMaybe, maybeToList)
27+
import Data.Maybe (fromJust, mapMaybe,
28+
maybeToList)
2329
import Data.Text (Text)
30+
import Data.Unique (hashUnique, newUnique)
2431
import Development.IDE (IdeState,
2532
NormalizedFilePath,
33+
NormalizedUri,
2634
Pretty (..), Range,
2735
Recorder (..), Rules,
2836
WithPriority (..),
@@ -75,18 +83,22 @@ import Ide.Types (PluginDescriptor (..),
7583
PluginId (..),
7684
PluginMethodHandler,
7785
defaultPluginDescriptor,
86+
mkCodeActionHandlerWithResolve,
7887
mkPluginHandler)
88+
import Language.LSP.Protocol.Lens (HasChanges (changes))
7989
import qualified Language.LSP.Protocol.Lens as L
8090
import Language.LSP.Protocol.Message (Method (..),
8191
SMethod (..))
8292
import Language.LSP.Protocol.Types (CodeAction (..),
8393
CodeActionKind (CodeActionKind_RefactorRewrite),
8494
CodeActionParams (..),
8595
Command, TextEdit (..),
86-
WorkspaceEdit (WorkspaceEdit),
96+
Uri (..),
97+
WorkspaceEdit (WorkspaceEdit, _changeAnnotations, _changes, _documentChanges),
8798
fromNormalizedUri,
8899
normalizedFilePathToUri,
89100
type (|?) (..))
101+
import Language.LSP.Server (getClientCapabilities)
90102
data Log
91103
= LogShake Shake.Log
92104
| LogCollectedRecordSelectors [RecordSelectorExpr]
@@ -105,7 +117,14 @@ instance Hashable CollectRecordSelectors
105117
instance NFData CollectRecordSelectors
106118

107119
data CollectRecordSelectorsResult = CRSR
108-
{ recordInfos :: RangeMap RecordSelectorExpr
120+
{ -- |We store everything in here that we need to create the unresolved
121+
-- codeAction: the range, an uniquely identifiable int, and the selector
122+
--selector expression (HSExpr) that we use to generate the name
123+
records :: RangeMap (Int, HsExpr (GhcPass 'Renamed))
124+
-- |This is for when we need to fully generate a textEdit. It contains the
125+
-- whole expression we are interested in indexed to the unique id we got
126+
-- from the previous field
127+
, recordInfos :: IntMap.IntMap RecordSelectorExpr
109128
, enabledExtensions :: [Extension]
110129
}
111130
deriving (Generic)
@@ -135,56 +154,85 @@ instance Pretty RecordSelectorExpr where
135154
instance NFData RecordSelectorExpr where
136155
rnf = rwhnf
137156

157+
-- |The data that is serialized and placed in the data field of resolvable
158+
-- code actions
159+
data ORDResolveData = ORDRD {
160+
-- |We need the uri to get shake results
161+
uri :: Uri
162+
-- |The unique id that allows us to find the specific codeAction we want
163+
, uniqueID :: Int
164+
} deriving (Generic, Show)
165+
instance ToJSON ORDResolveData
166+
instance FromJSON ORDResolveData
167+
138168
descriptor :: Recorder (WithPriority Log) -> PluginId
139169
-> PluginDescriptor IdeState
140170
descriptor recorder plId = (defaultPluginDescriptor plId)
141171
{ pluginHandlers =
142-
mkPluginHandler SMethod_TextDocumentCodeAction codeActionProvider
172+
mkCodeActionHandlerWithResolve codeActionProvider resolveProvider
143173
, pluginRules = collectRecSelsRule recorder
144174
}
145175

176+
resolveProvider :: PluginMethodHandler IdeState 'Method_CodeActionResolve
177+
resolveProvider ideState pId ca@(CodeAction _ _ _ _ _ _ _ (Just resData)) =
178+
pluginResponse $ do
179+
case fromJSON resData of
180+
Success (ORDRD uri int) -> do
181+
nfp <- getNormalizedFilePath uri
182+
CRSR _ crsDetails exts <- collectRecSelResult ideState nfp
183+
pragma <- getFirstPragma pId ideState nfp
184+
case IntMap.lookup int crsDetails of
185+
Just rse -> pure $ ca {_edit = mkWorkspaceEdit uri rse exts pragma}
186+
-- We need to throw a content modified error here, see
187+
-- https://github.com/microsoft/language-server-protocol/issues/1738
188+
-- but we need fendor's plugin error response pr to make it
189+
-- convenient to use here, so we will wait to do that till that's merged
190+
_ -> throwE "Content Modified Error"
191+
_ -> throwE "Unable to deserialize the data"
192+
146193
codeActionProvider :: PluginMethodHandler IdeState 'Method_TextDocumentCodeAction
147194
codeActionProvider ideState pId (CodeActionParams _ _ caDocId caRange _) =
148195
pluginResponse $ do
149196
nfp <- getNormalizedFilePath (caDocId ^. L.uri)
150-
pragma <- getFirstPragma pId ideState nfp
151-
CRSR crsMap exts <- collectRecSelResult ideState nfp
152-
let pragmaEdit =
153-
if OverloadedRecordDot `elem` exts
154-
then Nothing
155-
else Just $ insertNewPragma pragma OverloadedRecordDot
156-
edits crs = convertRecordSelectors crs : maybeToList pragmaEdit
157-
changes crs =
158-
Just $ Map.singleton (fromNormalizedUri
159-
(normalizedFilePathToUri nfp))
160-
(edits crs)
161-
mkCodeAction crs = InR CodeAction
197+
CRSR crsMap crsDetails exts <- collectRecSelResult ideState nfp
198+
let mkCodeAction (crsM, nse) = InR CodeAction
162199
{ -- We pass the record selector to the title function, so that
163200
-- we can have the name of the record selector in the title of
164201
-- the codeAction. This allows the user can easily distinguish
165202
-- between the different codeActions when using nested record
166203
-- selectors, the disadvantage is we need to print out the
167204
-- name of the record selector which will decrease performance
168-
_title = mkCodeActionTitle exts crs
205+
_title = mkCodeActionTitle exts crsM nse
169206
, _kind = Just CodeActionKind_RefactorRewrite
170207
, _diagnostics = Nothing
171208
, _isPreferred = Nothing
172209
, _disabled = Nothing
173-
, _edit = Just $ WorkspaceEdit (changes crs) Nothing Nothing
210+
, _edit = Nothing
174211
, _command = Nothing
175-
, _data_ = Nothing
212+
, _data_ = Just $ toJSON $ ORDRD (caDocId ^. L.uri) crsM
176213
}
177214
actions = map mkCodeAction (RangeMap.filterByRange caRange crsMap)
178215
pure $ InL actions
179216
where
180-
mkCodeActionTitle :: [Extension] -> RecordSelectorExpr-> Text
181-
mkCodeActionTitle exts (RecordSelectorExpr _ se _) =
217+
mkCodeActionTitle :: [Extension] -> Int -> HsExpr (GhcPass 'Renamed) -> Text
218+
mkCodeActionTitle exts crsM se =
182219
if OverloadedRecordDot `elem` exts
183220
then title
184221
else title <> " (needs extension: OverloadedRecordDot)"
185222
where
186-
title = "Convert `" <> name <> "` to record dot syntax"
187-
name = printOutputable se
223+
title = "Convert `" <> printOutputable se <> "` to record dot syntax"
224+
225+
mkWorkspaceEdit:: Uri -> RecordSelectorExpr -> [Extension] -> NextPragmaInfo-> Maybe WorkspaceEdit
226+
mkWorkspaceEdit uri recSel exts pragma =
227+
Just $ WorkspaceEdit
228+
{ _changes =
229+
Just (Map.singleton uri (convertRecordSelectors recSel : maybeToList pragmaEdit))
230+
, _documentChanges = Nothing
231+
, _changeAnnotations = Nothing}
232+
where pragmaEdit =
233+
if OverloadedRecordDot `elem` exts
234+
then Nothing
235+
else Just $ insertNewPragma pragma OverloadedRecordDot
188236

189237
collectRecSelsRule :: Recorder (WithPriority Log) -> Rules ()
190238
collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $
@@ -201,11 +249,17 @@ collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $
201249
-- the OverloadedRecordDot pragma
202250
exts = getEnabledExtensions tmr
203251
recSels = mapMaybe (rewriteRange pm) (getRecordSelectors tmr)
252+
-- We are creating a list as long as our rec selectors of unique int s
253+
-- created by calling hashUnique on a Unique. The reason why we are
254+
-- extracting the ints is because they don't need any work to serialize.
255+
uniques <- liftIO $ replicateM (length recSels) (hashUnique <$> newUnique)
204256
logWith recorder Debug (LogCollectedRecordSelectors recSels)
205-
let -- We need the rangeMap to be able to filter by range later
206-
crsMap :: RangeMap RecordSelectorExpr
207-
crsMap = RangeMap.fromList location recSels
208-
pure ([], CRSR <$> Just crsMap <*> Just exts)
257+
let crsUniquesAndDetails = zip uniques recSels
258+
-- We need the rangeMap to be able to filter by range later
259+
rangeAndUnique = toRangeAndUnique <$> crsUniquesAndDetails
260+
crsMap :: RangeMap (Int, HsExpr (GhcPass 'Renamed))
261+
crsMap = RangeMap.fromList' rangeAndUnique
262+
pure ([], CRSR <$> Just crsMap <*> Just (IntMap.fromList crsUniquesAndDetails) <*> Just exts)
209263
where getEnabledExtensions :: TcModuleResult -> [Extension]
210264
getEnabledExtensions = getExtensions . tmrParsed
211265
getRecordSelectors :: TcModuleResult -> [RecordSelectorExpr]
@@ -217,6 +271,7 @@ collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $
217271
case toCurrentRange pm (location recSel) of
218272
Just newLoc -> Just $ recSel{location = newLoc}
219273
Nothing -> Nothing
274+
toRangeAndUnique (id, RecordSelectorExpr l (unLoc -> se) _) = (l, (id, se))
220275

221276
convertRecordSelectors :: RecordSelectorExpr -> TextEdit
222277
convertRecordSelectors (RecordSelectorExpr l se re) =

plugins/hls-overloaded-record-dot-plugin/test/Main.hs

+55-15
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,64 @@
55

66
module Main ( main ) where
77

8+
import Control.Lens ((^.))
89
import Data.Either (rights)
10+
import Data.Functor (void)
11+
import Data.Maybe (isNothing)
12+
import Data.Row
913
import qualified Data.Text as T
14+
import qualified Data.Text.Lazy as TL
15+
import qualified Data.Text.Lazy.Encoding as TL
16+
import Development.IDE.Types.Logger (Doc, Logger (Logger),
17+
Pretty (pretty),
18+
Priority (Debug),
19+
Recorder (Recorder, logger_),
20+
WithPriority (WithPriority, priority),
21+
cfilter, cmapWithPrio,
22+
makeDefaultStderrRecorder)
1023
import qualified Ide.Plugin.OverloadedRecordDot as OverloadedRecordDot
11-
import System.FilePath ((</>))
24+
import Language.LSP.Protocol.Lens as L
25+
import System.FilePath ((<.>), (</>))
1226
import Test.Hls
13-
27+
import Test.Hls.Util (codeActionNoResolveCaps,
28+
codeActionResolveCaps)
1429

1530
main :: IO ()
16-
main = defaultTestRunner test
31+
main =
32+
defaultTestRunner test
1733

1834
plugin :: PluginTestDescriptor OverloadedRecordDot.Log
1935
plugin = mkPluginTestDescriptor OverloadedRecordDot.descriptor "overloaded-record-dot"
2036

2137
test :: TestTree
2238
test = testGroup "overloaded-record-dot"
23-
[ mkTest "Simple" "Simple" "name" 10 7 10 15,
24-
mkTest "NoPragmaNeeded" "NoPragmaNeeded" "name" 11 7 11 15,
25-
mkTest "NestedParens" "NestedParens" "name" 15 7 15 24,
26-
mkTest "NestedDot" "NestedDot" "name" 17 7 17 22,
27-
mkTest "NestedDollar" "NestedDollar" "name" 15 7 15 24,
28-
mkTest "MultilineCase" "MultilineCase" "name" 10 7 12 15,
29-
mkTest "Multiline" "Multiline" "name" 10 7 11 15,
30-
mkTest "MultilineExpanded" "MultilineExpanded" "owner" 28 8 28 19
31-
]
32-
33-
mkTest :: TestName -> FilePath -> T.Text -> UInt -> UInt -> UInt -> UInt -> TestTree
39+
(mkTest "Simple" "Simple" "name" 10 7 10 15
40+
<> mkTest "NoPragmaNeeded" "NoPragmaNeeded" "name" 11 7 11 15
41+
<> mkTest "NestedParens" "NestedParens" "name" 15 7 15 24
42+
<> mkTest "NestedDot" "NestedDot" "name" 17 7 17 22
43+
<> mkTest "NestedDollar" "NestedDollar" "name" 15 7 15 24
44+
<> mkTest "MultilineCase" "MultilineCase" "name" 10 7 12 15
45+
<> mkTest "Multiline" "Multiline" "name" 10 7 11 15
46+
<> mkTest "MultilineExpanded" "MultilineExpanded" "owner" 28 8 28 19)
47+
48+
mkTest :: TestName -> FilePath -> T.Text -> UInt -> UInt -> UInt -> UInt -> [TestTree]
3449
mkTest title fp selectorName x1 y1 x2 y2 =
35-
goldenWithHaskellDoc plugin title testDataDir fp "expected" "hs" $ \doc -> do
50+
[mkNoResolveTest (title <> " without resolve") fp selectorName x1 y1 x2 y2,
51+
mkResolveTest (title <> " with resolve") fp selectorName x1 y1 x2 y2]
52+
53+
mkNoResolveTest :: TestName -> FilePath -> T.Text -> UInt -> UInt -> UInt -> UInt -> TestTree
54+
mkNoResolveTest title fp selectorName x1 y1 x2 y2 =
55+
goldenWithHaskellAndCaps codeActionNoResolveCaps plugin title testDataDir fp "expected" "hs" $ \doc -> do
3656
(act:_) <- getExplicitFieldsActions doc selectorName x1 y1 x2 y2
3757
executeCodeAction act
3858

59+
mkResolveTest :: TestName -> FilePath -> T.Text -> UInt -> UInt -> UInt -> UInt -> TestTree
60+
mkResolveTest title fp selectorName x1 y1 x2 y2 =
61+
goldenWithHaskellAndCaps codeActionResolveCaps plugin title testDataDir fp "expected" "hs" $ \doc -> do
62+
((Right act):_) <- getAndResolveExplicitFieldsActions doc selectorName x1 y1 x2 y2
63+
executeCodeAction act
64+
65+
3966
getExplicitFieldsActions
4067
:: TextDocumentIdentifier
4168
-> T.Text
@@ -46,6 +73,19 @@ getExplicitFieldsActions doc selectorName x1 y1 x2 y2 =
4673
where
4774
range = Range (Position x1 y1) (Position x2 y2)
4875

76+
getAndResolveExplicitFieldsActions
77+
:: TextDocumentIdentifier
78+
-> T.Text
79+
-> UInt -> UInt -> UInt -> UInt
80+
-> Session [Either ResponseError CodeAction]
81+
getAndResolveExplicitFieldsActions doc selectorName x1 y1 x2 y2 = do
82+
actions <- findExplicitFieldsAction selectorName <$> getCodeActions doc range
83+
rsp <- mapM (request SMethod_CodeActionResolve) (filter (\x -> isNothing (x ^. L.edit)) actions)
84+
pure $ (^. L.result) <$> rsp
85+
86+
where
87+
range = Range (Position x1 y1) (Position x2 y2)
88+
4989
findExplicitFieldsAction :: T.Text -> [a |? CodeAction] -> [CodeAction]
5090
findExplicitFieldsAction selectorName = filter (isExplicitFieldsCodeAction selectorName) . rights . map toEither
5191

0 commit comments

Comments
 (0)