-
-
Notifications
You must be signed in to change notification settings - Fork 367
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 refactoring #3688
Resolve refactoring #3688
Conversation
Here's an example of how our function types and arguments change with the new resolve logic. Before a resolve provider looked something like this:
Now it will look something like this
The most obvious differences are now we always have a URI passed to us (necessary for almost everything, and we can start using it right away, without having to decode any data), and our data is already decoded into our custom type for us. Additionally, all responses that can be resolved will be (invisible to the plugin) wrapped in information that allows us to resolve specifically for that plugin only (it's also what allows us to provide the uri). We also no longer use pluginEnabled and combineResponses functions, or the type classes that provide them for any of the resolve methods. |
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.
At a high level I like the interface for plugins. I'm not sure why we need a separate entry in the plugin descriptor though? Why can't we just have a function that turns a ResolveFunction
into a PluginHandlers
like before? I don't think that the stuff in combineResponses
and so on was so bad - I think with the identifying information put into the data we had an okay solution there.
So the ResolveFunction, and how they are handled is pretty much equivalent to what's already done for Commands. And since Commands already had their own logic for being handled, I modeled the resolve logic off of them. Commands have an extra commandId that allows plugins to declare multiple commands per plugin. However the resolve stuff has extra complexity in that it has to allow plugins to register resolve handlers for different methods, hence the necessary usage of DMap. Both the ResolveFunction for resolve based functions and the CommandFunction for commands can be implemented on top of the existing Plugin Handler infrastructure. Why were commands originally handled differently, and what are the advantages and disadvantages of the different approaches? Non-disadvantage: Extra field in the PluginDescriptor. While this at first may seem like a disadvantage, no one actually creates a plugin descriptor from scratch, everyone just edits a default one with the stuff that they want, so the only time anyone would have to deal with it is if they actually want to provide resolve handlers themselves. Disadvantage: Technical debt. With commands and resolve handlers handled separately from the plugin handlers architecture, they need certain functions and data types, both in the hls-plugin-api, and in the core hls section of ghcide. I should add though that in the grand scheme of things, it's not large. For the resolve stuff something like 70 lines in hls-plugin-api's Types.hs and another around 50 lines in ghcide's Hls.hs. We also get to delete a bunch of lines from Types.hs (everything resolve related in the two main typeclasses there), so even If we build on top of the plugin handler architecture we could probably save a few lines, but not that many. Advantage: Less hacky, and better safety that nothing goes wrong. The reason the combineResponses is called combineResponses.. is because it's meant to... combine responses. While with combine responses it's possible to hackily just take the first item in the list, it's not an ideal solution. (What if more than one plugin returns a response, for example, because it fails by not changing anything rather than just returning an error (something that was previously done with ghcide completions I might add). Then we have the combineErrors, which is currently completely inadequate for the job of returning a response if everything errors out (It currently returns an internal error with the contents being a (show xs)!) It's not even clear how we would be able to single out only the error we want to use as a response. There are possibilities -- we could have resolve handlers return a specific error if the request is not for them, and filter those out, but that is super hacky, and you just need one plugin forgetting to return the right error to cause mayham for everyone. Non? advantage: Theoretically there is a performance improvement, albeit bound to be small, with the current approach we are only decoding the data field once, and only passing that on to one plugin, If we used the pluginHandler architecture we would have to decode the data field n times for n plugins. |
Checking a few things in my understanding:
Right, so I guess we could have different Perhaps a bad idea: what if we made TBH, I'm not sure whether there's any reason not to do the same for command handlers... I don't have this fully loaded into my head. But at least it seems like what you're doing with the resolve handlers is closer to what's being done for the other handlers?
Isn't your code basically doing the same thing? https://github.com/haskell/haskell-language-server/pull/3688/files#diff-7a4ae7207d77fe6892ad9315e62ebdc3ea929941d0e2d9a78e18074422657b27R240 i.e. only taking the first thing that comes up? Yes, you're filtering by the plugin id, but you might still have multiple resolve functions! And the existing code also filters with |
Okay, I completely dumped all extra logic to handle resolve stuff and wrote a mkResolveHelper that turns a ResolveFunction into a normal PluginHandlers. With the pluginEnabled logic we only route requests to the plugin listed in the data field, so as long as no plugin has more than one resolve method listed per resolve method then we should only get one potential response which should work with the current combineResponses and combineErrors. |
So his clients will ask us to resolve even if there is no data set, but currently, we respond with the "no plugin enabled" error message, which is of type InvalidRequest, and that doesn't seem to show an error for least vscode, so we should be fine there. |
hls-plugin-api/src/Ide/Types.hs
Outdated
then | ||
case fromJSON value of | ||
Success decodedValue -> | ||
let newParams = params & L.data_ ?~ value |
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.
Here we are unwrapping the PluginResolveData, to make it transparent to the resolveHandler.) However, there is one really weird bug here, where seemingly f is called with the old "params", not the updated "newParams". Have no clue why that is though.
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... would be good to track that down. A good case for having a logger in here - then you could log what's going on to see :)
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.
LGTM!
Oh right, I do think this module could do with a Note explaining the big picture of how we make the handlers all work nicely together. |
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.
Okay sorry, I do actually have more comments 😅
@@ -877,6 +886,57 @@ type CommandFunction ideState a | |||
|
|||
-- --------------------------------------------------------------------- | |||
|
|||
type ResolveFunction ideState a (m :: Method ClientToServer Request) = |
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.
These should probably go in Resolve.hs
?
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.
I left all the core Resolve stuff in Types, and moved the optional handler makers to Resolve. I am happy to move everything to resolve thought if you think that's better.
hls-plugin-api/src/Ide/Types.hs
Outdated
-> a | ||
-> LspM Config (Either ResponseError (MessageResult m)) | ||
|
||
-- | Make a handler for plugins with no extra data |
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.
unclear what "no extra data" means here
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.
I'm not sure either 🫤
hls-plugin-api/src/Ide/Types.hs
Outdated
in f ideState plId newParams uri decodedValue | ||
Error err -> | ||
pure $ Left $ ResponseError (InR ErrorCodes_ParseError) (parseError value err) Nothing | ||
else pure $ Left $ ResponseError (InR ErrorCodes_InvalidRequest) invalidRequest Nothing |
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 this is one of the cases where we would definitely want to say we are choosing not to respond rather than erroring !
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.
Actually pluginEnabled should prevent us from getting to this step, so if this is happening it probably means something is pretty broken
hls-plugin-api/src/Ide/Types.hs
Outdated
Error err -> | ||
pure $ Left $ ResponseError (InR ErrorCodes_ParseError) (parseError value err) Nothing | ||
else pure $ Left $ ResponseError (InR ErrorCodes_InvalidRequest) invalidRequest Nothing | ||
_ -> pure $ Left $ ResponseError (InR ErrorCodes_InvalidRequest) invalidRequest Nothing |
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.
use the aeson error!
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.
Also I don't think this should be using the "invalidRequest" message?
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.
Yeah, maybe InternalError would work better here
hls-plugin-api/src/Ide/Types.hs
Outdated
-> PluginHandlers ideState | ||
mkResolveHandler m f = mkPluginHandler m f' | ||
where f' ideState plId params = do | ||
case fromJSON <$> (params ^. L.data_) of |
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.
Do we want to give a different error if the data is missing?
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.
pluginEnabled should only route the request to us if the data field is full and decodes to PluginResolveData.
hls-plugin-api/src/Ide/Types.hs
Outdated
then | ||
case fromJSON value of | ||
Success decodedValue -> | ||
let newParams = params & L.data_ ?~ value |
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... would be good to track that down. A good case for having a logger in here - then you could log what's going on to see :)
@@ -18,10 +19,13 @@ getResolvedCompletions :: TextDocumentIdentifier -> Position -> Session [Complet | |||
getResolvedCompletions doc pos = do |
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.
don't you have a test helper for this? I guess it's in lsp
and we need to do a release
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.
Yes it's part of the lsp-test pr I did.
f' SMethod_TextDocumentCodeLens pid ide params@CodeLensParams{_textDocument=TextDocumentIdentifier {_uri}} = | ||
pure . fmap (wrapCodeLenses pid _uri) <$> f ide pid params | ||
f' SMethod_TextDocumentCompletion pid ide params@CompletionParams{_textDocument=TextDocumentIdentifier {_uri}} = | ||
pure . fmap (wrapCompletions pid _uri) <$> f ide pid params |
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... couldn't we push this stuff into the helper functions we have for making such handlers? Then, yes, you need to use those if you want to get resolve working, but if you don't care you can just write a bare handler and we won't insert extra stuff if you haven't asked for 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.
We're case matching on the method provided, and only actually wrapping the data field if it has anything in it, and the data field is only ever used for resolve. Otherwise we would need another specific function mkPluginHandlerThatSupportsResolve, wich imo would be kinda ugly
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.
The helpers in Resolve.hs are opinionated ways to provide fallback to resloveless methods if not supported, but it makes sense to support plugin users who want to write their own fallback logic. For example code lenses don't need fallbacks, and thus currently use mkPluginHandler directly even though they support resolve.
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, so I am suggesting something just like that, I was even thinking more specific: mkCodeLensHandlerWithResolve
or something. I don't think it's so bad for people to have to call a specific function to opt in to the magic?
161c8ea
to
7fe1fc0
Compare
I had higher hopes on what I could do here but ended up just implementing a ResolveFunction which makes what used to look like this.
Now look something like this
These resolve functions are complied with the mkResoveHandler, which is now the only way to write resolve handlers.
Things I still want to investigate, and possibly handle: