-
Notifications
You must be signed in to change notification settings - Fork 59
Explore type based autocomplete #493
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
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.
First, let's walk through what's happening with this example.
@@ -1,3 +1,3 @@ | |||
DCE src/Dce.res | |||
issues:235 | |||
issues:241 |
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 should get rid of these differences. This is testing several things at once and it's not necessary.
@@ -0,0 +1,12 @@ | |||
Complete src/TypeContextCompletion.res 10:44 |
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 see what's going on here first.
This is the position of the cursor: 10:44
unfortunately expressed in 0-based lsp format so you need to do some +1 gymnastics when looking in the actual editor
But if you look at 11:45
in the opened file in the editor that's where ^
is pointing.
@@ -0,0 +1,12 @@ | |||
Complete src/TypeContextCompletion.res 10:44 | |||
posCursor:[10:44] posNoWhite:[10:43] Found expr:[10:11->18:1] |
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.
Now, since the cursor in autocomplete is always after what you want to complete, we need to go back to find anything interesting. And eat up any white space. In this case, just one character back to 10:43
.
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.
We have hit an AST expression which goes from line 10
to 18
. Next we'll dig deeper into that expression.
Why so many lines? That's parser error recovery, which apparently is taking in also the module definition.
Let's see if this gets in the way. If so, we'll remove the second part of the example and start again. As dealing with parser recovery is orthogonal to this.
@@ -0,0 +1,12 @@ | |||
Complete src/TypeContextCompletion.res 10:44 | |||
posCursor:[10:44] posNoWhite:[10:43] Found expr:[10:11->18:1] | |||
Pexp_apply ...[10:11->10:30] (~someVariant10:32->10:43=...[13:0->17:3]) |
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.
Digging inside, we found a sub-expression that is an application.
This is looking interesting. The cursor points right after the label someVariant
.
Complete src/TypeContextCompletion.res 10:44 | ||
posCursor:[10:44] posNoWhite:[10:43] Found expr:[10:11->18:1] | ||
Pexp_apply ...[10:11->10:30] (~someVariant10:32->10:43=...[13:0->17:3]) | ||
Completable: CnamedArg(Value[someVariantToString], "", [someVariant]) |
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.
So this is where it gets a bit hairy. Not sure why it decides it's a named arg completion given that it's in the expression assigned to the label. It could be because that expression is exactly where parser recovery happened and some heuristic is needed because of the uncertainty that parser recovery brings.
Something orthogonal, but need to figure it out in order to understand this example.
Anyhow the CnamedArg
completion finds the function name, and the labels already visited: [someVariant]
so they are not repeated. This is not going to find anything as it's the only label.
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 looking at the code, I think we're hitting this case:
| [] ->
if posAfterFunExpr <= posBeforeCursor && posBeforeCursor < endPos then
Some (CnamedArg (contextPath, "", allNames))
So we went through the entire application and did not find anything interesting. So we'll just default to named arg completion.
This can anyway be overridden by visiting sub-expressions later. The innermost AST expression where something interesting is found wins. Because the innermost is the most precise hit of the cursor.
To see why this case is needed one can just turn it off and see what tests fail.
Looks like this is one such example: let x = Lib.foo(~
posCursor:[10:44] posNoWhite:[10:43] Found expr:[10:11->18:1] | ||
Pexp_apply ...[10:11->10:30] (~someVariant10:32->10:43=...[13:0->17:3]) | ||
Completable: CnamedArg(Value[someVariantToString], "", [someVariant]) | ||
Found type for function (~someVariant: someVariant) => string |
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.
Need to check where this is coming from: presumably named arg completion finding the type of the function.
Pexp_apply ...[10:11->10:30] (~someVariant10:32->10:43=...[13:0->17:3]) | ||
Completable: CnamedArg(Value[someVariantToString], "", [someVariant]) | ||
Found type for function (~someVariant: someVariant) => string | ||
[] |
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 predicted, no result.
So let's avoid getting carried away by parser recovery which needs to be taken into account but is orthogonal. What's going on is we do find a label, a function, and an expression being assigned to the label which hits the cursor. Only there's no code to mark that event. I think the next step is to
Back to you @zth |
A print just like others in the file. E.g. here is another example of print from the file |
@cristianoc I think I got it: 5ed8bb4 I added a regular labelled argument completion in Awaiting the next steps. Meanwhile, I'll continue exploring where this takes me. |
Continued my exploration here: #497. Separate PR to make things a bit easier to review. |
analysis/src/CompletionFrontEnd.ml
Outdated
else if exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor then None | ||
else if | ||
exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor | ||
|| charBeforeCursor = Some '=' |
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 go slowly and stick to the script.
This is what you do when there is no other choice.
I would like to see how far we can go before trying to do this.
Also, we don't know if it's the character before the cursor.
I'd like to go slowly and make a tiny step at a time. Let's see how far we can push it before checking for |
@cristianoc I've removed the Having type based completion on an empty assignment is critical I'd say. What do you think is the next step? And are there more examples you'd like to see added? |
Need to dig into the example that does not work. With the usual checking in and commenting on the debug output. |
@cristianoc Ok, think I made some more progress. I stole a trick from the JSX prop completion that helps me identify the empty assignment. This is not in the commit, but I also verified that it's indeed a |
@@ -433,6 +433,7 @@ module Completable = struct | |||
| Cpath of contextPath |
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.
Would you start adding comments to Cpath and contextPath?
There's the question of whether there should be an entirely different case of type t, or whether typed context info should be integrated into the existing ones.
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.
Would you mind helping me out with adding comments to those that exist already? I have a hard time parsing what they are/mean.
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.
Cpath
is for complex expressions
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.
CPString
is for string constants, used in pipe expressions 34 -> ...
CPArray
is for arrays used in pipe expressions [] -> ....
CPApply of contextPath * Asttypes.arg_label list
is application with a list of labels
CPId of string list * completionContext
is for a path A.
or A.B.C.
and the rhs is the rest to be completed
CPField of contextPath * string
is the .z
of record access expr.z
CPObj of contextPath * string
is the ["hello"]
of object access expr["hello"]
CPPipe of contextPath * string
is expr->foo
where foo
is the function whose first argument is expr
.
This is great progress! |
Awesome! So, essentially, the next step is to produce the list of constructors for completion, disregarding exactly how, what code is shared, how it's represented exactly etc. Did I get that right? |
Right |
analysis/src/SharedTypes.ml
Outdated
type typedContext = NamedArg of string | ||
|
||
let typedContextToString typedContext = | ||
match typedContext with | ||
| NamedArg argName -> "NamedArg(" ^ argName ^ ")" |
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 was a very quick solution just to complete the actual items. I understand this is probably not the right way to do it.
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 just debug printing. Unless the comment refers to the type definition, which is perfectly fine for now.
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, it was referring to the type def.
[{ | ||
"label": "One", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "One\n\n", | ||
"documentation": null | ||
}, { | ||
"label": "Two", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "Two\n\n", | ||
"documentation": null | ||
}, { | ||
"label": "Three", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "Three\n\n", | ||
"documentation": null | ||
}, { | ||
"label": "Four", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "Four\n\n", | ||
"documentation": null | ||
}, { | ||
"label": "Five(_)", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "Five(int)\n\n", | ||
"documentation": null | ||
}, { | ||
"label": "Six(_)", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "Six(option<string>)\n\n", | ||
"documentation": 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.
Barring details like the payloads, filtering according to what the user has already typed, whether we should use snippets etc, this at least produces the output we're after - all of the variant constructors.
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.
Filtering is the next step. Nothing else need to be done.
Snippets etc we don't have to worry about. Perhaps later.
@cristianoc very crude version implemented that produces the correct output. I've tried to familiarize myself with the code a bit. Feel like I'm slowly getting there. |
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.
Nice!
Very good progress.
analysis/src/CompletionBackEnd.ml
Outdated
@@ -992,6 +992,18 @@ let rec extractRecordType ~env ~package (t : Types.type_expr) = | |||
| _ -> None) | |||
| _ -> None | |||
|
|||
let rec extractVariantType ~env ~package (t : Types.type_expr) = |
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's probably something like that already. Or maybe not.
Anyhow no need to worry about this for now.
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 refactored this into a general one for extracting a type, so the code is slightly more ready for completing more things than just variants.
analysis/src/CompletionBackEnd.ml
Outdated
|> completionsGetTypeEnv | ||
with | ||
| Some (typ, _env) -> | ||
let rec getLabels ~env (t : Types.type_expr) = |
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 must exist already. Doesn't matter for now.
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, it exists in another branch. I think it can be done in a better way, so can refactor that later.
analysis/src/SharedTypes.ml
Outdated
type typedContext = NamedArg of string | ||
|
||
let typedContextToString typedContext = | ||
match typedContext with | ||
| NamedArg argName -> "NamedArg(" ^ argName ^ ")" |
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 just debug printing. Unless the comment refers to the type definition, which is perfectly fine for now.
analysis/src/SharedTypes.ml
Outdated
@@ -425,6 +425,12 @@ module Completable = struct | |||
| CPObj of contextPath * string | |||
| CPPipe of contextPath * string | |||
|
|||
type typedContext = NamedArg of string |
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.
You can add a comment: the argument name of a function 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 made it a record instead and added comments.
[{ | ||
"label": "One", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "One\n\n", | ||
"documentation": null | ||
}, { | ||
"label": "Two", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "Two\n\n", | ||
"documentation": null | ||
}, { | ||
"label": "Three", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "Three\n\n", | ||
"documentation": null | ||
}, { | ||
"label": "Four", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "Four\n\n", | ||
"documentation": null | ||
}, { | ||
"label": "Five(_)", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "Five(int)\n\n", | ||
"documentation": null | ||
}, { | ||
"label": "Six(_)", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "Six(option<string>)\n\n", | ||
"documentation": 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.
Filtering is the next step. Nothing else need to be done.
Snippets etc we don't have to worry about. Perhaps later.
"label": "Five(_)", | ||
"kind": 4, | ||
"tags": [], | ||
"detail": "Five(int)\n\n", |
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's the question of what one would like to show here.
O I get it. Now I understand what "snippets" means. They're the templates for a variant with payloads?
If so then sure why not tackle them next.
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, essentially we could return this: Five($1, $2)
and the editor would put the cursor on $1
first, and then a tab would put the cursor on $2
. This can be quite convenient when autocompleting things like this. For example, later when we do sub expression type aware completion (that's not a proper sentence...), the experience could be quite nice. Something like:
- Complete
someFn(~somethingOptional=
, givesSome($1)
- Select that completion, trigger autocomplete again. Get a list of all variant constructors if it's a variant, or all identifiers in scope matching the inner type of
somethingOptional
, etc
We do need to support both with and without snippets, because I'm unsure if all editors support them. I'll have a look at that 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.
Don't know if "sub expression type aware completion" is a proper sentence but is sure sounds cool.
What kind of format does vocode support for these kinds of templates? Does it expect things with dollar signs.
It would be great if there were a way to convey the type too, but don't know what format is possible and one would not like super-long names. But if the names were hoverable, then that would be pretty sweet.
Not sure if you want to go down the rabbit hole now, or move on with other aspects and come back to 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.
We can come back later, there's other things snippet syntax would be nice to leverage for too. But yeah, too much of a side track now.
analysis/src/SharedTypes.ml
Outdated
let typedContextToString typedContext = | ||
match typedContext with | ||
| NamedArg argName -> "NamedArg(" ^ argName ^ ")" | ||
|
||
type t = | ||
| Cdecorator of string (** e.g. @module *) | ||
| CnamedArg of contextPath * string * string list |
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.
~prefix
with [this, that]
seen.
So it would complete a label whose name starts with prefix
and excludes labels this
and that
which have been used already and should not be 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.
I've done that now, but I haven't done anything about this
/that
seen, because I don't think it makes sense in the context of completing a labelled argument? There's nothing else to have been seen already, right?
@@ -0,0 +1,62 @@ | |||
Dump AST src/BrokenParserCases.res 1:22 | |||
|
|||
Source: |
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 the source is repeated, how about also indicating what line number this is?
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, for sure! I need to re-do this part though, it's very hacky and breaks for a lot of cases. I did however find it quite convenient to be able to just scan the resulting txt and have everything I need to think about the AST there.
// let _ = ({someProp: , otherProp: 123}, 123) | ||
// ^ast | ||
|
||
<*>Pstr_value( |
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 does <*>
indicate? Seems redundant information-wise, but I guess it follows some existing convention of something.
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 existing convention, just something I made up. Here's a legend:
<*>
means it has the cursor<x>
means it has an empty loc
It's redundant at the top level Pstr_value
etc because the value itself would never get printed unless the cursor was there. But, I was confused several times myself not seeing <*>
, so I added it there too. Imo having it there too makes it easier to scan for "anchor points" of the cursor, like "there's no cursor (but a broken loc) inside of the structure item, but the structure item itself has the cursor, so we can use that to figure out the context".
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.
Got it. How about the ^ symbol instead of * so it's consistent with other tests.
Also question on terminology: is this a hit test? So everything containing the cursor has it, not a single node?
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.
Sure, that sounds reasonable.
If I understand what you mean then yes, it's a hit test in the sense that the structure item printed needs to have it, but nothing else inside of the structure item technically needs to have the cursor for it to hit.
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.
Ahhh yes. Now I understand. The answer is yes, it's a hit test, and everything containing the cursor has it, not just a single node.
…expression strictly does not compile (but the compiler understands what type it will be eventually
This work is superseeded by a bunch of other PRs. Closing. |
Work in progress. Exploring type based autocomplete.