@@ -13,16 +13,24 @@ module Ide.Plugin.OverloadedRecordDot
13
13
14
14
-- based off of Berk Okzuturk's hls-explicit-records-fields-plugin
15
15
16
- import Control.Lens ((^.) )
16
+ import Control.Lens (_Just , (^.) , (^?) )
17
+ import Control.Monad (replicateM )
17
18
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 )
19
23
import Data.Generics (GenericQ , everything ,
20
24
everythingBut , mkQ )
25
+ import qualified Data.IntMap.Strict as IntMap
21
26
import qualified Data.Map as Map
22
- import Data.Maybe (mapMaybe , maybeToList )
27
+ import Data.Maybe (fromJust , mapMaybe ,
28
+ maybeToList )
23
29
import Data.Text (Text )
30
+ import Data.Unique (hashUnique , newUnique )
24
31
import Development.IDE (IdeState ,
25
32
NormalizedFilePath ,
33
+ NormalizedUri ,
26
34
Pretty (.. ), Range ,
27
35
Recorder (.. ), Rules ,
28
36
WithPriority (.. ),
@@ -75,18 +83,22 @@ import Ide.Types (PluginDescriptor (..),
75
83
PluginId (.. ),
76
84
PluginMethodHandler ,
77
85
defaultPluginDescriptor ,
86
+ mkCodeActionHandlerWithResolve ,
78
87
mkPluginHandler )
88
+ import Language.LSP.Protocol.Lens (HasChanges (changes ))
79
89
import qualified Language.LSP.Protocol.Lens as L
80
90
import Language.LSP.Protocol.Message (Method (.. ),
81
91
SMethod (.. ))
82
92
import Language.LSP.Protocol.Types (CodeAction (.. ),
83
93
CodeActionKind (CodeActionKind_RefactorRewrite ),
84
94
CodeActionParams (.. ),
85
95
Command , TextEdit (.. ),
86
- WorkspaceEdit (WorkspaceEdit ),
96
+ Uri (.. ),
97
+ WorkspaceEdit (WorkspaceEdit , _changeAnnotations , _changes , _documentChanges ),
87
98
fromNormalizedUri ,
88
99
normalizedFilePathToUri ,
89
100
type (|? ) (.. ))
101
+ import Language.LSP.Server (getClientCapabilities )
90
102
data Log
91
103
= LogShake Shake. Log
92
104
| LogCollectedRecordSelectors [RecordSelectorExpr ]
@@ -105,7 +117,14 @@ instance Hashable CollectRecordSelectors
105
117
instance NFData CollectRecordSelectors
106
118
107
119
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
109
128
, enabledExtensions :: [Extension ]
110
129
}
111
130
deriving (Generic )
@@ -135,56 +154,85 @@ instance Pretty RecordSelectorExpr where
135
154
instance NFData RecordSelectorExpr where
136
155
rnf = rwhnf
137
156
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
+
138
168
descriptor :: Recorder (WithPriority Log ) -> PluginId
139
169
-> PluginDescriptor IdeState
140
170
descriptor recorder plId = (defaultPluginDescriptor plId)
141
171
{ pluginHandlers =
142
- mkPluginHandler SMethod_TextDocumentCodeAction codeActionProvider
172
+ mkCodeActionHandlerWithResolve codeActionProvider resolveProvider
143
173
, pluginRules = collectRecSelsRule recorder
144
174
}
145
175
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
+
146
193
codeActionProvider :: PluginMethodHandler IdeState 'Method_TextDocumentCodeAction
147
194
codeActionProvider ideState pId (CodeActionParams _ _ caDocId caRange _) =
148
195
pluginResponse $ do
149
196
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
162
199
{ -- We pass the record selector to the title function, so that
163
200
-- we can have the name of the record selector in the title of
164
201
-- the codeAction. This allows the user can easily distinguish
165
202
-- between the different codeActions when using nested record
166
203
-- selectors, the disadvantage is we need to print out the
167
204
-- name of the record selector which will decrease performance
168
- _title = mkCodeActionTitle exts crs
205
+ _title = mkCodeActionTitle exts crsM nse
169
206
, _kind = Just CodeActionKind_RefactorRewrite
170
207
, _diagnostics = Nothing
171
208
, _isPreferred = Nothing
172
209
, _disabled = Nothing
173
- , _edit = Just $ WorkspaceEdit (changes crs) Nothing Nothing
210
+ , _edit = Nothing
174
211
, _command = Nothing
175
- , _data_ = Nothing
212
+ , _data_ = Just $ toJSON $ ORDRD (caDocId ^. L. uri) crsM
176
213
}
177
214
actions = map mkCodeAction (RangeMap. filterByRange caRange crsMap)
178
215
pure $ InL actions
179
216
where
180
- mkCodeActionTitle :: [Extension ] -> RecordSelectorExpr -> Text
181
- mkCodeActionTitle exts ( RecordSelectorExpr _ se _) =
217
+ mkCodeActionTitle :: [Extension ] -> Int -> HsExpr ( GhcPass 'Renamed) -> Text
218
+ mkCodeActionTitle exts crsM se =
182
219
if OverloadedRecordDot `elem` exts
183
220
then title
184
221
else title <> " (needs extension: OverloadedRecordDot)"
185
222
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
188
236
189
237
collectRecSelsRule :: Recorder (WithPriority Log ) -> Rules ()
190
238
collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $
@@ -201,11 +249,17 @@ collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $
201
249
-- the OverloadedRecordDot pragma
202
250
exts = getEnabledExtensions tmr
203
251
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)
204
256
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)
209
263
where getEnabledExtensions :: TcModuleResult -> [Extension ]
210
264
getEnabledExtensions = getExtensions . tmrParsed
211
265
getRecordSelectors :: TcModuleResult -> [RecordSelectorExpr ]
@@ -217,6 +271,7 @@ collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $
217
271
case toCurrentRange pm (location recSel) of
218
272
Just newLoc -> Just $ recSel{location = newLoc}
219
273
Nothing -> Nothing
274
+ toRangeAndUnique (id , RecordSelectorExpr l (unLoc -> se) _) = (l, (id , se))
220
275
221
276
convertRecordSelectors :: RecordSelectorExpr -> TextEdit
222
277
convertRecordSelectors (RecordSelectorExpr l se re) =
0 commit comments