Skip to content
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

Combine metadata for completion items instead of choosing arbitrarily. #1940

Merged
merged 5 commits into from
Jun 18, 2024

Conversation

jneem
Copy link
Member

@jneem jneem commented Jun 5, 2024

Fixes #1933

@jneem jneem requested a review from yannham June 5, 2024 00:24
@github-actions github-actions bot temporarily deployed to pull request June 5, 2024 00:26 Inactive
Copy link
Member

@yannham yannham left a comment

Choose a reason for hiding this comment

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

Sorry, I proposed this solution, but I just realize it's not correct, because you don't really know if you should actually conflate all foos or if they are logically different.

For example:

let C = { foo | Number | doc "Some documentation" } in
{
  foo | String,
  bar = {
    f<CURSOR> 
  } | C
}

In this example, the LSP incorrectly suggests that the contracts for foo are String, Number. But the foo from the enclosing scope and the one coming from C shouldn't actually be merged.

Could we have a way of computing if the external foo is actually shadowed by the inner foo (as in the example above), in which case the correct behavior is to show only the inner one? I guess this was the original behavior, right? The difficult part is to know when they aren't actually shadowing each other but are "at the same level" and are going to be merged.

I thought that another dumber solution could be to just special case the pattern of the original issue (which is common, a bit like "declaring a function argument"), but even that is incorrect:

let C = { foo | Number | doc "Some documentation" } in
{
  bar = {
    foo,

    f<CURSOR> 
  }
} | C

This solution would suggest that foo must be Number, while once, again, the two foos are different and the inner one is unconstrained.

@jneem
Copy link
Member Author

jneem commented Jun 5, 2024

Hm, I think the issue isn't just the scope, but the syntactic position of the thing we're completing, right? I mean, if you're trying to complete the value instead of the field name like this:

let C = { foo | Number | doc "Some documentation", .. } in
{
  foo | String,
  bar = {
    baz = f<CURSOR> 
  } | C
}

then you want the string one, right? The current implementation does take scoping into account (for example, in let foo = 1 in let foo = 2 in fo<CURSOR> it knows which foo is in scope), but it's combining multiple sources of completions (specifically: variables in scope, and "cousins" in record merges) without knowing which sources are appropriate for the context.

I think the mechanism for this should be somewhat similar to #1932, where we need to do filtering of completion candidates for field names.

Also, the current behavior just takes the metadata from an arbitrary completion candidate. I think that combining all valid completion candidates is the right behavior; we just need some more work to filter out invalid ones. As an example where you'd want to merge multiple candidates:

let GenericC = { foos | Array Dyn | doc "some things"} in
let SpecificC = { foos  | Array String | doc "some specific things" } in
{
  fo<CURSOR>
} | GenericC | SpecificC

(Ok, so in this example we actually want the docs from SpecificC, but this might be hard to determine so I think merging them is fine.)

@yannham
Copy link
Member

yannham commented Jun 6, 2024

Hm, I think the issue isn't just the scope, but the syntactic position of the thing we're completing, right? I mean, if you're trying to complete the value instead of the field name like this:

Ah, this is a good point, but I think still rather a separate one. At any location in a source file, there is a local environment where many foos can appear coming from different sources/scopes. The original issue is, some of them will actually be merged in the final result (cousins, siblings?), so you want to merge their metadata to have maximal information. I believe which one you should merge is entirely based on scoping (maybe some extended notion of scoping, because in Nickel {foo | C = ...}doesn't bring the variables ofC` into scope, but completion does - the point stands that it's purely lexical).

In your last example we should indeed merge them - the issue with merging documentation is unfortunate, but kinda beside the point. In fact we decided to just arbitrarily get rid of one docstring in combine, but we could very much concatenate documentation when they're non-empty and different.

Then, it turns out we only want to keep one suggestion, because of shadowing. So it's true that in practice this is equivalent to decide "should I get rid of this other suggestion or merge it with the current, most local one?". In the end we're probably saying the same thing in a different way 🙂

About the fact that you might want a different rationale for completion when defining a field or using a field, I think it boils down to the official Nickel scoping versus NLS completion "scoping": stuff brought into "scope" by contracts and types aren't considered to be into scope by Nickel (yet, but some people would actually expect it, see #1915 (comment) and the whole discussion). So maybe just filtering out stuff coming from types/contracts for completion in the right-hand side of ab =, while considering it as a proper part of the environment like any other sibling for the left hand side is sufficient?

At a distance it looks like the more involved part is deciding what to merge and what to drop. I imagine you'd need some sort of "level" or similar metadata attached to bindings in the environment that help you decide which declaration takes precedence, and which declarations are at the same level and should be merged. Then, when doing {...record literal...} | C, completion info coming from C should be at the same level as top-level recursive fields in the record literal; that is, if the literal is {foo = .., ..etc..} and C = {bar, baz}, then level(foo) == level(bar) == level(baz).

@jneem jneem requested a review from yannham June 14, 2024 21:47
@github-actions github-actions bot temporarily deployed to pull request June 14, 2024 21:49 Inactive
@jneem
Copy link
Member Author

jneem commented Jun 14, 2024

@yannham I think this is ready for another look. I basically just separated out an extra case, so now we have three cases: completing record paths like foo.bar.baz in expression position, completing record field names like { foo.bar.baz }, and everything else. Only the "everything else" looks in the environment.

There's one (pre-existing) bug that I know of, in the that incomplete parsing heuristics misclassify { foo.bar. } as the first case instead of the second. I'm not sure of a quick way to improve this...

(edit: this will also fix #1932)

}
grouped
.entry(item.label.clone())
.and_modify(|old| *old = Combine::combine(old.clone(), item.clone()))
Copy link
Member

Choose a reason for hiding this comment

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

Could be nice to write documentation for this function explaining that it combines the metadata of identical items, because I fear it's not obvious from the function's name anymore

Copy link
Member Author

Choose a reason for hiding this comment

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

I refactored it a little to make it more clear (and also added documentation)

lsp/nls/src/requests/completion.rs Show resolved Hide resolved
lsp/nls/src/requests/completion.rs Outdated Show resolved Hide resolved
@yannham
Copy link
Member

yannham commented Jun 18, 2024

@jneem Oh and I forgot: could we also have the test case that I included in my comment above:

let C = { foo | Number | doc "Some documentation" } in
{
  foo | String,
  bar = {
    f<CURSOR> 
  } | C
}

I'm not entirely convinced the issue is solved, because I don't know where foos are coming from here, in the divide field_completion/env_completion

jneem and others added 2 commits June 18, 2024 14:07
@github-actions github-actions bot temporarily deployed to pull request June 18, 2024 19:12 Inactive
@jneem jneem enabled auto-merge June 18, 2024 19:17
@github-actions github-actions bot temporarily deployed to pull request June 18, 2024 19:19 Inactive
@jneem
Copy link
Member Author

jneem commented Jun 18, 2024

The foo | String will be in the environment so it will be there if we ask for env_completion. The foo | Number will come from the "cousin" (really more like a sibling) record so it will be there if we ask for field_completion. In that cursor position it asks for field completion, so it will get only foo | Number. An example where you'd get env completion instead would be

let C = { foo | Number | doc "Some documentation" } in
{
  foo | String,
  bar = {
    f = fo<CURSOR> 
  } | C
}

@jneem jneem added this pull request to the merge queue Jun 18, 2024
Merged via the queue into master with commit f5b5876 Jun 18, 2024
5 checks passed
@jneem jneem deleted the doc-fusion branch June 18, 2024 19:40
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.

A completion candidate can shadow a more informative one
2 participants