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:
parent
7937e5a175
commit
df15becc5d
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ name = "nls"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["format", "old-completer"]
|
||||
default = ["format"]
|
||||
format = ["nickel-lang-core/format"]
|
||||
old-completer = []
|
||||
|
||||
|
@ -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(),
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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>,
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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::*;
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user