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

feat: unused variable code action #349

Merged
merged 10 commits into from
Feb 21, 2024

Conversation

NJichev
Copy link
Collaborator

@NJichev NJichev commented Dec 20, 2023

Very barebones draft for the code action

end

defimpl NextLS.CodeActionable do
alias NextLS.CodeAction
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 imagine we'll have several CodeActions, some basic ones for example:
NextLS.CodeAction.UnusedVar
NextLS.CodeAction.AddRequire

And each will have a new/1 function that converts a diagnostic/check/placeholder struct to the code action which will contain the necessary TextEdits inside for the client.

@@ -0,0 +1,30 @@
defmodule NextLS.Check do
Copy link
Collaborator

Choose a reason for hiding this comment

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

i assume you got this terminology from the credo language server.

there its called a check because there are credo checks. i'm not sure if the same concept applies here.

I think the thing to do would have the elixir extension have the code for converting its diagnostics into code actions.

with that, i'm not sure exactly if a protocol is going to be the best way to go here, as they are all going to be the same diagnostic struct.

lib/next_ls.ex Outdated
Comment on lines 171 to 177
code_actions =
for %Diagnostic{} = diagnostic <- diagnostics,
check =
NextLS.Check.new(
diagnostic: diagnostic,
uri: uri,
document: lsp.assigns.documents[uri]
),
action <- NextLS.CodeActionable.fetch(check) do
action
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

so for this, I think you're going to one to loop through the diagnostics, then branch on the namespace and ask the appropriate extension to return code actions for it.

you'll also need to modify each extension to add the "data" property with the namespace. the data property is used to send data down to the client, then it sends it back up.

lib/next_ls.ex Outdated
# Note: the diagnostic data doesn't have a namespace attribute yet,
# this is only so that there are no compile error
# Maybe add the namespace to the data attribute
# or retrieve the diagnostics for the current file from diagnostic cache
Copy link
Collaborator Author

@NJichev NJichev Jan 10, 2024

Choose a reason for hiding this comment

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

Doing the latter, i.e. retrieving the diagnostic from the cache would probably be wrong, since as documented for the diagnostics field in %CodeActionContext, it says:

  * diagnostics: An array of diagnostics known on the client side overlapping the range provided to the
    `textDocument/codeAction` request. They are provided so that the server knows which
    errors are currently presented to the user for the given range. There is no guarantee
    that these accurately reflect the error state of the resource. The primary parameter
    to compute code actions is the provided range.

It seems to me that getting the diagnostics from the cache could lead to situations where the client isn't seeing a diagnostic but we send back a code action for it or the vice versa where the client sends a diagnostic that we ignore.

So I think it's better to store the extension namespace in the data attribute.


@callback to_code_action(arg :: Data.t()) :: [CodeAction.t()]

def from(:elixir, diagnostic_data) do
Copy link
Collaborator

Choose a reason for hiding this comment

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

These will need to be dispatched to the extension processes dynamically, as eventually libraries can provide their own extensions and we won't statically know what extensions exist or have their code loaded.

This is fine for now, it maybe leave a todo comment

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe we can store the module name to dispatch to in the data attribute of the diagnostic? And libraries will have to obey the contract for this to work.
I think we can do something like:

def from(:elixir, data), do: NextLS.ElixirExtension.CodeAction.from(data)
def from(:credo, data), do: NextLS.CredoExtension.CodeAction.from(data)
def from(namespace, data), do: Module.concat(namespace, CodeAction).from(data)

Then it's going to be clear for external libraries what behaviour they need to implement.

I still like the static part being there since it's probably faster for the BEAM

Copy link
Collaborator

Choose a reason for hiding this comment

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

The code might not even be loaded on the NextLS node, which is by dispatching it to the extension process will be required.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah I see, then I'll leave it as a TODO comment.

@mhanberg
Copy link
Collaborator

I think this looks good for a starting place, so feel free to implement the actual code action now 👍

@NJichev NJichev marked this pull request as ready for review February 17, 2024 06:56

alias GenLSP.Structures.CodeAction

defmodule Data 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.

I think I can remove this in favour of just using schematic

@mhanberg
Copy link
Collaborator

CleanShot 2024-02-21 at 17 35 50@2x
this is dope.

i didn't realize my neovim extension would show a diff preview of what the code action would do

@@ -111,4 +112,18 @@ defmodule NextLS.ElixirExtension do
end

def clamp(line), do: max(line, 0)

@unused_variable ~r/variable\s\"[^\"]+\"\sis\sunused/
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if elixir core would consider tagging diagnostics with IDs, like EX1 means "unused variable" 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That would be dope

@mhanberg mhanberg enabled auto-merge (squash) February 21, 2024 22:59
@mhanberg
Copy link
Collaborator

you are a rockstar 🎸

@mhanberg mhanberg merged commit 8b9a57c into elixir-tools:main Feb 21, 2024
13 checks passed
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