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

support add-argument action #3149

Merged
merged 20 commits into from Nov 6, 2022
Merged

Conversation

santiweight
Copy link
Collaborator

@santiweight santiweight commented Sep 12, 2022

This action supports an action "Add argument xxx to function" whenever there is an undefined variable error:

foo = unknown
-- becomes
foo unknown = unknown

The only minor wrinkle is that

foo = True
   where
      bar = unknown
-- becomes
foo unknown = True
   where
      bar = unknown

But this is good enough for now as a matter of pragmatism.

As part of this MR, I fixed some behavior. Previously an unknown-type undefined variable did not offer a "define x" option, but now does:

foo = unknown
-- previously no code actions
-- now becomes
foo = unknown

unknown :: _
unknown = _

@kokobd
Copy link
Collaborator

kokobd commented Sep 12, 2022

Thanks for your contribution!

For reference, below are the behavior of some other language servers/IDEs on this:

  • rust-analyzer in VSCode: no quickfix
  • TypeScript in VSCode: no quickfix
  • Java in Intellij IDEA Community: has this quickfix, but provides a dialog to modify function signature
screenshots image image image image

I'm not quite sure about the design, maybe someone else will provide better insight.

@santiweight
Copy link
Collaborator Author

santiweight commented Sep 12, 2022

Thanks for the screenshots! Very helpful context :) You note that Rust/TS have no quick fix.

I am quite familiar with the IntelliJ interface from my previous job. I would love to support this workflow, but after I looked around the HLS and LSP issues I got the impression that such a pretty workflow would require too much work to justify. Is that you guys' experience? I really think our UX could be best in class given Haskell's type system, but I'm concerned about maintenance hell.

On a side note, it would be really great to have some sort of function:

alterAllUsages :: {- function name -} RdrName -> (LHsExpr -> TransformT m LHsExpr) -> IDEState -> TransformT m IDEState

The idea being that when I add a function parameter to a definition, I can insert appropriate holes in all usages project wide. Does anyone have any thoughts on the feasibility of this? @OliverMadine seems like a candidate for such knowledge!

module One where

import Two

bar = foo True
----
module Two where

foo True = unknown False

----- BECOMES -----
module One where

import Two

bar = foo True _unknown
----
module Two where

foo True unknown = unknown False

@OliverMadine
Copy link
Collaborator

The idea being that when I add a function parameter to a definition, I can insert appropriate holes in all usages project wide. Does anyone have any thoughts on the feasibility of this? @OliverMadine seems like a candidate for such knowledge!

Yes, it seems very feasible to me!
Most of the logic could be extracted from the rename plugin. It would, however, be subject to the same multi-component limitation of the rename plugin (#2193)

@@ -623,6 +653,14 @@ eqSrcSpanA l r = leftmost_smallest l r == EQ
#endif

#if MIN_VERSION_ghc(9,2,0)

spanContainsRange :: SrcSpan -> Range -> Bool
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe you can use realSrcSpanToRange and isSubrangeOf ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I forgot to respond. This worked perfectly thanks :)

pure $ Just [decl']
_ -> pure Nothing
case runTransformT $ modifySmallestDeclWithM (`spanContainsRange` range) insertArg (makeDeltaAst parsedSource) of
Left err -> error $ "Error when inserting argument: " <> err
Copy link
Collaborator

Choose a reason for hiding this comment

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

Avoid throwing an error here, as it will at best kill all other code actions and at worst kill all other plugins. Ideally you should return a ResponseError but that probably requires too much refactoring.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm unclear on what to do here. For the time being I used a trace call and returned no diffs. Is ResponseError part of the MonadLSP interface?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes it is :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I summoned up the courage to reveal ResponseErrors in the CodeAction API. The only hold up is I don't know how to throw/report ResponseErrors with MonadLSP after some hoogling.

Anyone have any pointers? 😅 Note that I am returning a list of errors and successes (since there are many potential code actions), so it should just be logResponseError :: MonadLSP m => ResponseError -> m (), not throw

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 fine to just use logError or logWarn and return nothing

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

After some investigation, it appears that the only Recorder available to CodeActions is Recorder (WithPriority E.Log), which is the logging from Development.IDE.Core.Shake.

Which logger should I be using for code actions? I can't find a Logger or nonShake recorder in the parent functions...

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is because this plugin was only recently extracted from ghcide core. The plugin needs to define its own Logger I guess.

@drsooch
Copy link
Collaborator

drsooch commented Sep 27, 2022

There is a plug-in that attempts to update signatures when it can. change-type-signature It's pretty rudimentary and only works in a handful of cases where GHC provides enough information. When the upstream lsp package updates to the lsp spec 3.17. We should be able to get direct access to GHCs Error Message API and potentially be worthwhile to this case.

@santiweight
Copy link
Collaborator Author

santiweight commented Oct 2, 2022

@drsooch I've thought about the change-type-sig plugin in this context. I think they're fairly different situations. In change-type-sig, I can only see occurrences where the new type signature is given.

However, in this case, we do not have the full type signature: we only have the type of the argument we're adding. That means we do have to manually alter the HsSigType with the exact print api, which is more complicated.

I implemented a working first attempt (it's surely very buggy for non-trivial layouts): https://github.com/santiweight/haskell-language-server/tree/add-arg-type-sig

Copy link
Collaborator

@pepeiborra pepeiborra left a comment

Choose a reason for hiding this comment

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

Looks good to me modulo the build failures

@michaelpj
Copy link
Collaborator

Can you add yourself to CODEOWNERS? You might also want to mention this in features.md.

@santiweight
Copy link
Collaborator Author

It seems that the tests didn't run as part of CI? Besides that I've added myself to codeowners but I don't have write access, which is causing an error in the code owners file!

I'm happy to merge whenever now :) Thanks for the help getting this one through!

@michaelpj
Copy link
Collaborator

It seems that the tests didn't run as part of CI?

They should have, we're testing hls-refactor-plugin which should include the new stuff, right?

Besides that I've added myself to codeowners but I don't have write access, which is causing an error in the code owners file!

I've invited you to the repo :)

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.

I took a fairly superficial look, but I think we can get this over the line :)

Thank you for digging into the code actions in hls-refactor-plugin. As Pepe mentioned, they only recently got extracted from ghcide, so they quite probably be tidied up more to be more in keeping with the style of more recent code action handlers if you felt moved to!

@@ -119,6 +121,9 @@ p `isInsideSrcSpan` r = case srcSpanToRange r of
Just (Range sp ep) -> sp <= p && p <= ep
_ -> False

spanContainsRange :: SrcSpan -> Range -> Bool
spanContainsRange srcSpan range = maybe False (range `isSubrangeOf`) $ srcSpanToRange srcSpan
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 this is clearer as a simple case

Suggested change
spanContainsRange srcSpan range = maybe False (range `isSubrangeOf`) $ srcSpanToRange srcSpan
spanContainsRange srcSpan range = case srcSpanToRange srcSpan of
Just r -> range `isSubrangeOf` r
-- Nice place to explain why this is the right policy
Nothing -> False

And having written it that way, I'm not sure whether we have got the right policy there. Do we want to return False in the "can't turn a src span into a range" case? Should we be returning a Maybe Bool and preserving that information?

Copy link
Collaborator

Choose a reason for hiding this comment

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

At the very least the Haddock should tell you that the returned bool includes this information!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I returned a Nothing and added a comment; I think it's clearer now...

]

#if MIN_VERSION_ghc(9,2,1)
addFunctionArgumentTests :: TestTree
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a great time to break the cycle of violence and stop adding things to the single gigantic test file :) put it in another module!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have already done this in a followup change. Unfortunately fixing this now would cost me some time with conflicts, but I promise to follow up :)

@@ -881,34 +891,95 @@ suggestReplaceIdentifier contents Diagnostic{_range=_range,..}
= [ ("Replace with ‘" <> name <> "’", [mkRenameEdit contents _range name]) | name <- renameSuggestions ]
| otherwise = []

matchVariableNotInScope :: T.Text -> Maybe (T.Text, Maybe T.Text)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps an annoying suggestion, but these matching functions are all nice pure functions that could benefit from some direct tests checking that they do definitely match all the cases you care about.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, this module is also quite large, perhaps the add-action stuff could go in a separate module also?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I actually already did this in a followup MR. Would you be okay with following up with this change (to avoid unnecessary conflicts)?

suggestNewDefinition ideOptions parsedModule contents Diagnostic {_message, _range}
| Just (name, typ) <- matchVariableNotInScope message =
newDefinitionAction ideOptions parsedModule _range name typ
| Just (name, typ) <- matchFoundHole message,
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 you have a Plan. Something like:

  • Look for errors of a particular kind
  • Based on the kind of error, make certain kinds of suggestion

But the Plan is not written down anywhere, and as a reader it's hard to figure out what it is. Maybe worth writing it down somewhere and referring to it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Revised

@santiweight
Copy link
Collaborator Author

Thanks for the review @michaelpj

A heads up on the status of this PR... I'm pretty happy with where it is right now, but I'm a little worried about my approach to using the Exactprint API, and so I'm going to attempt reimplementing the code in a better style (by following the examples in ghc-exactprint).

This MR isn't forgotten, just hopefully being replaced by something better...

@santiweight
Copy link
Collaborator Author

I was hoping to upstream some of the changes here in ghc-exactprint before merging, but I think that process will take some time.

I've answered the comments from reviewers, and I now think this is ready to merge!

@michaelpj
Copy link
Collaborator

I'm happy for you to continue improving things, especially since it sounds like you're definitely continuing to work in this area :)

@santiweight santiweight merged commit 9d70df0 into haskell:master Nov 6, 2022
@santiweight santiweight deleted the add-argument branch November 20, 2022 23:46
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

7 participants