Skip to content

Resolve 1: Support for resolve in overloaded-record-dot #3658

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

Merged
merged 24 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bb742b3
resolve for overloaded-record-dot (checkpoint)
joyfulmantis Jun 13, 2023
be71eb9
resolve support works on VSCode (tests need to be redone)
joyfulmantis Jun 14, 2023
fb21134
Tests for both resolve and non resolve variants
joyfulmantis Jun 15, 2023
f347ebc
Added more tests
joyfulmantis Jun 19, 2023
c19480d
Fix merge mistakes; move function to hls-test-utils
joyfulmantis Jun 19, 2023
5e37f6f
Remove codeLens resolve
joyfulmantis Jun 19, 2023
4bcd45b
Don't use partial functions
joyfulmantis Jun 21, 2023
7d4f01e
Implement michaelpj's suggestions
joyfulmantis Jun 22, 2023
225152e
Make owned resolve data transparent to the plugins
joyfulmantis Jun 26, 2023
4b34265
Improve ord's resolve handler's error handling
joyfulmantis Jun 26, 2023
9985195
Oh well, if only we had MonadFail
joyfulmantis Jun 26, 2023
355e95c
Generic support for resolve in hls packages
joyfulmantis Jun 27, 2023
2e4d14c
Merge branch 'resolve-support' into ord-resolve
joyfulmantis Jun 27, 2023
fb49c31
Add a new code action resolve helper that falls backs to commands
joyfulmantis Jun 27, 2023
d1d299b
add resolve capability set to hls-test-utils
joyfulmantis Jun 28, 2023
e025840
Merge branch 'resolve-support' into ord-resolve
joyfulmantis Jun 28, 2023
0b57d5a
use caps defined at hls-test-utils
joyfulmantis Jun 28, 2023
735feca
Add code lens resolve support
joyfulmantis Jun 29, 2023
6b3b915
Merge branch 'master' into resolve-support
michaelpj Jun 29, 2023
1ba6098
Merge branch 'resolve-support' into ord-resolve
joyfulmantis Jun 29, 2023
7e9bf1d
Merge branch 'master' into ord-resolve
michaelpj Jun 29, 2023
0271ce2
Improve comments
joyfulmantis Jun 29, 2023
794034b
remove Benchmark as it wasn't that useful and triggered a lsp-test bug
joyfulmantis Jun 30, 2023
7790755
Merge branch 'master' into ord-resolve
joyfulmantis Jun 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions hls-plugin-api/hls-plugin-api.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ library
, opentelemetry >=0.4
, optparse-applicative
, regex-tdfa >=1.3.1.0
, row-types
, text
, transformers
, unordered-containers
Expand Down
71 changes: 66 additions & 5 deletions hls-plugin-api/src/Ide/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
{-# LANGUAGE MonadComprehensions #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE PolyKinds #-}
Expand Down Expand Up @@ -47,6 +48,8 @@ module Ide.Types
, installSigUsr1Handler
, responseError
, lookupCommandProvider
, OwnedResolveData(..)
, mkCodeActionHandlerWithResolve
)
where

Expand All @@ -59,7 +62,9 @@ import System.Posix.Signals
#endif
import Control.Applicative ((<|>))
import Control.Arrow ((&&&))
import Control.Lens ((.~), (^.))
import Control.Lens (_Just, (.~), (^.), (^?))
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except (ExceptT (..), runExceptT)
import Data.Aeson hiding (Null, defaultOptions)
import Data.Default
import Data.Dependent.Map (DMap)
Expand All @@ -74,6 +79,7 @@ import Data.List.NonEmpty (NonEmpty (..), toList)
import qualified Data.Map as Map
import Data.Maybe
import Data.Ord
import Data.Row ((.!))
import Data.Semigroup
import Data.String
import qualified Data.Text as T
Expand All @@ -85,7 +91,9 @@ import Ide.Plugin.Properties
import qualified Language.LSP.Protocol.Lens as L
import Language.LSP.Protocol.Message
import Language.LSP.Protocol.Types
import Language.LSP.Server (LspM, getVirtualFile)
import Language.LSP.Server (LspM, LspT,
getClientCapabilities,
getVirtualFile)
import Language.LSP.VFS
import Numeric.Natural
import OpenTelemetry.Eventlog
Expand Down Expand Up @@ -404,7 +412,9 @@ instance PluginMethod Request Method_TextDocumentCodeAction where
uri = msgParams ^. L.textDocument . L.uri

instance PluginMethod Request Method_CodeActionResolve where
pluginEnabled _ msgParams pluginDesc config = pluginEnabledConfig plcCodeActionsOn (configForPlugin config pluginDesc)
pluginEnabled _ msgParams pluginDesc config =
pluginResolverResponsible (msgParams ^. L.data_) pluginDesc
&& pluginEnabledConfig plcCodeActionsOn (configForPlugin config pluginDesc)

instance PluginMethod Request Method_TextDocumentDefinition where
pluginEnabled _ msgParams pluginDesc _ =
Expand Down Expand Up @@ -856,7 +866,7 @@ type CommandFunction ideState a

newtype PluginId = PluginId T.Text
deriving (Show, Read, Eq, Ord)
deriving newtype (FromJSON, Hashable)
deriving newtype (ToJSON, FromJSON, Hashable)

instance IsString PluginId where
fromString = PluginId . T.pack
Expand Down Expand Up @@ -957,7 +967,6 @@ instance HasTracing WorkspaceSymbolParams where
instance HasTracing CallHierarchyIncomingCallsParams
instance HasTracing CallHierarchyOutgoingCallsParams
instance HasTracing CompletionItem
instance HasTracing CodeLens
instance HasTracing CodeAction
-- ---------------------------------------------------------------------

Expand Down Expand Up @@ -992,3 +1001,55 @@ getProcessID = fromIntegral <$> P.getProcessID

installSigUsr1Handler h = void $ installHandler sigUSR1 (Catch h) Nothing
#endif

-- |When provided with both a codeAction provider and an affiliated codeAction
-- resolve provider, this function creates a handler that automatically uses
-- your resolve provider to fill out you original codeAction if the client doesn't
-- have codeAction resolve support. This means you don't have to check whether
-- the client supports resolve and act accordingly in your own providers.
mkCodeActionHandlerWithResolve
:: (ideState -> PluginId -> CodeActionParams -> LspM Config (Either ResponseError ([Command |? CodeAction] |? Null)))
-> (ideState -> PluginId -> CodeAction -> LspM Config (Either ResponseError CodeAction))
-> PluginHandlers ideState
mkCodeActionHandlerWithResolve codeActionMethod codeResolveMethod =
let newCodeActionMethod ideState pid params = runExceptT $
do codeActionReturn <- ExceptT $ codeActionMethod ideState pid params
caps <- lift getClientCapabilities
case codeActionReturn of
r@(InR _) -> pure r
(InL ls) -> do
let mapper :: (Command |? CodeAction) -> ExceptT ResponseError (LspT Config IO) (Command |? CodeAction)
mapper c@(InL _) = pure c
mapper (InR codeAction) = fmap (InR . dropData) $ ExceptT $ codeResolveMethod ideState pid codeAction
if supportsResolve caps
then pure $ InL ls
else InL <$> traverse mapper ls --This is the actual part where we fill in the edit data for the client
in mkPluginHandler SMethod_TextDocumentCodeAction newCodeActionMethod
<> mkPluginHandler SMethod_CodeActionResolve codeResolveMethod
where supportsResolve :: ClientCapabilities -> Bool
supportsResolve caps =
caps ^? L.textDocument . _Just . L.codeAction . _Just . L.dataSupport . _Just == Just True
&& case caps ^? L.textDocument . _Just . L.codeAction . _Just . L.resolveSupport . _Just of
Just row -> "edit" `elem` row .! #properties
_ -> False
dropData :: CodeAction -> CodeAction
dropData ca = ca & L.data_ .~ Nothing

-- |Allow plugins to "own" resolve data, allowing only them to be queried for
-- the resolve action. This design has added flexibility at the cost of nested
-- Value types
data OwnedResolveData = ORD {
owner :: PluginId
, value :: Value
} deriving (Generic, Show)
instance ToJSON OwnedResolveData
instance FromJSON OwnedResolveData

pluginResolverResponsible :: Maybe Value -> PluginDescriptor c -> Bool
pluginResolverResponsible (Just val) pluginDesc =
case fromJSON val of
(Success (ORD o _)) -> pluginId pluginDesc == o
_ -> True -- We want to fail open in case our resolver is not using the ORD type
-- This is a wierd case, because anything that gets resolved should have a data
-- field, but in any case, failing open is safe enough.
pluginResolverResponsible Nothing _ = True
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,6 @@ test-suite tests
, lens
, lsp-test
, lsp-types
, row-types
, hls-test-utils

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ 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 Control.Monad.Trans.Except (ExceptT, throwE)
import Data.Aeson (FromJSON, Result (..),
ToJSON, fromJSON, toJSON)
import Data.Generics (GenericQ, everything,
Expand Down Expand Up @@ -79,10 +79,12 @@ import qualified Ide.Plugin.RangeMap as RangeMap
import Ide.PluginUtils (getNormalizedFilePath,
handleMaybeM,
pluginResponse)
import Ide.Types (PluginDescriptor (..),
import Ide.Types (OwnedResolveData (..),
PluginDescriptor (..),
PluginId (..),
PluginMethodHandler,
defaultPluginDescriptor,
mkCodeActionHandlerWithResolve,
mkPluginHandler)
import Language.LSP.Protocol.Lens (HasChanges (changes))
import qualified Language.LSP.Protocol.Lens as L
Expand All @@ -93,7 +95,7 @@ import Language.LSP.Protocol.Types (CodeAction (..),
CodeActionParams (..),
Command, TextEdit (..),
Uri (..),
WorkspaceEdit (WorkspaceEdit),
WorkspaceEdit (WorkspaceEdit, _changeAnnotations, _changes, _documentChanges),
fromNormalizedUri,
normalizedFilePathToUri,
type (|?) (..))
Expand All @@ -116,7 +118,13 @@ instance Hashable CollectRecordSelectors
instance NFData CollectRecordSelectors

data CollectRecordSelectorsResult = CRSR
{ records :: RangeMap Int
{ -- |We store everything in here that we need to create the unresolved
-- codeAction, the range, an uniquely identifiable int, and the expression
-- for the selector expression that we use to generate the name
records :: RangeMap (Int, HsExpr (GhcPass 'Renamed))
-- |This is for when we need to fully generate a textEdit. It contains the
-- whole expression we are interested in indexed to the unique id we got
-- from the previous field
, recordInfos :: IntMap.IntMap RecordSelectorExpr
, enabledExtensions :: [Extension]
}
Expand Down Expand Up @@ -147,8 +155,12 @@ instance Pretty RecordSelectorExpr where
instance NFData RecordSelectorExpr where
rnf = rwhnf

-- |The data that is serialized and placed in the data field of resolvable
-- code actions
data ORDResolveData = ORDRD {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about the idea of just reusing the CodeActionParams?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

-- |We need the uri to get shake results
uri :: Uri
-- |The unique id that allows us to find the specific codeAction we want
, uniqueID :: Int
} deriving (Generic, Show)
instance ToJSON ORDResolveData
Expand All @@ -158,89 +170,65 @@ descriptor :: Recorder (WithPriority Log) -> PluginId
-> PluginDescriptor IdeState
descriptor recorder plId = (defaultPluginDescriptor plId)
{ pluginHandlers =
mkPluginHandler SMethod_TextDocumentCodeAction codeActionProvider
<> mkPluginHandler SMethod_CodeActionResolve resolveProvider

mkCodeActionHandlerWithResolve codeActionProvider resolveProvider
, pluginRules = collectRecSelsRule recorder
}

resolveProvider :: PluginMethodHandler IdeState 'Method_CodeActionResolve
resolveProvider ideState pId ca@(CodeAction _ _ _ _ _ _ _ (Just resData)) =
pluginResponse $ do
case fromJSON $ resData of
case fromJSON resData >>= \x -> fromJSON $ value x 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
pure $ ca {_edit = mkWorkspaceEdit uri int crsDetails exts pragma}
_ -> throwE "Unable to deserialize the data"

codeActionProvider :: PluginMethodHandler IdeState 'Method_TextDocumentCodeAction
codeActionProvider ideState pId (CodeActionParams _ _ caDocId caRange _) =
pluginResponse $ do
nfp <- getNormalizedFilePath (caDocId ^. L.uri)
pragma <- getFirstPragma pId ideState nfp
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
pragmaEdit =
if OverloadedRecordDot `elem` exts
then Nothing
else Just $ insertNewPragma pragma OverloadedRecordDot
edits (Just crs) = convertRecordSelectors crs : maybeToList pragmaEdit
edits _ = []
changes crsM crsD =
case supportsResolve of
Just False -> Just $ WorkspaceEdit
(Just (Map.singleton (fromNormalizedUri
(normalizedFilePathToUri nfp))
(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
let mkCodeAction (crsM, nse) = 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 crsM crsD
_title = mkCodeActionTitle exts crsM nse
, _kind = Just CodeActionKind_RefactorRewrite
, _diagnostics = Nothing
, _isPreferred = Nothing
, _disabled = Nothing
, _edit = changes crsM crsD
, _edit = Nothing
, _command = Nothing
, _data_ = resolveData crsM
, _data_ = Just $ toJSON $ ORD pId $ toJSON $ ORDRD (caDocId ^. L.uri) crsM
}
actions = map (mkCodeAction crsDetails) (RangeMap.filterByRange caRange crsMap)
actions = map mkCodeAction (RangeMap.filterByRange caRange crsMap)
pure $ InL actions
where
mkCodeActionTitle :: [Extension] -> Int -> IntMap.IntMap RecordSelectorExpr-> Text
mkCodeActionTitle exts crsM crsD =
mkCodeActionTitle :: [Extension] -> Int -> HsExpr (GhcPass 'Renamed) -> Text
mkCodeActionTitle exts crsM se =
if OverloadedRecordDot `elem` exts
then title
else title <> " (needs extension: OverloadedRecordDot)"
where
title = "Convert `" <> name (IntMap.lookup crsM crsD) <> "` to record dot syntax"
name (Just (RecordSelectorExpr _ se _)) = printOutputable se
name _ = ""
title = "Convert `" <> printOutputable se <> "` to record dot syntax"

mkWorkspaceEdit:: Uri -> Int -> IntMap.IntMap RecordSelectorExpr -> [Extension] -> NextPragmaInfo-> Maybe WorkspaceEdit
mkWorkspaceEdit uri crsM crsD exts pragma =
Just $ WorkspaceEdit
{ _changes = Just (Map.singleton uri (edits (IntMap.lookup crsM crsD)))
, _documentChanges = Nothing
, _changeAnnotations = Nothing}
where pragmaEdit =
if OverloadedRecordDot `elem` exts
then Nothing
else Just $ insertNewPragma pragma OverloadedRecordDot
edits (Just crs) = convertRecordSelectors crs : maybeToList pragmaEdit
edits _ = []

collectRecSelsRule :: Recorder (WithPriority Log) -> Rules ()
collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $
Expand All @@ -259,13 +247,12 @@ collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $
recSels = mapMaybe (rewriteRange pm) (getRecordSelectors tmr)
uniques <- liftIO $ replicateM (length recSels) (hashUnique <$> newUnique)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is newUnique threadsafe? We run handlers in individual threads, so there could well be multiple instances of this running at once.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 crsDetails = IntMap.fromList $ zip uniques recSels
let crsUniquesAndDetails = 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)
rangeAndUnique = toRangeAndUnique <$> crsUniquesAndDetails
crsMap :: RangeMap (Int, HsExpr (GhcPass 'Renamed))
crsMap = RangeMap.fromList' rangeAndUnique
pure ([], CRSR <$> Just crsMap <*> Just (IntMap.fromList crsUniquesAndDetails) <*> Just exts)
where getEnabledExtensions :: TcModuleResult -> [Extension]
getEnabledExtensions = getExtensions . tmrParsed
getRecordSelectors :: TcModuleResult -> [RecordSelectorExpr]
Expand All @@ -277,6 +264,7 @@ collectRecSelsRule recorder = define (cmapWithPrio LogShake recorder) $
case toCurrentRange pm (location recSel) of
Just newLoc -> Just $ recSel{location = newLoc}
Nothing -> Nothing
toRangeAndUnique (id, RecordSelectorExpr l (unLoc -> se) _) = (l, (id, se))

convertRecordSelectors :: RecordSelectorExpr -> TextEdit
convertRecordSelectors (RecordSelectorExpr l se re) =
Expand Down
Loading