Skip to content

Additional code actions/quick fixes#164

Open
friedbrice wants to merge 2 commits intojosephsumabat:mainfrom
friedbrice:main
Open

Additional code actions/quick fixes#164
friedbrice wants to merge 2 commits intojosephsumabat:mainfrom
friedbrice:main

Conversation

@friedbrice
Copy link
Collaborator

This PR introduces six new code actions/quick fixes:

  • Add required extension
  • Insert associated type
  • Insert cases
  • Insert fields
  • Insert missing methods
  • Use valid hole fit

These code actions work by parsing relevant information from GHC error messages.

* Add required extension

* Insert associated type

* Insert cases

* Insert fields

* Insert missing methods

* Use valid hole fit
@friedbrice friedbrice requested a review from josephsumabat July 6, 2025 15:44
Comment on lines -56 to +65
getHieFileFromPath :: (HasStaticEnv m, HasLogger m, MonadIO m, HasLogger m) => AbsPath -> MaybeT m HieFile
getHieFileFromPath :: (HasStaticEnv m, HasLogger m, MonadIO m) => AbsPath -> MaybeT m HieFile
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Drive-by improvement.

Comment on lines +32 to +38
getCodeActions ::
LSP.TextDocumentIdentifier ->
LSP.CodeActionContext ->
AbsPath ->
LineCol ->
StaticLsM [LSP.CodeAction]
getCodeActions tdi ctx path lineCol = do
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

My code actions were written to a TextDocumentIdentifier -> CodeActionContext -> StaticLsM [CodeAction] shape, whereas the others were written to conform to AbsPath -> LineCol -> StaticLsM [Assist]. I attempted to refactor mine to be more idiomatic with the existing code actions, but I ran into significant difficulty constructing Assist values when I needed to provide them a Data.Range.Range (from tree-sitter). A tree-sitter Range requires two Data.Pos.Pos (which is what I need to construct), each of which carries just a single Int representing a document as either a sequence of characters or a sequence of bytes---that part's not clear to me---whereas an LSP Position (which is what I have in scope) has line number and column. I doubt there even is a way to convert between the two kinds of positions, considering platform differences such as newline character sequences and such.

Since I didn't have the information at hand to construct a tree-sitter Pos, I changed codeActions to take the info I needed to construct my CodeActions, and pushed assistToCodeAction into the body of codeActions so that it returns CodeAction instead of Assist.

Comment on lines +45 to +46
let issues = S.fromList (mapMaybe Parse.actionableIssue ctx._diagnostics)
issueActions <- join <$> traverse (issueToActions tdi) (S.toList issues)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These are the new code actions. See issueToActions and the type ActionableIssue.

let issues = S.fromList (mapMaybe Parse.actionableIssue ctx._diagnostics)
issueActions <- join <$> traverse (issueToActions tdi) (S.toList issues)
assistActions <-
traverse assistToCodeAction $
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

See discussion above for rationale on moving assistToCodeAction into the body of codeAction.

LSP.Diagnostic ->
Parse.KnownExtension ->
LSP.CodeAction
addRequiredExtension tdi diag (Parse.KnownExtension _ text) =
Copy link
Collaborator Author

@friedbrice friedbrice Jul 6, 2025

Choose a reason for hiding this comment

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

If GHC says "Perhaps you intended to enable TypeApplications" or something like that, this function will create a quick fix for it.

codeAction = insertAssociatedType

insertAssociatedType :: LSP.TextDocumentIdentifier -> LSP.Diagnostic -> T.Text -> LSP.CodeAction
insertAssociatedType tdi diag ty =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This code action is pretty superficial. Maybe we should remove it.

codeAction = insertCases

insertCases :: LSP.TextDocumentIdentifier -> LSP.Diagnostic -> [T.Text] -> LSP.CodeAction
insertCases tdi diag pats =
Copy link
Collaborator Author

@friedbrice friedbrice Jul 6, 2025

Choose a reason for hiding this comment

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

If a GHC error (or warning) tells you which cases are missing, this function will create a quick fix that will insert the missing cases.

Maybe T.Text ->
[T.Text] ->
LSP.CodeAction
insertFields tdi diag ctor existingFields missingFields =
Copy link
Collaborator Author

@friedbrice friedbrice Jul 6, 2025

Choose a reason for hiding this comment

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

If a GHC error (or warning) tells you which fields are missing, this code action will create a quick fix that will insert the missing fields for you. Works with both kinds of missing-fields error messages (i.e. the one for strict fields and the one for non-strict fields).

codeAction = insertMissingMethods

insertMissingMethods :: LSP.TextDocumentIdentifier -> LSP.Diagnostic -> [T.Text] -> LSP.CodeAction
insertMissingMethods tdi diag methods =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If a GHC error (or warning) says your class instance is missing methods, this function will create a quick fix that will insert the missing methods for you.

instance Eq (Ignored a) where (==) _ _ = True
instance Ord (Ignored a) where compare _ _ = EQ

data KnownExtension = KnownExtension {-# UNPACK #-} !Extension {-# UNPACK #-} !T.Text
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 guess I should remove those pragmas. I don't remember why I even had them in there in the first place.

Comment on lines +26 to +28
{-# NOINLINE knownExtensions #-}
knownExtensions :: [KnownExtension]
knownExtensions = (\ext -> KnownExtension ext $ T.pack $ show ext) <$> [minBound .. maxBound]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No-inline because we only want to allocate this once, and refer to it again and again during execution. That said, it's probably not a huge deal to rebuild it every time. It's at most (what?) 40 or so items.

knownExtensions :: [KnownExtension]
knownExtensions = (\ext -> KnownExtension ext $ T.pack $ show ext) <$> [minBound .. maxBound]

data ActionableIssue
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

An ActionableIssue is a defunctionalization of a method of creating one or more quick fixes. They are constructed by parsing GHC diagnostic messages, extracting the info relevant for making text edits in the relevant source file.

Comment on lines +39 to +40
actionableIssue :: LSP.Diagnostic -> Maybe ActionableIssue
actionableIssue diag =
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 parsing code in this module follows similar code in HLS. They literally do the same asinine thing for their quick fixes. That is, instead of using structured data that GHC would hypothetically provided, they parse the human-readable error messages. Go Haskell!

codeAction = useValidHoleFit

useValidHoleFit :: LSP.TextDocumentIdentifier -> LSP.Diagnostic -> T.Text -> LSP.CodeAction
useValidHoleFit tdi diag sym =
Copy link
Collaborator Author

@friedbrice friedbrice Jul 6, 2025

Choose a reason for hiding this comment

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

When GHC suggests valid hole fits, this function will create quick fixes for each such hole fit. (This one is the million-dollar feature.)

Copy link
Collaborator Author

@friedbrice friedbrice left a comment

Choose a reason for hiding this comment

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

I've annotated the PR with context intended to assist the reviewer.

@josephsumabat
Copy link
Owner

Thanks for the PR! I'll take a look some time this week/test it out :)

@josephsumabat
Copy link
Owner

Sorry I haven't gotten to this yet - I was a bit sick earlier this week so have been catching up on things but I will get this merged when I have the chance!

Copy link
Owner

@josephsumabat josephsumabat left a comment

Choose a reason for hiding this comment

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

Thank you for submitting this, these are some really nice features! I think the PR is pretty close to being merge-able but after testing I ran into a few issues

  • Some of the code actions don't actually show up because of the parsing for me for one reason or another - e.g. the _message field of diagnostics actually has the bars stripped on my machine e.g.
_message = "Pattern match(es) are non-exhaustive\nIn a case alternative:\n    Patterns of type \8216A _\nB \n C\n        ..."

This causes the messages relying on bars to parse to fail. I'm not sure if this behavior is platform dependent though.

Changing the nonExhaustivePatterns locally to not use the bar seemed to make it work for me.

  • The actual action doesn't insert anything (likely related to me not fully fixing the parsing)

  • I suspect that this issue

I would like 2 main changes to the PR before I merge

  1. Fixing the functionality - if this is local to my machine we can try to figure this out together

  2. I would like at the very least a parsing test suite for the actionable messages - This can be golden tests or just unit tests - the main thing I want is something where I can paste a diagnostic and easily see if it matches its expected output.

it's possible that the error messages change across ghc versions or flags and it would be much easier to see why something isn't working and iterate with such a suite

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.

2 participants