-
Notifications
You must be signed in to change notification settings - Fork 262
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: Code action support #2021
Conversation
# Conflicts: # Source/DafnyLanguageServer/Language/BoogieExtensions.cs
|
||
public override Task<CodeAction> Handle(CodeAction request, CancellationToken cancellationToken) { | ||
var command = request.Data; | ||
if (command != null && command.Count() >= 2 && command[1] != null) { |
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.
Could you reduce indentation of the remaining code by inverting the condition and returning early?
documentUriToQuickFixes.AddOrUpdate(documentUri, | ||
_ => fixesWithId, (_, _) => fixesWithId); | ||
var codeActions = fixesWithId.Select(fixWithId => { | ||
CommandOrCodeAction t = new CodeAction { |
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 think the intention of the CodeAction API is to use the field command
or edit
to define what happens when a user triggers the action, so here you could have
Command = new Command(fixWithId.QuickFix.Title, "dafnyCodeAction", new JArray(documentUri, fixWithId.Id))
Instead of the Data field
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 about what you mean.
The code that handles the code action is this one:
public override Task<CodeAction> Handle(CodeAction request...
, which explicitly requires an unresolved code action. So I don't understand why I should send a command?
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 saying you could return a CodeAction that includes a Command instead of one that includes Data, since this seems how the API is intended to be used. From the documentation:
A CodeAction must set either
edit
and/or acommand
. If both are supplied, theedit
is applied first, then thecommand
is executed.
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.
Looks like the documentation of the API is incomplete. Although what you say works well theoretically, it has the side-effect that it creates an error in VSCode.
And that's what I thought: Code actions can either give edits on the text or more general actions.
However, the API documentation you describe don't talk about the construction of code actions which is two phases, which I've seen while browing the code online
- Creation of the code action with possible data attached
- Resolution of the code action when it's being clicked.
In 1) there is no need to create an edit or a command if all the edits and commands are going to be computed on 2.
As a conclusion, adding a "command" would require us to change the VSCode extension to support this command, which would have to manually invoke the code action resolver... which would be useless and hard to maintain
So I'm going to revert my changes there and just use the regular data field which is there for this purpose - the data between code action creation and code action resolution.
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.
As a conclusion, adding a "command" would require us to change the VSCode extension to support this command, which would have to manually invoke the code action resolver... which would be useless and hard to maintain
That indeed seems bad.
OK, let's keep this as it is. Thanks for trying.
var fixesWithId = GetFixesWithIds(quickFixers, document, request).ToArray(); | ||
|
||
var documentUri = document.Uri.ToString(); | ||
documentUriToQuickFixes.AddOrUpdate(documentUri, |
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.
Newline here seems unneeded
/// </summary> | ||
/// <param name="Range">The range to replace. The start is given by the token's start, and the length is given by the val's length.</param> | ||
/// <param name="Replacement"></param> | ||
public record QuickFixEdit(Range Range, string Replacement = ""); |
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.
Given that this API only supports actions whose edits span a single file, is the idea to start with this API and evolve it over time?
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, we could add a field Uri to this down the road, so now we can start with this simpler API.
var codeActions = fixesWithId.Select(fixWithId => { | ||
CommandOrCodeAction t = new CodeAction { | ||
Title = fixWithId.QuickFix.Title, | ||
Data = new JArray(documentUri, fixWithId.Id) |
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.
One of the fields of a CodeAction is diagnostics
, and filling this field causes the code action to show up grouped under the diagnostic in the Problems view. The code action provided by VerificationQuickFixer
relates to a diagnostic, so shouldn't we connect those two using the API ?
|
||
namespace Microsoft.Dafny.LanguageServer.Plugins; | ||
|
||
public interface IQuickFixInput { |
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.
Why not use DafnyDocument
instead of this?
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 prefer to have an interface for the API rather than a concrete implementation that might be evolving.
Also, for a suitable API, we don't want to expose plugin developers with more information than necessary, do we?
|
||
namespace Microsoft.Dafny.LanguageServer.Plugins; | ||
|
||
public interface IDafnyCodeActionInput { |
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.
To me the name ..Input
doesn't tell me much about what to expect from this interface. I think ICompilation
or IDocumentCompilation
is more telling, since it contains information collected during compilation, such as the AST and diagnostics.
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.
Interesting point of view!
I think the convention in C# is to usually name interfaces in what purpose they serve, not about how they are implemented. A document compilation (report?) would seem like an implementation detail.
So let's keep this naming for now.
} | ||
} | ||
|
||
public class VerificationDafnyCodeActionProviderInput : IDafnyCodeActionInput { |
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.
What's Verification
doing in the name here?
Why don't we use DafnyDocument
to implement IDafnyCodeActionInput
?
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.
Verification is obsolete. It dates from the time when I had the quick fixer be defined in Dafny core instead of DafnyLanguageServer
I renamed it DafnyCodeActionInput
. And what do you mean by your question? This class uses DafnyDocument
to implement IDafnyCodeActionInput
(which is the interface plugins implementers are going to work with)
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.
To me a name ending in Input
is a code smell.
When I read a function signature SomeFunction(SomeFunctionInput x)
then the name SomeFunctionInput
doesn't give me more information than what the rest of the function signature already did. What I'd like to know is what information can I get from x
, and the name of the type of x
should help with that.
Instead of IDafnyCodeActionInput
, I think something like IResolvedCompilation
or IResolvedDocument
would tell me better what information I can get from it. If it's resolved then I expect that I can get the document text, the document AST, and the resolved types.
I think the convention in C# is to usually name interfaces in what purpose they serve, not about how they are implemented. A document compilation (report?) would seem like an implementation detail.
Do you feel the names I suggested reveal unnecessary information?
line = position.Line - LineOffset, | ||
col = position.Character - ColumnOffset, | ||
val = "", | ||
pos = position.ToAbsolutePosition(document) |
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.
This call will only happen when the user places their caret on a line with a verification diagnostic.
How come? Code actions are available on lines without diagnostics, are they not?
return null; | ||
} | ||
|
||
var expression = DafnyCodeActionHelpers.Extract(relatedInformation.Location.Range, input.Code); |
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.
This call still seems like an O(N) operation. Why don't you get a DocumentTextBuffer
from IDafnyCodeActionInput
and let DocumentTextBuffer
store a mapping from line to index that can be used to extract text using a Range in constant time?
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.
How come? Code actions are available on lines without diagnostics, are they not?
Not every code action requires extracting some substring of the code all the time. If no code action is returned, or a code action is returned that does not call this particular method, then this line is not called. I won't go into more details as I removed this call.
This call still seems like an O(N) operation. Why don't you get a DocumentTextBuffer from IDafnyCodeActionInput and let DocumentTextBuffer store a mapping from line to index that can be used to extract text using a Range in constant time?
Well, it's a O(N*C) where C is the number of code actions and N is the size of the file, and C is expected to be very small, so I wouldn't have worried about it.
However, I implemented such a conversion in the implementation DafnyCodeActionInput, so that the interface can expose the method Extract, and I removed the three ToToken methods that were converting relative positions to absolute positions.
/// If the edits can be provided instantly, you can use the derived class | ||
/// `new InstantDafnyCodeAction(title, edits)` | ||
/// </summary> | ||
public abstract class DafnyCodeAction { |
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.
Consider having record DafnyCodeAction(string Title, Container<Diagnostic> Diagnostics, Func<DafnyCodeActionEdit[]> GetEdits)
instead of the current InstantDafnyCodeAction
and DafnyCodeAction
.
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 would like to avoid this, since I know some implementers of code actions in F# and since it's an abstract class, they would need to implement boilerplate methods such as clone, copy, etc.
But even if you make it concrete, I'm not sure how the Func<> interfaces which are C# specific would translate to F#. So, let's avoid the complexity and keep the API as simple as possible.
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 think it might be good to take some time to agree on what language we're writing the API for. You seem to have settled on a particular style of API that sits between what is idiomatic for C# and F#, while I'm still trying to optimise for C#.
My reasoning is that if you want to have great APIs, you will need two APIs anyways, one for C# and one for F#, and if you start with a mixed API now you'll have to still create both idiomatic APIs later, while if you start with a C# optimised one now, you'll only have to add the F# one later. Secondly, for now, most of our API consumption will be from the dafny-lang/dafny
codebase, which is only C#.
The mixed API approach seems great if that's also what you plan on ending up with, which makes sense if you want to reduce the API maintenance cost by having only 1 for both C# and F#. However, investing in idiomatic APIs seems worth the effort to me.
In this particular case, wouldn't record DafnyCodeAction(string Title, Container<Diagnostic> Diagnostics, Func<DafnyCodeActionEdit[]> GetEdits)
work well for F# as well, since there is no abstract class, and F# can use a lambda expression for the GetEdits
argument?
|
||
// Edits are all performed at the same time | ||
// They are lazily invoked, so that they can be as complex as needed. | ||
public abstract DafnyCodeActionEdit[] GetEdits(); |
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.
This can return an IEnumerable<DafnyCodeActionEdit>
. Generally in C#, IEnumerable
is used whenever a collection is required, and this is only changed when performance requires something else. In this case, returning an array does not improve performance over returning an IEnumerable
, so the latter is preferred.
Regardless of C# conventions, returning an IEnumerable
here gives more flexibility to the API users, without downsides.
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.
without downsides.
It forces API users to link to the library System.Collection.Generic (I have to add one more import to the file as well). I can't call this "without downside".
Nevertheless, I did as you suggested.
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.
It forces API users to link to the library System.Collection.Generic
But that's part of the .NET framework is it not? Any C# application will use that namespace. Does this relate to C# vs F#?
/// <param name="range">The range to extract</param> | ||
/// <param name="text">The document</param> | ||
/// <returns>The substring of the document in the range</returns> | ||
public static string Extract(Range range, string text) { |
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 don't think this linear time complexity method should exist. We can add an Extract(Range range)
to DocumentTextBuffer
though.
|
||
var expression = DafnyCodeActionHelpers.Extract(relatedInformation.Location.Range, input.Code); | ||
var (beforeEndBrace, indentationExtra, indentationUntilBrace) = | ||
DafnyCodeActionHelpers.GetInformationToInsertAtEndOfBlock(input, diagnostic.Range.Start); |
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 find the API of DafnyCodeActionHelpers.GetInformationToInsertAtEndOfBlock
hard to understand. How about something like
static class RefactorTools {
public DafnyCodeActionEdit? InsertBlockAtReturnLocation(
DocumentTextBuffer text,
Range returnLocationRange,
IEnumerable<string> statementsToInsert)
}
?
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.
Thanks for this refactoring suggestion, I applied it.
Note that I only need one statement to insert. For multiple statement, it's sufficient to call this function repeatedly and store the edits in the same order, because this is how they are going to be applied.
/// <param name="endToken">The position of the closing brace</param> | ||
/// <param name="text">The document text</param> | ||
/// <returns>(extra indentation for a statement, current indentation)</returns> | ||
public static (string, string) GetIndentationBefore(IToken endToken, int startLine, int startCol, string text) { |
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.
In the long run, I imagine we might want to provide a different API for creating refactorings in Dafny, one that doesn't require thinking about formatting.
Here's a bad example:
Edit AddStatementToEnd(method: DafnyMethod) {
var original = method.Snapshot();
method.Body = method.Body.Concat(new SomeStatement(..));
Formatter.Format(method);
return DiffEngine.ComputeEdit(original, method);
}
I'm sure Roslyn has some interesting APIs for this very same purpose. I'd be great if we could get familiar with them, although I imagine it's quite a learning curve.
Here's an example: https://github.com/dotnet/roslyn/blob/main/src/Analyzers/CSharp/CodeFixes/MisplacedUsingDirectives/MisplacedUsingDirectivesCodeFixProvider.cs
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 yes ! That's also part of what I envision we will be able to do with the Dafny formater.
In any case, in the short-term after this PR, we will need to handle expressions, substitutions, etc. So formatting them might be a good idea.
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.
Ok,
Just want us and our users to be aware that the API might still change (a lot)
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, I hope we can move toward AST-based rewrites relatively soon. All this string processing makes me nervous.
/// <param name="line">The line of the opening brace</param> | ||
/// <param name="col">The column of the opening brace</param> | ||
/// <returns>The token of a matching closing brace, typically the `ÈndTok` of a BlockStmt</returns> | ||
private static IToken? GetMatchingEndToken(Dafny.Program program, string documentUri, int line, int col) { |
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.
Why not pass a Position openingBrace
instead of a line and col ?
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.
Because we need to compare the line and the column to a lot of tokens, and Position is offset by tokens by 1, so we would be computing unnecessary affine transformations all the time.
This is a private method anyway, we can always upgrade this later.
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.
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 agree we should do a better job at indexing the program to access various definitions, as we get more and more features about navigation.
For this PR, implementing a tree system is out of scope, because the verification completion plugin is a toy example and is meant to evolve; to avoid traversing the body of 10k methods, I added a check about line numbers, so that it will be very fast. I think that's the best reasonable we should aim at for now if we want this PR to be merged.
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.
This looks like it's in pretty good shape. Thanks! I requested a few changes, but they should be pretty quick.
/// Override this method to provide quick fixers | ||
/// </summary> | ||
/// <returns>An array of quick fixers implemented by this plugin</returns> | ||
public virtual DafnyCodeActionProvider[] GetDafnyCodeActionProviders() { |
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.
Given that you changed DafnyCodeActionProvider
to return an IEnumerable
, would it make sense to do so here, too? If you don't do that, I think you could instead remove using System.Collections.Generic
above.
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.
Good suggestion. For consistency with the original Dafny.Plugins.PluginConfiguration
that returns Compiler[]
and Rewriter[]
, I prefer to keep returning an array here. So I removed using System.Collections.Generic
@@ -121,4 +130,122 @@ Plugins are typically used to report additional diagnostics such as unsupported | |||
|
|||
Note that all plugin errors should use the original program's expressions' token and NOT `Token.NoToken`, else no error will be displayed in the IDE. | |||
|
|||
Morover, plugins should not write anything to `stdout` as it interferes with the communication protocol with the IDE. |
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.
Is this no longer true?
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, this is no longer true. Plugins can write to stdout now, and this information is recorded in a log file.
dafny/Source/DafnyLanguageServer/Program.cs
Lines 30 to 32 in 1472e40
// Prevent any other parts of the language server to actually write to standard output. | |
await using var logWriter = new LogWriter(); | |
Console.SetOut(logWriter); |
|
||
/// <summary> | ||
/// A verification quick fixers provides quick "fixes" for verification errors. | ||
/// For now, it offers to inline a failing postcondition if there is no "return" keyword. |
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 don't see logic in here related to whether there's a return
keyword or not. I'm not sure it's necessary to have that, but I'd update the comment to match the code.
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 rephrased it to
/// For now, it offers to inline a failing postcondition if its failure is
/// indicated on the '{' -- meaning there is no explicit return.
} | ||
|
||
/// <summary> | ||
/// You can safely assume that input.Program is not null |
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.
Could you update this to start with a summary of what the method does?
var result = Document.Diagnostics.ToArray(); | ||
if (result.Length == 0) { | ||
// For anonymous documents opened in VSCode | ||
result = Document.Diagnostics.ToArray(); |
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 I understand what's happening here. Does this do anything different from line 150?
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.
Ahah interesting ! the code there used to be
var result = Document.Errors.GetDiagnostics(Document.Uri).ToArray();
if (result.Length == 0) {
// For anonymous documents opened in VSCode
result = Document.Errors.GetDiagnostics(DocumentUri).ToArray();
}
so that it would handle separatedly the document URI for anonymous documents. However, since the recent refactorings, and the fact that URI are universally used, the case split is no longer user. Thanks for pointing out.
/// <param name="endToken">The position of the closing brace</param> | ||
/// <param name="text">The document text</param> | ||
/// <returns>(extra indentation for a statement, current indentation)</returns> | ||
public static (string, string) GetIndentationBefore(IToken endToken, int startLine, int startCol, string text) { |
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, I hope we can move toward AST-based rewrites relatively soon. All this string processing makes me nervous.
Me too. It's just that, with the guarantee that we don't need to replace variables for inlining postconditions, we should be fine. But with the PR of the formatter, we'll have access to tokens, so this will be even easier to print things (and indent them correctly!) |
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.
Looks good!
This PR introduces the DafnyLanguageServer support for QuickFixes, which are a kind of code actions, restricted to same-file edits.
QuickFixer
in dafny plugins only requires a plugin instantiation to implement the methodpublic abstract QuickFix[] GetQuickFixes(IQuickFixInput input, IToken selection);
(the current selection is passed as a token).QuickFix
have a title and a lazily computed list of text edits.IQuickFixInput
notably contains the code and the last program.I make use of this class in
DafnyLanguageServer
in the fileDafnyCodeActionHandler
, with two major functions calledHandle
, one is for returning a list of title + data to identity the command, the other is for resolving the code action to concrete edits.I included 1 battery:
VerificationQuickFixer
in its own file, which can currently inline a failing postcondition when there is no return statement, in method bodies only. It also work in nested blocks.This PR includes tests with a unified representation of code, designated hover space with
>>>
and expected insertion with[[message|insertedText]]
that should be pretty straightforward to understand and to maintain.It also includes a test where a plugin provides quick fixes to ensure plugins can correctly provide this functionality.
By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.