Skip to content
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 for explicit-imports #3682

Merged
merged 19 commits into from
Jul 12, 2023

Conversation

joyfulmantis
Copy link
Collaborator

I am pretty much rewriting the interface with hls for this plugin, as it was pretty poorly written (functions inside both an io and maybe monad, that used neither of them). In the process besides adding support for resolve for the code lens and code actions, I am also adding specific code actions for each import (before there was just one code action for explicit imports for everything)

@michaelpj
Copy link
Collaborator

In the process besides adding support for resolve for the code lens and code actions, I am also adding specific code actions for each import

This is nice, I think I even have an issue for this. In particular, it seems easier for people to write client-side stuff to trigger specific code actions, so this will make it easier for people to define a hotkey for "make current import (only) explicit".

@michaelpj
Copy link
Collaborator

#496

Probably still needs some clean up and general refactoring
@joyfulmantis joyfulmantis marked this pull request as ready for review July 2, 2023 18:57
Copy link
Collaborator Author

@joyfulmantis joyfulmantis left a comment

Choose a reason for hiding this comment

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

The plugin "works" now and there are specific tests for it. While I am pretty happy with the general structure, I will want to go over and see if there is anything I want to refactor or clean up still though

Copy link
Collaborator

@michaelpj michaelpj left a comment

Choose a reason for hiding this comment

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

Needs a bit more work but generally looking plausible. I like how we get to unify all the logic into the rule and the (sort of) command handler.

It almost feels like there's a pattern here you could use to unify all of the things. Something like:

  • You have a rule that generates a bunch of possible actions in a file, with ranges.
  • You have a command handler/resolve function handler that applies the action
  • You have a code lens handler that generates lenses for some subset of the actions
  • You have a code lens resolve handler that calls the command handler and uses some logic to set the title appropriately
  • You have a code action handler that generates code actions for some subset of the actions.
  • You have a code action resolve handler that calls the command handler/fills in the edit, depending.

I think it probably would only work for a restricted set of things: you really need your "logic handler" to just produce an edit, so you can use that same edit in all scenarios (applyEdit, filling it in on a code action, etc.).

pluginHandlers =
-- This plugin provides code lenses
mkPluginHandler SMethod_TextDocumentCodeLens (lensProvider pred)
<> mkPluginHandler SMethod_CodeLensResolve lensResolveProvider
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we want to design a similar paired handler function for lenses like we have for code actions?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Since the client never advertises a code lens resolve capability, we have to assume that all clients support it, and can't change our behaviour depending on whether they support it.

Copy link
Collaborator

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.

I think it's very likely that almost no clients support it so this may just break for many clients.

Copy link
Collaborator Author

@joyfulmantis joyfulmantis Jul 3, 2023

Choose a reason for hiding this comment

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

I think that actually not having a capability for it must mean that all clients (need to?) support it. It's also a relatively important resolve type, because you send all your codelens every edit. Vscode works with resolve, and I know the rust server uses it too.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Could still be useful to have the paired function since maybe we can simplify some stuff that's always the same like the data handling.

runImportCommand :: CommandFunction IdeState EIResolveData
runImportCommand ideState eird = pluginResponse $ do
case eird of
ResolveOne uri int -> do
Copy link
Collaborator

Choose a reason for hiding this comment

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

Missing case for ResolveAll?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, since you've named the fields, this can be a nice place to use NamedFieldPuns, i.e.

ResolveOne{uri, int} -> ...

just makes sure you can't mix up the positional args

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The command actually is responding only to the lens, so shouldn't handle the ResolveAll case. In this case I'm actually reusing the data function, but it might be better to have seperate types.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it seems reasonable to share the code for running the action if possible? Having one function that handles either case and then we just call it seems good.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Okay good point, there is no reason to use resolve from codeActions then, but no reason to implement resolve for no reason.

{getMinimalImportsResult :: [(LImportDecl GhcRn, Maybe T.Text)]}
data MinimalImportsResult = MinimalImportsResult
{ forLens :: [(Range, Int)]
, forCodeActions :: RM.RangeMap (Range, Int)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need the previous field, can't we just enumerate the (Range, Int) pairs from the RangeMap?

Copy link
Collaborator

Choose a reason for hiding this comment

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

... also why does this have a range in the output? is it the same range as in the input?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

From what I've researched it's not possible to easily extract the Range key from the RangeMap. Also the range in the output is the range of the textedit, but the range of the input is (in this case) what the client sends over, which is not identical

Copy link
Collaborator

@michaelpj michaelpj left a comment

Choose a reason for hiding this comment

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

Generally looking good!

--------------------------------------------------------------------------------

resolveWTextEdit :: IdeState -> EIResolveData -> ExceptT String (LspT Config IO) WorkspaceEdit
resolveWTextEdit ideState (ResolveOne uri int) = do
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: you could deduplicate a bit by doing:

resolveWTextEdit ideState action = do
  nfp <- ..
  (MinimalImports ...
  case action of
     ResolveOne ...

let edits = IM.elems forResolve
pure $ mkWorkspaceEdit uri edits

mkWorkspaceEdit :: Uri -> [TextEdit] -> WorkspaceEdit
Copy link
Collaborator

Choose a reason for hiding this comment

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

... and if you did the deduplication you could probably inline this

codeActionBreakFile :: FilePath -> Int -> Int -> TestTree
codeActionBreakFile fp l c = goldenWithExplicitImports " code action" fp codeActionNoResolveCaps $ \doc -> do
_ <- waitForDiagnostics
changeDoc doc [edit]
actions <- getCodeActions doc (pointRange l c)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test doesn't look quite right to me: shouldn't we be getting the code actions, picking one, then editing the file, then resolving the action we picked before and seeing it fail?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So this test causes a parsing error in the file and then gets a code action and executes it. Because we are using "useWithStale" we can still operate on a file that has a parse fail.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah I see, that's slightly different then. I think it might also be interesting to put an edit in between getting the actions and resolving one. Potentially one that doesn't cause a parse error, so it just mixes up the uniques, which should cause a failure still.

@@ -79,18 +123,16 @@ caTitle _ = Nothing
-- code lens tests

codeLensGoldenTest :: FilePath -> Int -> TestTree
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe these things could also go in hls-test-utils? Seems like you've been repeating something similar a few times. Good to deduplicate the test stuff too!

@joyfulmantis joyfulmantis merged commit 27f46d7 into haskell:master Jul 12, 2023
44 of 45 checks passed
@fendor fendor mentioned this pull request Aug 8, 2023
19 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants