-
-
Notifications
You must be signed in to change notification settings - Fork 869
Code actions to add and remove anonymous functions #4789
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
base: main
Are you sure you want to change the base?
Conversation
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.
Thank you! Looking really good! I've left a bunch of comments inline, let me know if anything is unclear.
Please un-draft the PR and tag me when you are ready for a review. Thank you
variables | ||
} | ||
|
||
fn from_expr(expr: &TypedExpr) -> Self { |
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.
No abbreviations please 🙏 expr -> expression
It's not very clear to me from the name what this does. A doc comment would help a lot, and maybe a more descriptive name
} | ||
} | ||
|
||
struct FunctionToWrap { |
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.
Document this please
} | ||
} | ||
|
||
impl<'ast> ast::visit::Visit<'ast> for WrapInAnonymousFunction<'ast> { |
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 this is computing the code action for all non-nested function references that are assigned to variables or are passed as arguments, but we want only the ones that are currently within the range specified by the code action parameters.
I think it would ideally be any non-called function rather than specifically these two positions in the AST, and think the non-nested restriction should be removed.
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.
the range check makes sense, I'd missed it before bc I was looking at RemoveUnusedImports
which obviously doesn't need it 🤦
Lots of the other actions store their TextEdits
in the struct but only need it there for the range check, which can be done with just a LineNumbers
. Storing LineNumbers
seems more natural to me but I'm splitting hairs and will defer to consistency if you like
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.
The non-nested restriction was inadvertent, if I'm reading it right I need to call back into the visitor after I'm done?
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.
Using line numbers seems good to me!
|
||
if call_arguments != expected_arguments { | ||
return; | ||
} |
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.
Document the logic in this section please
} | ||
} | ||
|
||
// match fn bodies with only a single function call |
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.
Capital letters at the start of comments please, and explain why something is done rather than what is done. Thank you
const GENERATE_VARIANT: &str = "Generate variant"; | ||
const REMOVE_BLOCK: &str = "Remove block"; | ||
const WRAP_IN_ANONYMOUS_FUNCTION: &str = "Wrap in anonymous function"; | ||
const UNWRAP_ANONYMOUS_FUNCTION: &str = "Unwrap anonymous function"; |
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 this sounds like the anonymous function remains but its wrapper will be removed. "Remove anonymous function wrapper" perhaps?
"import gleam/list | ||
pub fn main() { | ||
list.map([1, 2, 3], op) |
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.
2 space indentation for Gleam please 🙏
", | ||
find_position_of("fn(int)").to_selection() | ||
); | ||
} |
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.
Let's add more tests to cover all the possible scenarios. We'll want to cover:
- Labelled arguments
- Pipes
- Qualified functions from other modules
- Records
- Comments
And anything else you can think of!
Ok, there was more to do here than I thought but the code is better than before 🎉 I've copped out and not offered the action on functions with comments in. I think it's doable, but found an issue I wanted some advice on while I was on my way there. The |
Probably also want a changelog entry, right? Thanks for doing this! |
Ok, I've fixed |
Comments shouldn't change the behaviour of the language server, so we would want to provide code actions regardless of whether or not there are comments. Having imperfect formatting afterwards when there's comments doesn't sound too bad, the programmer can run the formatter. Do you have some examples of what the output looks like? I've less context than you, but it seems like comments inside the body of the function shouldn't be much of an issue as they can be left as-is: fn(x, y) {
// comment
thingy(x, y)
} // comment
thingy For comments inside the parameters I think we can discard them happily. fn(
// Hello
x,
y,
) {
thingy(x, y)
} thingy |
It's not really anything that the autoformatter wouldn't tidy up. There are some snapshots in this commit, the worst it gets is probably things like this one where the comment has moved lines but that's not too big a deal. If that's acceptable (or close enough to work on) I'll include it in this PR |
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.
Awesome, really nice work!! I've left a few small notes inline.
When you are ready for a review please un-draft this PR and tag me for a review. Thank you!
compiler-core/src/parse/extra.rs
Outdated
best = self.comments.get(index).copied(); | ||
search_list = search_list.get(0..index).unwrap_or(&[]); | ||
} | ||
best |
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 are we now traversing the comments and copying their locations multiple times?
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've moved the copy out of the loop but I think the multiple-traversal is necessary to ensure we return the first comment in the range rather than just a comment in the range. I added some failing tests for the previous implementation in this commit, I can comment this new one more to explain the method properly
/// Helper struct, a target for [WrapInAnonymousFunction]. | ||
struct FunctionToWrap { | ||
location: SrcSpan, | ||
arguments: Vec<Arc<Type>>, |
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 could be borrowed I think
arguments: Vec<Arc<Type>>, | |
arguments: &'a [Arc<Type>], |
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 can't figure out how to get that past borrowck 😕. The arguments are pulled from a field of an Arc<Type>
here. If we want to avoid cloning the argument list then I could clone the whole Arc<Fn>
for the function instead and pull the arguments out later
--- | ||
----- BEFORE ACTION | ||
pub fn main() { | ||
let f = fn(in) { ception(in) } |
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 we should be offering to wrap an anonymous function in another anonymous function
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.
fixed
--- | ||
----- BEFORE ACTION | ||
pub fn main() { | ||
1 |> wibble |> wobble |
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 we have a test for wibble taking 2 arguments please
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.
added
|
||
// We need to delete the anonymous function's head but preserve | ||
// comments between it and the inner function call. | ||
edits.delete(self.span_until_comment(SrcSpan { |
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.
Rather than seek through the comments multiple times I think we could use the location stored in the FunctionLiteralKind::Anonymous
variant.
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 by pulling more data through from the parser we can eliminate both comment-checks, but I'm not sure we can currently eliminate either.
Here's what I think we get from the parser currently:
fn(arg_1: T1, arg_2: T2) -> T3 { [comments] inner_function(arg_1, arg_2) [comments] }
└─────────────head───────────┘ └────inner───┘
└─────────────────────────────────────────outer──────────────────────────────────────┘
So means if I pull the opening brace through the parser I can eliminate this first comment-seek, and if I pull the span of the inner argument-list through I can eliminate the second. Without either I think we risk mangling things if the input isn't well-formatted (e.g. too much space before the opening brace)
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've pulled more locations through the parser, and have followed this approach so we now don't care about comments. I've left the bug-fix in though!
|
||
// Now we need to delete the inner function call's arguments, | ||
// preserving comments before the outer function tail. | ||
edits.delete(self.span_until_comment(SrcSpan { |
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 TypedExpr::Call
variant we could store the byte index of the (
to make it easier to get that position.
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.
see previous
Do remember to undraft this PR and tag me when you're ready for a review! |
plus a bunch more tests
previously this could have returned any comment in the span rather than guaranteeing the first
The indentation of comments when we're done with them might be a bit off, but it's nothing an autoformat shouldn't be able to fix
a0c331f
to
203a523
Compare
This increases the blast radius of the change a bit, but means we don't need to worry about comments any more.
203a523
to
58626bf
Compare
Sorry for the delay, I wanted some advice on which route to take to fix the problems, and then life got in the way. I think the new approach is better, and I've rebased on top of main. Ready for review @lpil |
I've had a go at implementing new code actions discussed in #4614, one to wrap a reference to a function in an anonymous function, and one to unwrap a trivial anonymous function into a bare reference.
These seemed pretty straightforward to implement so I'm a bit concerned I've missed a whole slew of edge cases. Any pointers as to more test cases I should add would be great
I know the issue was inconclusive about the names. These ones seemed straightforward but bikeshedding is welcome
Closes #4614