Skip to content

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

Closed
wants to merge 81 commits into from
Closed

Explore type based autocomplete #493

wants to merge 81 commits into from

Conversation

zth
Copy link
Collaborator

@zth zth commented Jul 14, 2022

Work in progress. Exploring type based autocomplete.

Copy link
Collaborator

@cristianoc cristianoc left a 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
Copy link
Collaborator

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
Copy link
Collaborator

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]
Copy link
Collaborator

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.

Copy link
Collaborator

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])
Copy link
Collaborator

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])
Copy link
Collaborator

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.

Copy link
Collaborator

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
Copy link
Collaborator

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
[]
Copy link
Collaborator

Choose a reason for hiding this comment

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

As predicted, no result.

@cristianoc
Copy link
Collaborator

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.
What's needed is code to recognise that event, and emit a new completion primitive for expressions in a type context. E.g. CtypedContext.

I think the next step is to

  • Recognise when it is the case that we hit an expression assigned to a label and the label does not hit the cursor but the expression does.
  • When that happens, print "found typed context" in debug mode. That print should be visible in the test output.
  • As a refinement, also print the name of the function and the name of the label.

Back to you @zth

@cristianoc
Copy link
Collaborator

cristianoc commented Jul 15, 2022

A print just like others in the file. E.g. here is another example of print from the file if debug then Printf.printf "Attribute id:%s:%s label:%s\n" id.txt (Loc.toString id.loc) label;

@zth
Copy link
Collaborator Author

zth commented Jul 15, 2022

@cristianoc I think I got it: 5ed8bb4

I added a regular labelled argument completion in TypedContextCompletion.res to help distinguish between the various types.

Awaiting the next steps. Meanwhile, I'll continue exploring where this takes me.

@zth
Copy link
Collaborator Author

zth commented Jul 15, 2022

Continued my exploration here: #497. Separate PR to make things a bit easier to review.

else if exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor then None
else if
exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor
|| charBeforeCursor = Some '='
Copy link
Collaborator

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.

@cristianoc
Copy link
Collaborator

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 =.
It will mean trying several examples.
And if it does not work for all of them, something will be learned from that.

@zth
Copy link
Collaborator Author

zth commented Jul 16, 2022

@cristianoc I've removed the '=' special treatment, and added a few more examples. Looks like a typed context is picked up for everything but an empty assignment, which is likely due to parser recovery omitting the assignment if it's empty.

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?

@cristianoc
Copy link
Collaborator

Need to dig into the example that does not work. With the usual checking in and commenting on the debug output.

@zth
Copy link
Collaborator Author

zth commented Jul 16, 2022

@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 %rescript.exprhole that's being inserted in the expression by the parser error recovery. So the parser does recover the error properly and nicely, I was wrong.

@@ -433,6 +433,7 @@ module Completable = struct
| Cpath of contextPath
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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

Copy link
Collaborator

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.

@cristianoc
Copy link
Collaborator

This is great progress!
I think we can move on to the next phase and produce the desired result for all the existing examples. Without worrying too much about how things are represented or code duplication for now. (We'll do that after tackling at least one more kind of type context).

@zth
Copy link
Collaborator Author

zth commented Jul 17, 2022

This is great progress! I think we can move on to the next phase and produce the desired result for all the existing examples. Without worrying too much about how things are represented or code duplication for now. (We'll do that after tackling at least one more kind of type context).

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?

@cristianoc
Copy link
Collaborator

Right

Comment on lines 428 to 432
type typedContext = NamedArg of string

let typedContextToString typedContext =
match typedContext with
| NamedArg argName -> "NamedArg(" ^ argName ^ ")"
Copy link
Collaborator Author

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.

Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Comment on lines +20 to +56
[{
"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
}]
Copy link
Collaborator Author

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.

Copy link
Collaborator

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.

@zth
Copy link
Collaborator Author

zth commented Jul 17, 2022

@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.

Copy link
Collaborator

@cristianoc cristianoc left a 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.

@@ -992,6 +992,18 @@ let rec extractRecordType ~env ~package (t : Types.type_expr) =
| _ -> None)
| _ -> None

let rec extractVariantType ~env ~package (t : Types.type_expr) =
Copy link
Collaborator

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.

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 refactored this into a general one for extracting a type, so the code is slightly more ready for completing more things than just variants.

|> completionsGetTypeEnv
with
| Some (typ, _env) ->
let rec getLabels ~env (t : Types.type_expr) =
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Comment on lines 428 to 432
type typedContext = NamedArg of string

let typedContextToString typedContext =
match typedContext with
| NamedArg argName -> "NamedArg(" ^ argName ^ ")"
Copy link
Collaborator

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.

@@ -425,6 +425,12 @@ module Completable = struct
| CPObj of contextPath * string
| CPPipe of contextPath * string

type typedContext = NamedArg of string
Copy link
Collaborator

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.

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 made it a record instead and added comments.

Comment on lines +20 to +56
[{
"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
}]
Copy link
Collaborator

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",
Copy link
Collaborator

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.

Copy link
Collaborator Author

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:

  1. Complete someFn(~somethingOptional=, gives Some($1)
  2. 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.

Copy link
Collaborator

@cristianoc cristianoc Jul 17, 2022

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.

Copy link
Collaborator Author

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.

let typedContextToString typedContext =
match typedContext with
| NamedArg argName -> "NamedArg(" ^ argName ^ ")"

type t =
| Cdecorator of string (** e.g. @module *)
| CnamedArg of contextPath * string * string list
Copy link
Collaborator

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.

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'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:
Copy link
Collaborator

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?

Copy link
Collaborator Author

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(
Copy link
Collaborator

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.

Copy link
Collaborator Author

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".

Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

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.

@zth
Copy link
Collaborator Author

zth commented Jan 24, 2023

This work is superseeded by a bunch of other PRs. Closing.

@zth zth closed this Jan 24, 2023
@zth zth deleted the type-based-autocomplete branch December 25, 2023 21:12
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.

None yet

2 participants