1
1
mirror of https://github.com/tweag/nickel.git synced 2024-10-06 08:07:37 +03:00

Context completion (#1584)

* Add context completion

* Review comments

* Use either record completion or env completion, but not both

* Add test case for env completion

* Fix compilation
This commit is contained in:
jneem 2023-09-21 09:33:53 -05:00 committed by GitHub
parent 7937e5a175
commit df15becc5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 426 additions and 163 deletions

View File

@ -324,8 +324,12 @@ impl Traverse<RichTerm> for RuntimeContract {
Ok(RuntimeContract { contract, ..self })
}
fn traverse_ref<U>(&self, f: &mut dyn FnMut(&RichTerm) -> TraverseControl<U>) -> Option<U> {
self.contract.traverse_ref(f)
fn traverse_ref<S, U>(
&self,
f: &mut dyn FnMut(&RichTerm, &S) -> TraverseControl<S, U>,
state: &S,
) -> Option<U> {
self.contract.traverse_ref(f, state)
}
}
@ -478,8 +482,12 @@ impl Traverse<RichTerm> for LabeledType {
typ.traverse(f, order).map(|typ| LabeledType { typ, label })
}
fn traverse_ref<U>(&self, f: &mut dyn FnMut(&RichTerm) -> TraverseControl<U>) -> Option<U> {
self.typ.traverse_ref(f)
fn traverse_ref<S, U>(
&self,
f: &mut dyn FnMut(&RichTerm, &S) -> TraverseControl<S, U>,
state: &S,
) -> Option<U> {
self.typ.traverse_ref(f, state)
}
}
@ -592,11 +600,15 @@ impl Traverse<RichTerm> for TypeAnnotation {
Ok(TypeAnnotation { typ, contracts })
}
fn traverse_ref<U>(&self, f: &mut dyn FnMut(&RichTerm) -> TraverseControl<U>) -> Option<U> {
fn traverse_ref<S, U>(
&self,
f: &mut dyn FnMut(&RichTerm, &S) -> TraverseControl<S, U>,
state: &S,
) -> Option<U> {
self.contracts
.iter()
.find_map(|c| c.traverse_ref(f))
.or_else(|| self.typ.as_ref().and_then(|t| t.traverse_ref(f)))
.find_map(|c| c.traverse_ref(f, state))
.or_else(|| self.typ.as_ref().and_then(|t| t.traverse_ref(f, state)))
}
}
@ -1625,8 +1637,14 @@ impl RichTerm {
}
/// Flow control for tree traverals.
pub enum TraverseControl<U> {
pub enum TraverseControl<S, U> {
/// Normal control flow: continue recursing into the children.
///
/// Pass the state &S to all children.
ContinueWithScope(S),
/// Normal control flow: continue recursing into the children.
///
/// The state that was passed to the parent will be re-used for the children.
Continue,
/// Skip this branch of the tree.
@ -1636,7 +1654,7 @@ pub enum TraverseControl<U> {
Return(U),
}
impl<U> From<Option<U>> for TraverseControl<U> {
impl<S, U> From<Option<U>> for TraverseControl<S, U> {
fn from(value: Option<U>) -> Self {
match value {
Some(u) => TraverseControl::Return(u),
@ -1659,7 +1677,19 @@ pub trait Traverse<T>: Sized {
///
/// Through its return value, `f` can short-circuit one branch of the traversal or
/// the entire traversal.
fn traverse_ref<U>(&self, f: &mut dyn FnMut(&T) -> TraverseControl<U>) -> Option<U>;
///
/// This traversal can make use of "scoped" state. The `scope` argument is passed to
/// each callback, and the callback can optionally override that scope just for its
/// own subtree in the traversal. For example, when traversing a tree of terms you can
/// maintain an environment. Most of the time the environment should get passed around
/// unchanged, but a `Term::Let` should override the environment of its subtree. It
/// does this by returning a `TraverseControl::ContinueWithScope` that contains the
/// new environment.
fn traverse_ref<S, U>(
&self,
f: &mut dyn FnMut(&T, &S) -> TraverseControl<S, U>,
scope: &S,
) -> Option<U>;
}
impl Traverse<RichTerm> for RichTerm {
@ -1857,9 +1887,14 @@ impl Traverse<RichTerm> for RichTerm {
}
}
fn traverse_ref<U>(&self, f: &mut dyn FnMut(&RichTerm) -> TraverseControl<U>) -> Option<U> {
match f(self) {
TraverseControl::Continue => {}
fn traverse_ref<S, U>(
&self,
f: &mut dyn FnMut(&RichTerm, &S) -> TraverseControl<S, U>,
state: &S,
) -> Option<U> {
let child_state = match f(self, state) {
TraverseControl::Continue => None,
TraverseControl::ContinueWithScope(s) => Some(s),
TraverseControl::SkipBranch => {
return None;
}
@ -1867,6 +1902,7 @@ impl Traverse<RichTerm> for RichTerm {
return Some(ret);
}
};
let state = child_state.as_ref().unwrap_or(state);
match &*self.term {
Term::Null
@ -1883,7 +1919,7 @@ impl Traverse<RichTerm> for RichTerm {
| Term::RuntimeError(_) => None,
Term::StrChunks(chunks) => chunks.iter().find_map(|ch| {
if let StrChunk::Expr(term, _) = ch {
term.traverse_ref(f)
term.traverse_ref(f, state)
} else {
None
}
@ -1891,29 +1927,37 @@ impl Traverse<RichTerm> for RichTerm {
Term::Fun(_, t)
| Term::FunPattern(_, _, t)
| Term::Op1(_, t)
| Term::Sealed(_, t, _) => t.traverse_ref(f),
| Term::Sealed(_, t, _) => t.traverse_ref(f, state),
Term::Let(_, t1, t2, _)
| Term::LetPattern(_, _, t1, t2)
| Term::App(t1, t2)
| Term::Op2(_, t1, t2) => t1.traverse_ref(f).or_else(|| t2.traverse_ref(f)),
Term::Record(data) => data.fields.values().find_map(|field| field.traverse_ref(f)),
| Term::Op2(_, t1, t2) => t1
.traverse_ref(f, state)
.or_else(|| t2.traverse_ref(f, state)),
Term::Record(data) => data
.fields
.values()
.find_map(|field| field.traverse_ref(f, state)),
Term::RecRecord(data, dyn_data, _) => data
.fields
.values()
.find_map(|field| field.traverse_ref(f))
.find_map(|field| field.traverse_ref(f, state))
.or_else(|| {
dyn_data.iter().find_map(|(id, field)| {
id.traverse_ref(f).or_else(|| field.traverse_ref(f))
id.traverse_ref(f, state)
.or_else(|| field.traverse_ref(f, state))
})
}),
Term::Match { cases, default } => cases
.iter()
.find_map(|(_id, t)| t.traverse_ref(f))
.or_else(|| default.as_ref().and_then(|t| t.traverse_ref(f))),
Term::Array(ts, _) => ts.iter().find_map(|t| t.traverse_ref(f)),
Term::OpN(_, ts) => ts.iter().find_map(|t| t.traverse_ref(f)),
Term::Annotated(annot, t) => t.traverse_ref(f).or_else(|| annot.traverse_ref(f)),
Term::Type(ty) => ty.traverse_ref(f),
.find_map(|(_id, t)| t.traverse_ref(f, state))
.or_else(|| default.as_ref().and_then(|t| t.traverse_ref(f, state))),
Term::Array(ts, _) => ts.iter().find_map(|t| t.traverse_ref(f, state)),
Term::OpN(_, ts) => ts.iter().find_map(|t| t.traverse_ref(f, state)),
Term::Annotated(annot, t) => t
.traverse_ref(f, state)
.or_else(|| annot.traverse_ref(f, state)),
Term::Type(ty) => ty.traverse_ref(f, state),
}
}
}
@ -1934,11 +1978,18 @@ impl Traverse<Type> for RichTerm {
)
}
fn traverse_ref<U>(&self, f: &mut dyn FnMut(&Type) -> TraverseControl<U>) -> Option<U> {
self.traverse_ref(&mut |rt: &RichTerm| match &*rt.term {
Term::Type(ty) => ty.traverse_ref(f).into(),
_ => TraverseControl::Continue,
})
fn traverse_ref<S, U>(
&self,
f: &mut dyn FnMut(&Type, &S) -> TraverseControl<S, U>,
state: &S,
) -> Option<U> {
self.traverse_ref(
&mut |rt: &RichTerm, state: &S| match &*rt.term {
Term::Type(ty) => ty.traverse_ref(f, state).into(),
_ => TraverseControl::Continue,
},
state,
)
}
}

View File

@ -191,15 +191,19 @@ impl Traverse<RichTerm> for Field {
})
}
fn traverse_ref<U>(&self, f: &mut dyn FnMut(&RichTerm) -> TraverseControl<U>) -> Option<U> {
fn traverse_ref<S, U>(
&self,
f: &mut dyn FnMut(&RichTerm, &S) -> TraverseControl<S, U>,
state: &S,
) -> Option<U> {
self.metadata
.annotation
.traverse_ref(f)
.or_else(|| self.value.as_ref().and_then(|v| v.traverse_ref(f)))
.traverse_ref(f, state)
.or_else(|| self.value.as_ref().and_then(|v| v.traverse_ref(f, state)))
.or_else(|| {
self.pending_contracts
.iter()
.find_map(|c| c.traverse_ref(f))
.find_map(|c| c.traverse_ref(f, state))
})
}
}

View File

@ -624,11 +624,16 @@ impl Traverse<Type> for RecordRows {
Ok(RecordRows(rows))
}
fn traverse_ref<U>(&self, f: &mut dyn FnMut(&Type) -> TraverseControl<U>) -> Option<U> {
fn traverse_ref<S, U>(
&self,
f: &mut dyn FnMut(&Type, &S) -> TraverseControl<S, U>,
state: &S,
) -> Option<U> {
match &self.0 {
RecordRowsF::Extend { row, tail } => {
row.typ.traverse_ref(f).or_else(|| tail.traverse_ref(f))
}
RecordRowsF::Extend { row, tail } => row
.typ
.traverse_ref(f, state)
.or_else(|| tail.traverse_ref(f, state)),
_ => None,
}
}
@ -1054,10 +1059,13 @@ impl Type {
/// Searches for a `TypeF::Flat`. If one is found, returns the term it contains.
pub fn find_flat(&self) -> Option<RichTerm> {
self.traverse_ref(&mut |ty: &Type| match &ty.typ {
TypeF::Flat(f) => TraverseControl::Return(f.clone()),
_ => TraverseControl::Continue,
})
self.traverse_ref(
&mut |ty: &Type, _: &()| match &ty.typ {
TypeF::Flat(f) => TraverseControl::Return(f.clone()),
_ => TraverseControl::Continue,
},
&(),
)
}
}
@ -1089,12 +1097,22 @@ impl Traverse<Type> for Type {
}
}
fn traverse_ref<U>(&self, f: &mut dyn FnMut(&Type) -> TraverseControl<U>) -> Option<U> {
match f(self) {
TraverseControl::Continue => (),
TraverseControl::SkipBranch => return None,
TraverseControl::Return(ret) => return Some(ret),
fn traverse_ref<S, U>(
&self,
f: &mut dyn FnMut(&Type, &S) -> TraverseControl<S, U>,
state: &S,
) -> Option<U> {
let child_state = match f(self, state) {
TraverseControl::Continue => None,
TraverseControl::ContinueWithScope(s) => Some(s),
TraverseControl::SkipBranch => {
return None;
}
TraverseControl::Return(ret) => {
return Some(ret);
}
};
let state = child_state.as_ref().unwrap_or(state);
match &self.typ {
TypeF::Dyn
@ -1105,12 +1123,14 @@ impl Traverse<Type> for Type {
| TypeF::Var(_)
| TypeF::Enum(_)
| TypeF::Wildcard(_) => None,
TypeF::Flat(rt) => rt.traverse_ref(f),
TypeF::Arrow(t1, t2) => t1.traverse_ref(f).or_else(|| t2.traverse_ref(f)),
TypeF::Flat(rt) => rt.traverse_ref(f, state),
TypeF::Arrow(t1, t2) => t1
.traverse_ref(f, state)
.or_else(|| t2.traverse_ref(f, state)),
TypeF::Forall { body: t, .. }
| TypeF::Dict { type_fields: t, .. }
| TypeF::Array(t) => t.traverse_ref(f),
TypeF::Record(rrows) => rrows.traverse_ref(f),
| TypeF::Array(t) => t.traverse_ref(f, state),
TypeF::Record(rrows) => rrows.traverse_ref(f, state),
}
}
}
@ -1131,17 +1151,24 @@ impl Traverse<RichTerm> for Type {
)
}
fn traverse_ref<U>(&self, f: &mut dyn FnMut(&RichTerm) -> TraverseControl<U>) -> Option<U> {
self.traverse_ref(&mut |ty: &Type| match &ty.typ {
TypeF::Flat(t) => {
if let Some(ret) = t.traverse_ref(f) {
TraverseControl::Return(ret)
} else {
TraverseControl::SkipBranch
fn traverse_ref<S, U>(
&self,
f: &mut dyn FnMut(&RichTerm, &S) -> TraverseControl<S, U>,
state: &S,
) -> Option<U> {
self.traverse_ref(
&mut |ty: &Type, s: &S| match &ty.typ {
TypeF::Flat(t) => {
if let Some(ret) = t.traverse_ref(f, s) {
TraverseControl::Return(ret)
} else {
TraverseControl::SkipBranch
}
}
}
_ => TraverseControl::Continue,
})
_ => TraverseControl::Continue,
},
state,
)
}
}

View File

@ -15,7 +15,7 @@ name = "nls"
path = "src/main.rs"
[features]
default = ["format", "old-completer"]
default = ["format"]
format = ["nickel-lang-core/format"]
old-completer = []

View File

@ -131,6 +131,18 @@ pub struct DefWithPath {
pub metadata: Option<FieldMetadata>,
}
impl DefWithPath {
pub fn completion_item(&self) -> CompletionItem {
CompletionItem {
label: ident_quoted(&self.ident.into()),
detail: self.metadata.as_ref().and_then(metadata_detail),
kind: Some(CompletionItemKind::Property),
documentation: self.metadata.as_ref().and_then(metadata_doc),
..Default::default()
}
}
}
#[cfg(test)]
impl DefWithPath {
pub fn path(&self) -> &[Ident] {
@ -242,6 +254,7 @@ impl<'a> FieldResolver<'a> {
let defs = self.resolve_annot(annot);
defs.chain(self.resolve_term(term)).collect()
}
Term::Type(typ) => self.resolve_type(typ),
_ => Default::default(),
};

View File

@ -82,6 +82,10 @@ pub fn handle_save(server: &mut Server, params: DidChangeTextDocumentParams) ->
let invalid = server.cache.get_rev_imports_transitive(file_id);
for f in &invalid {
server.lin_registry.map.remove(f);
server.lin_registry.position_lookups.remove(f);
server.lin_registry.usage_lookups.remove(f);
server.lin_registry.parent_lookups.remove(f);
server.lin_registry.type_lookups.remove(f);
}
// TODO: make this part more abstracted

View File

@ -11,7 +11,7 @@ use nickel_lang_core::{
cache::SourcePath,
parser::lexer::{self, NormalToken, SpannedToken, Token},
position::RawSpan,
term::RichTerm,
term::{RichTerm, Term},
transform::import_resolution,
};
@ -160,13 +160,13 @@ pub fn parse_path_from_incomplete_input(
.replace_string(SourcePath::Snippet(path), to_parse);
match server.cache.parse_nocache(file_id) {
Ok((rt, _errors)) => {
Ok((rt, _errors)) if !matches!(rt.as_ref(), Term::ParseError(_)) => {
server
.lin_registry
.usage_lookups
.insert(file_id, UsageLookup::new_with_env(&rt, env));
Some(resolve_imports(rt, server))
}
Err(_) => None,
_ => None,
}
}

View File

@ -51,6 +51,7 @@ impl Completed {
/// Returns the closest item to the left (if any) and to the right (if any) of
/// a specified item. The "closeness" metric in this context is just the source
/// position.
#[cfg(feature = "old-completer")]
pub fn get_items_adjacent(
&self,
id: ItemId,
@ -141,6 +142,7 @@ impl Completed {
}
/// Return all the items in the scope of the given linearization item.
#[cfg(feature = "old-completer")]
pub fn get_in_scope<'a>(
&'a self,
LinearizationItem { env, .. }: &'a LinearizationItem<Resolved>,

View File

@ -7,7 +7,7 @@ use nickel_lang_core::{
position::TermPos,
term::{
record::{Field, FieldMetadata},
RichTerm, Term, UnaryOp,
RichTerm, Term, Traverse, TraverseControl, UnaryOp,
},
typ::TypeF,
typecheck::{linearization::Linearizer, reporting::NameReg, UnifType},
@ -30,6 +30,57 @@ pub mod interface;
pub type Environment = nickel_lang_core::environment::Environment<Ident, ItemId>;
#[derive(Clone, Debug)]
pub struct ParentLookup {
table: HashMap<RichTermPtr, RichTerm>,
}
impl ParentLookup {
pub fn new(rt: &RichTerm) -> Self {
let mut table = HashMap::new();
let mut traverse_merge =
|rt: &RichTerm, parent: &Option<RichTerm>| -> TraverseControl<Option<RichTerm>, ()> {
if let Some(parent) = parent {
table.insert(RichTermPtr(rt.clone()), parent.clone());
}
TraverseControl::ContinueWithScope(Some(rt.clone()))
};
rt.traverse_ref(&mut traverse_merge, &None);
ParentLookup { table }
}
pub fn parent(&self, rt: &RichTerm) -> Option<&RichTerm> {
self.table.get(&RichTermPtr(rt.clone()))
}
pub fn parent_chain<'a>(&'a self, rt: &'a RichTerm) -> ParentChainIter<'_> {
ParentChainIter {
table: self,
next: Some(rt),
}
}
}
pub struct ParentChainIter<'a> {
table: &'a ParentLookup,
next: Option<&'a RichTerm>,
}
impl<'a> Iterator for ParentChainIter<'a> {
type Item = &'a RichTerm;
fn next(&mut self) -> Option<Self::Item> {
if let Some(next) = self.next {
self.next = self.table.parent(next);
Some(next)
} else {
None
}
}
}
/// A registry mapping file ids to their corresponding linearization. The registry stores the
/// linearization of every file that has been imported and analyzed, including the main open
/// document.
@ -40,11 +91,17 @@ pub type Environment = nickel_lang_core::environment::Environment<Ident, ItemId>
pub struct LinRegistry {
pub map: HashMap<FileId, Completed>,
// TODO: these are supposed to eventually *replace* part of the linearization, at
// which point we'll rename `LinRegistry` (and probably just have one HashMap<FileId,
// everything>)
// which point we'll rename `LinRegistry` (and probably just have one
// HashMap<FileId, everything>)
//
// Most of these tables do one more lookup than necessary: they look up a
// file id and then they look up a term in an inner table. This is a little
// inefficient for lookups, but it makes it easy to invalidate a whole file
// in one go.
pub position_lookups: HashMap<FileId, PositionLookup>,
pub usage_lookups: HashMap<FileId, UsageLookup>,
pub type_lookups: HashMap<RichTermPtr, Type>,
pub parent_lookups: HashMap<FileId, ParentLookup>,
pub type_lookups: HashMap<FileId, HashMap<RichTermPtr, Type>>,
}
impl LinRegistry {
@ -63,8 +120,8 @@ impl LinRegistry {
self.position_lookups
.insert(file_id, PositionLookup::new(term));
self.usage_lookups.insert(file_id, UsageLookup::new(term));
self.type_lookups.extend(type_lookups);
self.parent_lookups.insert(file_id, ParentLookup::new(term));
self.type_lookups.insert(file_id, type_lookups);
}
/// Look for the linearization corresponding to an item's id, and return the corresponding item
@ -96,7 +153,13 @@ impl LinRegistry {
}
pub fn get_type(&self, rt: &RichTerm) -> Option<&Type> {
self.type_lookups.get(&RichTermPtr(rt.clone()))
let file = rt.pos.as_opt_ref()?.src_id;
self.type_lookups.get(&file)?.get(&RichTermPtr(rt.clone()))
}
pub fn get_parent_chain<'a>(&'a self, rt: &'a RichTerm) -> Option<ParentChainIter<'a>> {
let file = rt.pos.as_opt_ref()?.src_id;
Some(self.parent_lookups.get(&file)?.parent_chain(rt))
}
}

View File

@ -111,7 +111,7 @@ impl PositionLookup {
pub fn new(rt: &RichTerm) -> Self {
let mut all_term_ranges = Vec::new();
let mut idents = Vec::new();
let mut fun = |term: &RichTerm| {
let mut fun = |term: &RichTerm, _state: &()| {
if let TermPos::Original(pos) = &term.pos {
all_term_ranges.push((
Range {
@ -139,10 +139,10 @@ impl PositionLookup {
Term::Match { cases, .. } => idents.extend(cases.keys().cloned()),
_ => {}
}
TraverseControl::<()>::Continue
TraverseControl::<(), ()>::Continue
};
rt.traverse_ref(&mut fun);
rt.traverse_ref(&mut fun, &());
let mut ident_ranges: Vec<_> = idents
.into_iter()

View File

@ -1,3 +1,5 @@
#![allow(unused_imports)]
use crate::{
cache::CacheExt,
field_walker::{FieldHaver, FieldResolver},
@ -17,7 +19,6 @@ use lsp_server::{RequestId, Response, ResponseError};
use lsp_types::{
CompletionItem, CompletionItemKind, CompletionParams, Documentation, MarkupContent, MarkupKind,
};
use nickel_lang_core::cache::InputFormat;
use nickel_lang_core::{
cache,
identifier::{Ident, LocIdent},
@ -28,6 +29,7 @@ use nickel_lang_core::{
},
typ::{RecordRows, RecordRowsIteratorItem, Type, TypeF},
};
use nickel_lang_core::{cache::InputFormat, term::BinaryOp};
use regex::Regex;
use std::collections::HashSet;
use std::ffi::OsString;
@ -43,6 +45,7 @@ use std::path::PathBuf;
// We follow the path by traversing a term, type or contract which represents a record
// and stop when there is nothing else on the path
#[cfg(feature = "old-completer")]
#[derive(Debug)]
struct IdentWithType {
ident: Ident,
@ -50,6 +53,7 @@ struct IdentWithType {
meta: Option<FieldMetadata>,
}
#[cfg(feature = "old-completer")]
impl From<Ident> for IdentWithType {
fn from(ident: Ident) -> Self {
IdentWithType {
@ -60,6 +64,7 @@ impl From<Ident> for IdentWithType {
}
}
#[cfg(feature = "old-completer")]
impl From<&str> for IdentWithType {
fn from(ident: &str) -> Self {
IdentWithType {
@ -70,6 +75,7 @@ impl From<&str> for IdentWithType {
}
}
#[cfg(feature = "old-completer")]
impl IdentWithType {
fn detail(&self) -> String {
self.meta
@ -119,6 +125,7 @@ impl IdentWithType {
}
}
#[cfg(feature = "old-completer")]
/// Completion context: general data structures required by most completion functions.
#[derive(Clone, Copy)]
pub struct ComplCtx<'a> {
@ -126,6 +133,7 @@ pub struct ComplCtx<'a> {
lin_registry: &'a LinRegistry,
}
#[cfg(feature = "old-completer")]
/// Find the record field associated with a particular ID in the linearization
/// using lexical scoping rules.
fn find_fields_from_term_kind(
@ -201,6 +209,7 @@ fn find_fields_from_term_kind(
result.into_iter().chain(contract_result).collect()
}
#[cfg(feature = "old-completer")]
/// Find the record fields associated with an ID in the linearization using
/// its contract information.
fn find_fields_from_contract(
@ -233,6 +242,7 @@ fn find_fields_from_contract(
}
}
#[cfg(feature = "old-completer")]
/// Find record field associated with a field's metadata. This can be gotten from the type or the
/// contracts.
fn find_fields_from_contracts(
@ -249,6 +259,7 @@ fn find_fields_from_contracts(
.collect()
}
#[cfg(feature = "old-completer")]
/// Find the fields that can be found from a type.
fn find_fields_from_type(
ty: &Type,
@ -266,6 +277,7 @@ fn find_fields_from_type(
}
}
#[cfg(feature = "old-completer")]
/// Extract the fields from a given record type.
fn find_fields_from_rrows(
rrows: &RecordRows,
@ -305,6 +317,7 @@ fn find_fields_from_rrows(
}
}
#[cfg(feature = "old-completer")]
fn find_fields_from_field(
field: &Field,
path: &mut Vec<LocIdent>,
@ -313,6 +326,7 @@ fn find_fields_from_field(
find_fields_from_term_with_annot(&field.metadata.annotation, field.value.as_ref(), path, info)
}
#[cfg(feature = "old-completer")]
// TODO: impl a trait FindField to avoid having long names
fn find_fields_from_term_with_annot(
annot: &TypeAnnotation,
@ -329,6 +343,7 @@ fn find_fields_from_term_with_annot(
info_from_metadata
}
#[cfg(feature = "old-completer")]
/// Extract record fields from a record term.
fn find_fields_from_term(
term: &RichTerm,
@ -371,6 +386,7 @@ fn find_fields_from_term(
}
}
#[cfg(feature = "old-completer")]
lazy_static! {
// Unwraps are safe here because we know these are correct regexes. This regexp must be the same
// as the regex for identifiers in the lexer (nickel_lang_core::parser::lexer)
@ -378,6 +394,7 @@ lazy_static! {
static ref RE_SPACE: Regex = Regex::new(r"\s+").unwrap();
}
#[cfg(feature = "old-completer")]
/// Get the string chunks that make up an identifier path.
fn get_identifier_path(text: &str) -> Option<Vec<String>> {
/// Remove quotes from a record fields name (if any) and return a tuple
@ -420,6 +437,7 @@ fn get_identifier_path(text: &str) -> Option<Vec<String>> {
Some(path)
}
#[cfg(feature = "old-completer")]
/// Get the identifiers before `.<text>` for record completion.
fn get_identifiers_before_field(text: &str) -> Option<Vec<String>> {
// Skip `<text>`
@ -445,6 +463,7 @@ fn remove_duplicates(items: &Vec<CompletionItem>) -> Vec<CompletionItem> {
seen
}
#[cfg(feature = "old-completer")]
/// Search the linearization to find the record information associated with a
/// partiular ID, and in the scope of a given linearization item.
fn collect_record_info(
@ -507,6 +526,7 @@ fn collect_record_info(
.unwrap_or_default()
}
#[cfg(feature = "old-completer")]
/// As we traverse a nested record all the way to the topmost record,
/// accumulate all the record metadata and corresponding path.
fn accumulate_record_meta_data<'a>(
@ -546,6 +566,7 @@ fn accumulate_record_meta_data<'a>(
}
}
#[cfg(feature = "old-completer")]
/// Generate possible completion identifiers given a source text, its linearization
/// and the current item the cursor points at.
fn get_completion_identifiers(
@ -682,7 +703,8 @@ fn extract_static_path(mut rt: RichTerm) -> (RichTerm, Vec<Ident>) {
}
}
fn sanitize_term_for_completion(
// Try to interpret `term` as a record path to offer completions for.
fn sanitize_record_path_for_completion(
term: &RichTerm,
cursor: RawPos,
server: &mut Server,
@ -709,16 +731,68 @@ fn sanitize_term_for_completion(
}
}
fn term_based_completion(
term: RichTerm,
server: &Server,
) -> Result<Vec<CompletionItem>, ResponseError> {
fn record_path_completion(term: RichTerm, server: &Server) -> Vec<CompletionItem> {
log::info!("term based completion path: {term:?}");
let (start_term, path) = extract_static_path(term);
let defs = FieldResolver::new(server).resolve_term_path(&start_term, &path);
Ok(defs.iter().flat_map(FieldHaver::completion_items).collect())
defs.iter().flat_map(FieldHaver::completion_items).collect()
}
fn env_completion(rt: &RichTerm, server: &Server) -> Vec<CompletionItem> {
let env = server.lin_registry.get_env(rt).cloned().unwrap_or_default();
let resolver = FieldResolver::new(server);
let mut items: Vec<_> = env
.iter_elems()
.map(|(_, def_with_path)| def_with_path.completion_item())
.collect();
// If the current term is a record, add its fields. (They won't be in the environment,
// because that's the environment *of* the current term. And we don't want to treat
// all possible FieldHavers here, because for example if the current term is a Term::Var
// that references a record, we don't want it.)
if matches!(rt.as_ref(), Term::RecRecord(..)) {
items.extend(
resolver
.resolve_term(rt)
.iter()
.flat_map(FieldHaver::completion_items),
);
}
// Iterate through all ancestors of our term, looking for identifiers that are "in scope"
// because they're in an uncle/aunt/cousin that gets merged into our direct ancestors.
if let Some(parents) = server.lin_registry.get_parent_chain(rt) {
// We're only interested in adding identifiers from terms that are records or
// merges/annotations of records. But actually we can skip the records, because any
// records that are our direct ancestor have already contributed to `env`.
let env_term = |rt: &RichTerm| {
matches!(
rt.as_ref(),
Term::Op2(BinaryOp::Merge(_), _, _) | Term::Annotated(_, _)
)
};
let mut parents = parents.peekable();
while let Some(rt) = parents.next() {
// If a parent and a grandparent were both merges, we can skip the parent
// because the grandparent will have a superset of its fields. This prevents
// quadratic behavior on long chains of merges.
if let Some(gp) = parents.peek() {
if env_term(gp) {
continue;
}
}
if env_term(rt) {
let records = resolver.resolve_term(rt);
items.extend(records.iter().flat_map(FieldHaver::completion_items));
}
}
}
items
}
pub fn handle_completion(
@ -758,11 +832,13 @@ pub fn handle_completion(
let sanitized_term = term
.as_ref()
.and_then(|rt| sanitize_term_for_completion(rt, cursor, server));
.and_then(|rt| sanitize_record_path_for_completion(rt, cursor, server));
let mut completions = match sanitized_term {
Some(sanitized) => term_based_completion(sanitized, server)?,
None => Vec::new(),
#[allow(unused_mut)] // needs to be mut with feature = old-completer
let mut completions = match (sanitized_term, term) {
(Some(sanitized), _) => record_path_completion(sanitized, server),
(_, Some(term)) => env_completion(&term, server),
(None, None) => Vec::new(),
};
log::info!("term-based completion provided {completions:?}");
@ -866,6 +942,7 @@ fn handle_import_completion(
Ok(completions)
}
#[cfg(feature = "old-completer")]
#[cfg(test)]
mod tests {
use super::*;

View File

@ -6,19 +6,19 @@ use nickel_lang_core::{
term::{RichTerm, SharedTerm, Term},
};
// A term that uses source position and a pointer to Term to implement Eq and Hash.
// A term that uses a pointer to Term to implement Eq and Hash.
#[derive(Clone, Debug)]
pub struct RichTermPtr(pub RichTerm);
impl PartialEq for RichTermPtr {
fn eq(&self, other: &Self) -> bool {
self.0.pos == other.0.pos && SharedTerm::ptr_eq(&self.0.term, &other.0.term)
SharedTerm::ptr_eq(&self.0.term, &other.0.term)
}
}
impl Hash for RichTermPtr {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(self.0.pos, self.0.term.as_ref() as *const Term).hash(state)
(self.0.term.as_ref() as *const Term).hash(state)
}
}

View File

@ -126,84 +126,81 @@ impl UsageLookup {
}
fn fill(&mut self, rt: &RichTerm, env: &Environment) {
rt.traverse_ref(&mut |term: &RichTerm| {
if let Some(span) = term.pos.as_opt_ref() {
self.def_table.insert(*span, env.clone());
}
match term.term.as_ref() {
Term::Fun(id, body) => {
let mut new_env = env.clone();
new_env.def_noval(*id, None);
self.fill(body, &new_env);
TraverseControl::SkipBranch
rt.traverse_ref(
&mut |term: &RichTerm, env: &Environment| {
if let Some(span) = term.pos.as_opt_ref() {
self.def_table.insert(*span, env.clone());
}
Term::FunPattern(maybe_id, pat, body) => {
let mut new_env = env.clone();
if let Some(id) = maybe_id {
match term.term.as_ref() {
Term::Fun(id, _body) => {
let mut new_env = env.clone();
new_env.def_noval(*id, None);
TraverseControl::ContinueWithScope(new_env)
}
for m in &pat.matches {
for (_path, id, field) in m.to_flattened_bindings() {
new_env.def_noval(id, Some(field.metadata));
Term::FunPattern(maybe_id, pat, _body) => {
let mut new_env = env.clone();
if let Some(id) = maybe_id {
new_env.def_noval(*id, None);
}
for m in &pat.matches {
for (_path, id, field) in m.to_flattened_bindings() {
new_env.def_noval(id, Some(field.metadata));
}
}
TraverseControl::ContinueWithScope(new_env)
}
self.fill(body, &new_env);
TraverseControl::SkipBranch
}
Term::Let(id, val, body, attrs) => {
let mut new_env = env.clone();
new_env.def(*id, Some(val.clone()), None);
self.fill(val, if attrs.rec { &new_env } else { env });
self.fill(body, &new_env);
TraverseControl::SkipBranch
}
Term::LetPattern(maybe_id, pat, val, body) => {
let mut new_env = env.clone();
if let Some(id) = maybe_id {
Term::Let(id, val, body, attrs) => {
let mut new_env = env.clone();
new_env.def(*id, Some(val.clone()), None);
}
for m in &pat.matches {
for (path, id, field) in m.to_flattened_bindings() {
let path = path.iter().map(|i| i.ident()).rev().collect();
let term = TermAtPath {
term: val.clone(),
path,
};
new_env.def(id, Some(term), Some(field.metadata));
self.fill(val, if attrs.rec { &new_env } else { env });
self.fill(body, &new_env);
TraverseControl::SkipBranch
}
Term::LetPattern(maybe_id, pat, val, _body) => {
let mut new_env = env.clone();
if let Some(id) = maybe_id {
new_env.def(*id, Some(val.clone()), None);
}
}
self.fill(body, &new_env);
TraverseControl::SkipBranch
}
Term::RecRecord(data, _interp_fields, _deps) => {
let mut new_env = env.clone();
// Records are recursive and the order of fields is unimportant, so define
// all the fields in the environment and then recurse into their values.
for (id, field) in &data.fields {
new_env.def(*id, field.value.clone(), Some(field.metadata.clone()));
for m in &pat.matches {
for (path, id, field) in m.to_flattened_bindings() {
let path = path.iter().map(|i| i.ident()).rev().collect();
let term = TermAtPath {
term: val.clone(),
path,
};
new_env.def(id, Some(term), Some(field.metadata));
}
}
TraverseControl::ContinueWithScope(new_env)
}
Term::RecRecord(data, _interp_fields, _deps) => {
let mut new_env = env.clone();
for val in data.fields.values().filter_map(|fld| fld.value.as_ref()) {
self.fill(val, &new_env);
// Records are recursive and the order of fields is unimportant, so define
// all the fields in the environment and then recurse into their values.
for (id, field) in &data.fields {
new_env.def(*id, field.value.clone(), Some(field.metadata.clone()));
}
TraverseControl::ContinueWithScope(new_env)
}
TraverseControl::SkipBranch
}
Term::Var(id) => {
let id = LocIdent::from(*id);
if let Some(def) = env.get(&id.ident) {
self.usage_table.entry(def.ident).or_default().push(id);
Term::Var(id) => {
let id = LocIdent::from(*id);
if let Some(def) = env.get(&id.ident) {
self.usage_table.entry(def.ident).or_default().push(id);
}
TraverseControl::Continue
}
TraverseControl::Continue
_ => TraverseControl::<_, ()>::Continue,
}
_ => TraverseControl::<()>::Continue,
}
});
},
env,
);
}
}

View File

@ -23,23 +23,45 @@ in
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 13, character = 18 }
###
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 14, character = 22 }
###
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 15, character = 26 }
###
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 16, character = 18 }
###
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 17, character = 22 }
###
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 18, character = 22 }
###
### # Env completions
###
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 1, character = 3 }
###
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 3, character = 5 }
###
### [[request]]
### type = "Completion"
### textDocument.uri = "file:///input.ncl"
### position = { line = 5, character = 6 }

View File

@ -11,10 +11,10 @@ expression: output
[foo, really, verified, version]
["has a space", lalala]
[falala]
[config, field]
[field]
[config, subfield]
[config, f, foo]
[config, field]
[config, field]
[field]
[subfield]
[foo]
[field]
[field]

View File

@ -2,10 +2,13 @@
source: lsp/nls/tests/main.rs
expression: output
---
[extra, foo]
[bar, more]
[baz, most]
[bar, more]
[baz, most]
[baz, most]
[extra, foo, inner, innermost, outer]
[bar, inner, innermost, more, outer]
[baz, inner, innermost, most, outer]
[bar, inner, innermost, more, outer]
[baz, inner, innermost, most, outer]
[baz, inner, innermost, most, outer]
[bar, extra, foo, inner, innermost, more, outer]
[bar, baz, extra, foo, inner, innermost, more, most, outer]