-
Notifications
You must be signed in to change notification settings - Fork 7
Implement signature help #34
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
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #34 +/- ##
==========================================
+ Coverage 68.55% 75.79% +7.23%
==========================================
Files 21 23 +2
Lines 1218 1417 +199
==========================================
+ Hits 835 1074 +239
+ Misses 383 343 -40 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Co-authored-by: Shuhei Kadowaki <[email protected]>
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 apologize for the delay in the review.
I was focused on submitting several other PRs that were pending.
I'm about to start the review on dtails, but I'd like to ask a question about the basic approach.
I believe the approach taken in this PR is based on JL, but could you explain why
a JL-based implementation is necessary, rather than the approach being attempted in
JuliaLang/julia#57767 (an advanced JS-based approach with setting up a custom node)?
Are there cases where scope resolution using JL is advantageous for implementing this feature?
I'm not opposed to using JL at all - in fact, I support it - but I'd like to deepen my understanding of the overall approach, while seeking for a possibly simpler alternative approach.
No worries, review whenever you have the time.
I do use JuliaLowering's syntax tree structure for parsed code, but nothing is lowered, and we could switch to
We don't use custom nodes currently, but Our current solution to finding the nodes containing the cursor is
No local scope resolution happens right now (our signatures are just |
|
Since I made quite large-scale changes to |
| """ | ||
| Resolve a name's value given a root module and an expression like `M1.M2.M3.f`, | ||
| which parses to `(. (. (. M1 M2) M3) f)`. If we hit something undefined, return | ||
| nothing. This doesn't support some cases, e.g. `(print("hi"); Base).print` | ||
| """ | ||
| function resolve_property(mod::Module, st0::JL.SyntaxTree) | ||
| if JS.is_leaf(st0) | ||
| # Would otherwise throw an unhelpful error. Is this true of all leaf nodes? | ||
| @assert JL.hasattr(st0, :name_val) | ||
| s = Symbol(st0.name_val) | ||
| !isdefined(mod, s) && return nothing | ||
| return getproperty(mod, s) | ||
| elseif kind(st0) === K"." | ||
| @assert JS.numchildren(st0) === 2 | ||
| lhs = resolve_property(mod, st0[1]) | ||
| return resolve_property(lhs, st0[2]) | ||
| end | ||
| JETLS_DEV_MODE && @info "resolve_property couldn't handle form:" mod st0 | ||
| return nothing | ||
| end |
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 you mention in the docstring, the better approach to do this might be to do something like
https://github.com/JuliaLang/julia/blob/1ea959a8b12f06391c305d600fb6942cb25905f6/stdlib/REPL/src/REPLCompletions.jl#L640-L674
It should allow us to handle cases like (print("hi"); Base).print.
Additionally, it might also allow us to account for argument types to some extent (by inferring the entire method call expression, and inspect types of IR elements corresponding to arguments).
However since it's a somewhat advanced approach, it's fine to tackle it in a separate PR. Maybe I'm better fit to write ConcreteEvaluator at least.
Also, since it's questionable whether that approach can handle incomplete code, it might be necessary to keep this resolve_property as a fallback, but I'm somewhat reluctant to provide two routines to do the same thing.
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 enhancement will be tackled in another PR.
src/signature-help.jl
Outdated
| # We could show the full docs, but there isn't(?) a way to separate by | ||
| # method (or resolve items lazily like completions), so we would be sending | ||
| # many copies. The user may have seen this already in the completions UI, | ||
| # too. | ||
| # documentation = MarkupContent(; | ||
| # kind = MarkupKind.Markdown, | ||
| # value = string(Base.Docs.doc(Base.Docs.Binding(m.var"module", m.name)))) |
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 is a way for doing this, but I agree with that the signature help don't need to show it.
Okay, got it. From you comment now I think |
| # TODO: (later) This should use type information from args (which we already | ||
| # have from m's params). For now, just parse the method signature like we | ||
| # do in make_siginfo. | ||
|
|
||
| mstr = sprint(show, m) | ||
| mnode = JS.parsestmt(JL.SyntaxTree, mstr; ignore_errors=true)[1] | ||
|
|
||
| params, kwp_i = flatten_args(mnode) | ||
| has_var_params = kwp_i > 1 && kind(params[kwp_i - 1]) === K"..." | ||
| has_var_kwp = kwp_i <= length(params) && kind(params[end]) === K"..." | ||
|
|
||
| kwp_map = find_kws(params, kwp_i; sig=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.
I feel that parsing the output of sprint(show, m::Method) and analyzing candidate methods based on it seems a bit hacky.
Is there a reason for taking this approach instead of using Base.arg_decl_parts/Base.kwarg_decl which are internally used by Base.show_method?
https://github.com/JuliaLang/julia/blob/9108dd08a572e394854d85aa0b2b680cc6a591c3/base/methodshow.jl#L217-L259
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.
(data race with the comment below) Discussed in meeting; definitely hacky, but since we want the provenance of parameters in the string, this might be the approach with the least amount of new code. Changing this later wouldn't be hard, just tedious (we'd copy the body of show(::Method) here and either use JuliaSyntax on that, or join strings together while keeping a counter)
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 we discussed in the meeting, since we're using the output of mstr = sprint(show, m::Method) as SignatureHelpInformation.label, we need position information within mstr for parameter highlighting, which is why the current implementation is designed this way.
For future improvements, I think we could consider the following options:
- Adjust
Base.show_methodto add an option that removes the@ /path/to/methodpart from the output (for making the parsing part safe) - For
Methodobjects that JETLS analyzes, maintain the original JS/JL definitions and use them for signature help as well
Anyway, I think we can proceed with this PR as it is for now.
src/signature-help.jl
Outdated
| # ============== | ||
|
|
||
| signature_help_options() = SignatureHelpOptions(; | ||
| triggerCharacters = ["(", ",", ";"], |
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.
Or, maybe we should register "all" characters (is that overkill?). We could consider cases where a user cancels signature help midway and then starts typing again, and it's a bit annoying that signature help is cancelled the moment " is typed right now. I feel like performance tuning handle_SignatureHelpRequest might be necessary though.
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.
By the way, as a hack that is only possible with VSCode, when experimenting with various triggerCharacters, the hack explained at
https://github.com/aviatesk/JETLS.jl?tab=readme-ov-file#dynamic-registration
might be useful.
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 would make sense to put = (and maybe more) in retriggerCharacters instead.
Agree the default vscode behaviour is annoying...for reference, eglot (in addition to trigger chars) just sends a signature help request whenever the cursor stops moving for a second or so. What does Zed do?
hack that is only possible with VSCode
Cool, will take a look
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 would make sense to put
=(and maybe more) inretriggerCharactersinstead.
The reason I think triggerCharacters might be better than retriggerCharacters is that currently, as per #34 (comment), when typing up to func(arg1, arg2; k|), the signature help gets canceled, so unless we trigger fresh signature help, it might not be retriggered.
At least in VSCode, once signature help disappears, it doesn't seem to be retriggered. However, eglot might handle this case better.
So, if #34 (comment) can be resolved, it might be fine to include = in retriggerCharacter. But registering it into triggerCharacters seems to be more robust and inefficient solution.
What does Zed do?
The situation with Zed is even worse...
The implementation of signature help in Zed is very primitive, and it doesn't seem to respect triggerCharacters at all. I've submitted this as an issue.
zed-industries/zed#31846
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.
if #34 (comment) can be resolved
Yup, I'm going to try and exclude k from the filtering logic here, as it doesn't make sense to filter on partially-typed kwargs. I'll test if = works well in retriggerCharacters then.
The situation with Zed is even worse
nooooo. Thanks for filing the issue haha
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 ended up liking = in triggerCharacters better, since typing = after a misspelled keyword would otherwise close the signature help for good
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 agree that that is a sensible choice.
Thanks! Co-authored-by: Shuhei Kadowaki <[email protected]>
They tend to show up anyway, so I'll stop trying to fight them. We might want
more type params.
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 PR is in a mergeable state, so let's move forward for now!
While there are still various things that could be improved, we'll enhance it iteratively in the future.
Thank you for producing excellent progress with this prototyping.


Shows method signatures when the user is typing out a function call.
Examples
Implemented
foo(|,foo(1,,|)Todo
dotcallsupport (simple to implement, I just need to decide what would go in the UI)One day
show(::Method)Misc griping
Ran into lots of flakiness in the clients. I estimate half of these are my own bugs and I've yet to figure it out, but I figured I should write them down somewhere.
.or;(and others)f(1,2,a...|). Eglot does the reasonable thing in my opinion, and vscode will fall back to highlighting the first.