-
-
Notifications
You must be signed in to change notification settings - Fork 370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Resolve 1: Support for resolve in overloaded-record-dot #3658
Changes from 7 commits
bb742b3
be71eb9
fb21134
f347ebc
c19480d
5e37f6f
4bcd45b
7d4f01e
225152e
4b34265
9985195
355e95c
2e4d14c
fb49c31
d1d299b
e025840
0b57d5a
735feca
6b3b915
1ba6098
7e9bf1d
0271ce2
794034b
7790755
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,16 +13,24 @@ module Ide.Plugin.OverloadedRecordDot | |
|
||
-- based off of Berk Okzuturk's hls-explicit-records-fields-plugin | ||
|
||
import Control.Lens ((^.)) | ||
import Control.Lens (_Just, (^.), (^?)) | ||
import Control.Monad (replicateM) | ||
import Control.Monad.IO.Class (MonadIO, liftIO) | ||
import Control.Monad.Trans.Class (lift) | ||
import Control.Monad.Trans.Except (ExceptT) | ||
import Data.Aeson (FromJSON, Result (..), | ||
ToJSON, fromJSON, toJSON) | ||
import Data.Generics (GenericQ, everything, | ||
everythingBut, mkQ) | ||
import qualified Data.IntMap.Strict as IntMap | ||
import qualified Data.Map as Map | ||
import Data.Maybe (mapMaybe, maybeToList) | ||
import Data.Maybe (fromJust, mapMaybe, | ||
maybeToList) | ||
import Data.Text (Text) | ||
import Data.Unique (hashUnique, newUnique) | ||
import Development.IDE (IdeState, | ||
NormalizedFilePath, | ||
NormalizedUri, | ||
Pretty (..), Range, | ||
Recorder (..), Rules, | ||
WithPriority (..), | ||
|
@@ -76,17 +84,20 @@ import Ide.Types (PluginDescriptor (..), | |
PluginMethodHandler, | ||
defaultPluginDescriptor, | ||
mkPluginHandler) | ||
import Language.LSP.Protocol.Lens (HasChanges (changes)) | ||
import qualified Language.LSP.Protocol.Lens as L | ||
import Language.LSP.Protocol.Message (Method (..), | ||
SMethod (..)) | ||
import Language.LSP.Protocol.Types (CodeAction (..), | ||
CodeActionKind (CodeActionKind_RefactorRewrite), | ||
CodeActionParams (..), | ||
Command, TextEdit (..), | ||
Uri (..), | ||
WorkspaceEdit (WorkspaceEdit), | ||
fromNormalizedUri, | ||
normalizedFilePathToUri, | ||
type (|?) (..)) | ||
import Language.LSP.Server (getClientCapabilities) | ||
data Log | ||
= LogShake Shake.Log | ||
| LogCollectedRecordSelectors [RecordSelectorExpr] | ||
|
@@ -105,7 +116,8 @@ instance Hashable CollectRecordSelectors | |
instance NFData CollectRecordSelectors | ||
|
||
data CollectRecordSelectorsResult = CRSR | ||
{ recordInfos :: RangeMap RecordSelectorExpr | ||
{ records :: RangeMap Int | ||
joyfulmantis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
, recordInfos :: IntMap.IntMap RecordSelectorExpr | ||
joyfulmantis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
, enabledExtensions :: [Extension] | ||
} | ||
deriving (Generic) | ||
|
@@ -135,56 +147,100 @@ instance Pretty RecordSelectorExpr where | |
instance NFData RecordSelectorExpr where | ||
rnf = rwhnf | ||
|
||
data ORDResolveData = ORDRD { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WDYT about the idea of just reusing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At least in this case, the CodeActionParams doesn't make sense. The problem with the codeActionParams is we are going to need to do processing anyways to know whether we can provide the codeAction, and right now it's a title too, so it makes sense to just process once instead of once to present and once to execute There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right. I think it definitely makes sense that we'll want a "stateful version" of resolve-based handlers. Maybe we'll also want a stateless one... I think there are some plugins that don't define any of their own rules so don't even have anywhere to put state. But perhaps easier to do a few and then refactor afterwards. |
||
uri :: Uri | ||
, uniqueID :: Int | ||
} deriving (Generic, Show) | ||
instance ToJSON ORDResolveData | ||
instance FromJSON ORDResolveData | ||
|
||
descriptor :: Recorder (WithPriority Log) -> PluginId | ||
-> PluginDescriptor IdeState | ||
descriptor recorder plId = (defaultPluginDescriptor plId) | ||
{ pluginHandlers = | ||
mkPluginHandler SMethod_TextDocumentCodeAction codeActionProvider | ||
mkPluginHandler SMethod_TextDocumentCodeAction codeActionProvider | ||
<> mkPluginHandler SMethod_CodeActionResolve resolveProvider | ||
|
||
, pluginRules = collectRecSelsRule recorder | ||
} | ||
|
||
resolveProvider :: PluginMethodHandler IdeState 'Method_CodeActionResolve | ||
resolveProvider ideState pId ca@(CodeAction _ _ _ _ _ _ _ (Just resData)) = | ||
pluginResponse $ do | ||
case fromJSON $ resData of | ||
Success (ORDRD uri int) -> do | ||
nfp <- getNormalizedFilePath uri | ||
CRSR _ crsDetails exts <- collectRecSelResult ideState nfp | ||
pragma <- getFirstPragma pId ideState nfp | ||
let pragmaEdit = | ||
if OverloadedRecordDot `elem` exts | ||
then Nothing | ||
else Just $ insertNewPragma pragma OverloadedRecordDot | ||
edits (Just crs) = convertRecordSelectors crs : maybeToList pragmaEdit | ||
edits _ = [] | ||
changes = Just $ WorkspaceEdit | ||
(Just (Map.singleton (fromNormalizedUri | ||
(normalizedFilePathToUri nfp)) | ||
(edits (IntMap.lookup int crsDetails)))) | ||
Nothing Nothing | ||
pure $ ca {_edit = changes} | ||
_ -> pure ca | ||
|
||
codeActionProvider :: PluginMethodHandler IdeState 'Method_TextDocumentCodeAction | ||
codeActionProvider ideState pId (CodeActionParams _ _ caDocId caRange _) = | ||
pluginResponse $ do | ||
nfp <- getNormalizedFilePath (caDocId ^. L.uri) | ||
pragma <- getFirstPragma pId ideState nfp | ||
CRSR crsMap exts <- collectRecSelResult ideState nfp | ||
let pragmaEdit = | ||
caps <- lift getClientCapabilities | ||
CRSR crsMap crsDetails exts <- collectRecSelResult ideState nfp | ||
let supportsResolve :: Maybe Bool | ||
supportsResolve = caps ^? L.textDocument . _Just . L.codeAction . _Just . L.dataSupport . _Just | ||
joyfulmantis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pragmaEdit = | ||
if OverloadedRecordDot `elem` exts | ||
then Nothing | ||
else Just $ insertNewPragma pragma OverloadedRecordDot | ||
edits crs = convertRecordSelectors crs : maybeToList pragmaEdit | ||
changes crs = | ||
Just $ Map.singleton (fromNormalizedUri | ||
edits (Just crs) = convertRecordSelectors crs : maybeToList pragmaEdit | ||
edits _ = [] | ||
changes crsM crsD = | ||
case supportsResolve of | ||
Just False -> Just $ WorkspaceEdit | ||
joyfulmantis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
(Just (Map.singleton (fromNormalizedUri | ||
(normalizedFilePathToUri nfp)) | ||
(edits crs) | ||
mkCodeAction crs = InR CodeAction | ||
(edits (IntMap.lookup crsM crsD)))) | ||
Nothing Nothing | ||
_ -> Nothing | ||
resolveData crsM = | ||
case supportsResolve of | ||
Just True -> Just $ toJSON $ ORDRD (caDocId ^. L.uri) crsM | ||
_ -> Nothing | ||
mkCodeAction crsD crsM = InR CodeAction | ||
{ -- We pass the record selector to the title function, so that | ||
-- we can have the name of the record selector in the title of | ||
-- the codeAction. This allows the user can easily distinguish | ||
-- between the different codeActions when using nested record | ||
-- selectors, the disadvantage is we need to print out the | ||
-- name of the record selector which will decrease performance | ||
_title = mkCodeActionTitle exts crs | ||
_title = mkCodeActionTitle exts crsM crsD | ||
, _kind = Just CodeActionKind_RefactorRewrite | ||
, _diagnostics = Nothing | ||
, _isPreferred = Nothing | ||
, _disabled = Nothing | ||
, _edit = Just $ WorkspaceEdit (changes crs) Nothing Nothing | ||
, _edit = changes crsM crsD | ||
, _command = Nothing | ||
, _data_ = Nothing | ||
, _data_ = resolveData crsM | ||
} | ||
actions = map mkCodeAction (RangeMap.filterByRange caRange crsMap) | ||
actions = map (mkCodeAction crsDetails) (RangeMap.filterByRange caRange crsMap) | ||
pure $ InL actions | ||
where | ||
mkCodeActionTitle :: [Extension] -> RecordSelectorExpr-> Text | ||
mkCodeActionTitle exts (RecordSelectorExpr _ se _) = | ||
mkCodeActionTitle :: [Extension] -> Int -> IntMap.IntMap RecordSelectorExpr-> Text | ||
mkCodeActionTitle exts crsM crsD = | ||
if OverloadedRecordDot `elem` exts | ||
then title | ||
else title <> " (needs extension: OverloadedRecordDot)" | ||
where | ||
title = "Convert `" <> name <> "` to record dot syntax" | ||
name = printOutputable se | ||
title = "Convert `" <> name (IntMap.lookup crsM crsD) <> "` to record dot syntax" | ||
name (Just (RecordSelectorExpr _ se _)) = printOutputable se | ||
name _ = "" | ||
joyfulmantis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
collectRecSelsRule :: Recorder (WithPriority Log) -> Rules () | ||
collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $ | ||
|
@@ -201,11 +257,15 @@ collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $ | |
-- the OverloadedRecordDot pragma | ||
exts = getEnabledExtensions tmr | ||
recSels = mapMaybe (rewriteRange pm) (getRecordSelectors tmr) | ||
uniques <- liftIO $ replicateM (length recSels) (hashUnique <$> newUnique) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. great There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we might have to worry about contention on the ioref if we end up using this everywhere, but I expect that won't manifest until we're using it a lot. We should remember to try and check when we're nearly done |
||
logWith recorder Debug (LogCollectedRecordSelectors recSels) | ||
let -- We need the rangeMap to be able to filter by range later | ||
crsMap :: RangeMap RecordSelectorExpr | ||
crsMap = RangeMap.fromList location recSels | ||
pure ([], CRSR <$> Just crsMap <*> Just exts) | ||
let crsDetails = IntMap.fromList $ zip uniques recSels | ||
-- We need the rangeMap to be able to filter by range later | ||
rangeAndUnique = mapM (\x -> (, x) . location <$> IntMap.lookup x crsDetails) uniques | ||
crsMap :: Maybe (RangeMap Int) | ||
crsMap = RangeMap.fromList' <$> rangeAndUnique | ||
crsDetails :: IntMap.IntMap RecordSelectorExpr | ||
pure ([], CRSR <$> crsMap <*> Just crsDetails <*> Just exts) | ||
where getEnabledExtensions :: TcModuleResult -> [Extension] | ||
getEnabledExtensions = getExtensions . tmrParsed | ||
getRecordSelectors :: TcModuleResult -> [RecordSelectorExpr] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, this made me realize that something funny may happen here. Suppose we have N code action handlers, each of which has a corresponding resolve handler. Now if one of the code action handlers produces a code action, then when the client asks to resolve it... it's going to get sent to every resolve handler. So we'll need to make sure that resolve handlers "know" if it's one of "their" code actions...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And indeed, I think we therefore don't want to combine the results of multiple resolve handlers firing. What would that even mean? Something has gone wrong if that has happened! We should get exactly one response.
Hard to do for now, but I do think that
combineResponses
should be able to throw an error in cases like this. For now I'd probably do the crappy "just take the first result" thing we do elsewhere.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So even if every plugin's resolve handler used the same type theoretically Data.Unique should allow types to know whether a resolve belongs to them (resolvable ones eventually anyways). To make sure no extra processing was needed I was thinking of having the plugin record their name or id in the type serialized to the data field, and then first match on that, to make sure no extra processing was needed.
Regarding combineResponses, I was actually originally just returning the unmodified codeAction if I was unable to resolve with the provided resolve data, which is why combineRespones would still be needed to find the modified codeAction from the list. I guess I could/should just throw an error, and that would be equivalent to returning Nothing? The plugin ultimately responsible for the resolve also needs to throw a ContentModified responseError if it can't resolve it. So not actually sure we can use pluginResponse in the end (or at least we need to modify it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, if we use standard infrastructure and tag every code action with a Unique then we probably should be fairly okay. Although it requires some state on the plugin side somewhere to remember which uniques it produced. The advantage of the "just pass the CodeActionParams again" approach is that it's easier to be stateless if you want to. Not sure if it's possible to be stateless in general, though 🤔
I think that's fine. I think we run all the handlers, throw away any that failed and then combine the results of the rest. We could potentially also use the
pluginResponsible
method for this, I think? We use it in a similar way to restrict the scope of some handlers. So if we put the responsible plugin ID in the data then I think we could restrict to just that plugin inpluginResponsible
.