Skip to content

Commit

Permalink
Tell lsp about variable bindings in match statements (#1926)
Browse files Browse the repository at this point in the history
* Tell lsp about variable bindings in match statements

* Also handle unapplied matches

* insta review

* Add a test getting match branch completions from type inference
  • Loading branch information
jneem authored May 27, 2024
1 parent ca0f78f commit b14aeea
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 1 deletion.
59 changes: 58 additions & 1 deletion lsp/nls/src/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use nickel_lang_core::{
environment::Environment as GenericEnvironment,
identifier::Ident,
position::RawSpan,
term::{RichTerm, Term, Traverse, TraverseControl},
term::{MatchData, RichTerm, Term, Traverse, TraverseControl},
};

use crate::{field_walker::Def, identifier::LocIdent, pattern::Bindings};
Expand Down Expand Up @@ -90,6 +90,46 @@ impl UsageLookup {
self.syms.insert(def.ident(), def);
}

// In general, a match is like a function in that it needs to be applied before we
// know what's being matched on. So for example, in
// ```
// match { x => x.ba }
// ```
// we can't do much to auto-complete "ba". But it's very common in practice for the match to
// be applied immediately, like
// ```
// y |> match { x => x.ba }
// ```
// and in this case we can look into `y` to extract completions for "ba".
//
// This is a long-winded way of saying that we can treat pattern bindings like we treat
// function bindings (if we don't know where the application is) or like we treat let bindings
// (if we know what the match gets applied to). Here, `value` is the value that the match
// is applied to, if we know it.
fn fill_match(&mut self, env: &Environment, data: &MatchData, value: Option<&RichTerm>) {
for branch in &data.branches {
let mut new_env = env.clone();
for (path, ident, _field) in branch.pattern.bindings() {
let def = match value {
Some(v) => Def::Let {
ident: ident.into(),
value: v.clone(),
path: path.into_iter().map(|x| x.ident()).collect(),
},
None => Def::Fn {
ident: ident.into(),
},
};
new_env.insert_def(def.clone());
self.add_sym(def);
}
self.fill(&branch.body, &new_env);
if let Some(guard) = &branch.guard {
self.fill(guard, &new_env);
}
}
}

fn fill(&mut self, rt: &RichTerm, env: &Environment) {
rt.traverse_ref(
&mut |term: &RichTerm, env: &Environment| {
Expand Down Expand Up @@ -162,6 +202,23 @@ impl UsageLookup {

TraverseControl::ContinueWithScope(new_env)
}
Term::App(f, value) => {
if let Term::Match(data) = f.as_ref() {
self.fill_match(env, data, Some(value));

// We've already traversed the branch bodies. We don't want to continue
// traversal because that will traverse them again. But we need to traverse
// the value we're matching on.
self.fill(value, env);
TraverseControl::SkipBranch
} else {
TraverseControl::Continue
}
}
Term::Match(data) => {
self.fill_match(env, data, None);
TraverseControl::SkipBranch
}
Term::Var(id) => {
let id = LocIdent::from(*id);
if let Some(def) = env.get(&id.ident) {
Expand Down
7 changes: 7 additions & 0 deletions lsp/nls/tests/inputs/completion-match-typed.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### /input.ncl
fun x => match { y => y.fo }
: { foo: Number, fo: Number } -> Number
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 0, character = 26 }
37 changes: 37 additions & 0 deletions lsp/nls/tests/inputs/completion-match.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
### /input.ncl
let p = 1 in
{
foo = { bar = 1 },
baz,
} |> match {
'Bar payload => p,
x => x.foo.ba,
{ foo = x } if x.ba == 1 => 3,
# In this next one, the match isn't applied so there will be no completions for the `blah`,
# but there should be a completion for `p`.
y => match { payload => p.blah },
}
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 5, character = 18 }
###
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 6, character = 14 }
###
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 7, character = 20 }
###
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 10, character = 26 }
###
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 10, character = 29 }
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: lsp/nls/tests/main.rs
expression: output
---
[fo, foo]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: lsp/nls/tests/main.rs
expression: output
---
[p, payload, std]
[bar]
[bar]
[p, payload, std, y]
[]

0 comments on commit b14aeea

Please sign in to comment.