Merge pull request #37 from nushell/completions

Completions and Row Conditions
This commit is contained in:
JT 2021-09-10 10:14:30 +12:00 committed by GitHub
commit 5edcf3910d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 295 additions and 23 deletions

View File

@ -17,7 +17,8 @@
- [x] Column path
- [x] ...rest without calling it rest
- [x] Iteration (`each`) over tables
- [ ] Row conditions
- [x] Row conditions
- [x] Simple completions
- [ ] Value serialization
- [ ] Handling rows with missing columns during a cell path
- [ ] Error shortcircuit (stopping on first error)

View File

@ -0,0 +1,54 @@
use std::{cell::RefCell, rc::Rc};
use nu_parser::{flatten_block, parse};
use nu_protocol::engine::{EngineState, StateWorkingSet};
use reedline::Completer;
pub struct NuCompleter {
engine_state: Rc<RefCell<EngineState>>,
}
impl NuCompleter {
pub fn new(engine_state: Rc<RefCell<EngineState>>) -> Self {
Self { engine_state }
}
}
impl Completer for NuCompleter {
fn complete(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> {
let engine_state = self.engine_state.borrow();
let mut working_set = StateWorkingSet::new(&*engine_state);
let offset = working_set.next_span_start();
let pos = offset + pos;
let (output, _err) = parse(&mut working_set, Some("completer"), line.as_bytes(), false);
let flattened = flatten_block(&working_set, &output);
for flat in flattened {
if pos >= flat.0.start && pos <= flat.0.end {
match flat.1 {
nu_parser::FlatShape::External | nu_parser::FlatShape::InternalCall => {
let prefix = working_set.get_span_contents(flat.0);
let results = working_set.find_commands_by_prefix(prefix);
return results
.into_iter()
.map(move |x| {
(
reedline::Span {
start: flat.0.start - offset,
end: flat.0.end - offset,
},
String::from_utf8_lossy(&x).to_string(),
)
})
.collect();
}
_ => {}
}
}
}
vec![]
}
}

View File

@ -1,5 +1,7 @@
mod completions;
mod errors;
mod syntax_highlight;
pub use completions::NuCompleter;
pub use errors::{report_parsing_error, report_shell_error};
pub use syntax_highlight::NuHighlighter;

View File

@ -5,7 +5,9 @@ use nu_protocol::{
Signature, SyntaxShape,
};
use crate::{Alias, Benchmark, BuildString, Def, Do, Each, For, If, Length, Let, LetEnv};
use crate::{
where_::Where, Alias, Benchmark, BuildString, Def, Do, Each, For, If, Length, Let, LetEnv,
};
pub fn create_default_context() -> Rc<RefCell<EngineState>> {
let engine_state = Rc::new(RefCell::new(EngineState::new()));
@ -33,6 +35,8 @@ pub fn create_default_context() -> Rc<RefCell<EngineState>> {
working_set.add_decl(Box::new(Each));
working_set.add_decl(Box::new(Where));
working_set.add_decl(Box::new(Do));
working_set.add_decl(Box::new(Benchmark));

View File

@ -11,7 +11,7 @@ impl Command for If {
}
fn usage(&self) -> &str {
"Create a variable and give it a value."
"Conditionally run a block."
}
fn signature(&self) -> nu_protocol::Signature {

View File

@ -10,6 +10,7 @@ mod if_;
mod length;
mod let_;
mod let_env;
mod where_;
pub use alias::Alias;
pub use benchmark::Benchmark;

View File

@ -0,0 +1,92 @@
use nu_engine::eval_expression;
use nu_protocol::ast::{Call, Expr, Expression};
use nu_protocol::engine::{Command, EvaluationContext};
use nu_protocol::{IntoValueStream, ShellError, Signature, SyntaxShape, Value};
pub struct Where;
impl Command for Where {
fn name(&self) -> &str {
"where"
}
fn usage(&self) -> &str {
"Filter values based on a condition."
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("where").required("cond", SyntaxShape::RowCondition, "condition")
}
fn run(
&self,
context: &EvaluationContext,
call: &Call,
input: Value,
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
let cond = call.positional[0].clone();
let context = context.enter_scope();
let (var_id, cond) = match cond {
Expression {
expr: Expr::RowCondition(var_id, expr),
..
} => (var_id, expr),
_ => return Err(ShellError::InternalError("Expected row condition".into())),
};
match input {
Value::Stream { stream, span } => {
let output_stream = stream
.filter(move |value| {
context.add_var(var_id, value.clone());
let result = eval_expression(&context, &cond);
match result {
Ok(result) => result.is_true(),
_ => false,
}
})
.into_value_stream();
Ok(Value::Stream {
stream: output_stream,
span,
})
}
Value::List { vals, span } => {
let output_stream = vals
.into_iter()
.filter(move |value| {
context.add_var(var_id, value.clone());
let result = eval_expression(&context, &cond);
match result {
Ok(result) => result.is_true(),
_ => false,
}
})
.into_value_stream();
Ok(Value::Stream {
stream: output_stream,
span,
})
}
x => {
context.add_var(var_id, x.clone());
let result = eval_expression(&context, &cond)?;
if result.is_true() {
Ok(x)
} else {
Ok(Value::Nothing { span: call.head })
}
}
}
}
}

View File

@ -135,6 +135,7 @@ pub fn eval_expression(
value.follow_cell_path(&column_path.tail)
}
Expr::RowCondition(_, expr) => eval_expression(context, expr),
Expr::Call(call) => eval_call(context, call, Value::nothing()),
Expr::ExternalCall(_, _) => Err(ShellError::ExternalNotSupported(expr.span)),
Expr::Operator(_) => Ok(Value::Nothing { span: expr.span }),

View File

@ -114,6 +114,7 @@ pub fn flatten_expression(
Expr::String(_) => {
vec![(expr.span, FlatShape::String)]
}
Expr::RowCondition(_, expr) => flatten_expression(working_set, expr),
Expr::Subexpression(block_id) => {
flatten_block(working_set, working_set.get_block(*block_id))
}

View File

@ -851,7 +851,7 @@ pub(crate) fn parse_dollar_expr(
} else if let (expr, None) = parse_range(working_set, span) {
(expr, None)
} else {
parse_full_column_path(working_set, span)
parse_full_column_path(working_set, None, span)
}
}
@ -922,7 +922,7 @@ pub fn parse_string_interpolation(
end: b + 1,
};
let (expr, err) = parse_full_column_path(working_set, span);
let (expr, err) = parse_full_column_path(working_set, None, span);
error = error.or(err);
output.push(expr);
}
@ -957,7 +957,7 @@ pub fn parse_string_interpolation(
end,
};
let (expr, err) = parse_full_column_path(working_set, span);
let (expr, err) = parse_full_column_path(working_set, None, span);
error = error.or(err);
output.push(expr);
}
@ -1047,6 +1047,7 @@ pub fn parse_variable_expr(
pub fn parse_full_column_path(
working_set: &mut StateWorkingSet,
implicit_head: Option<VarId>,
span: Span,
) -> (Expression, Option<ParseError>) {
// FIXME: assume for now a paren expr, but needs more
@ -1057,10 +1058,10 @@ pub fn parse_full_column_path(
let (tokens, err) = lex(source, span.start, &[b'\n'], &[b'.']);
error = error.or(err);
let mut tokens = tokens.into_iter();
if let Some(head) = tokens.next() {
let mut tokens = tokens.into_iter().peekable();
if let Some(head) = tokens.peek() {
let bytes = working_set.get_span_contents(head.span);
let head = if bytes.starts_with(b"(") {
let (head, mut expect_dot) = if bytes.starts_with(b"(") {
let mut start = head.span.start;
let mut end = head.span.end;
@ -1085,27 +1086,42 @@ pub fn parse_full_column_path(
let source = working_set.get_span_contents(span);
let (tokens, err) = lex(source, span.start, &[b'\n'], &[]);
let (output, err) = lex(source, span.start, &[b'\n'], &[]);
error = error.or(err);
let (output, err) = lite_parse(&tokens);
let (output, err) = lite_parse(&output);
error = error.or(err);
let (output, err) = parse_block(working_set, &output, true);
error = error.or(err);
let block_id = working_set.add_block(output);
tokens.next();
Expression {
expr: Expr::Subexpression(block_id),
span,
ty: Type::Unknown, // FIXME
}
(
Expression {
expr: Expr::Subexpression(block_id),
span,
ty: Type::Unknown, // FIXME
},
true,
)
} else if bytes.starts_with(b"$") {
let (out, err) = parse_variable_expr(working_set, head.span);
error = error.or(err);
out
tokens.next();
(out, true)
} else if let Some(var_id) = implicit_head {
(
Expression {
expr: Expr::Var(var_id),
span: Span::unknown(),
ty: Type::Unknown,
},
false,
)
} else {
return (
garbage(span),
@ -1119,7 +1135,6 @@ pub fn parse_full_column_path(
let mut tail = vec![];
let mut expect_dot = true;
for path_element in tokens {
let bytes = working_set.get_span_contents(path_element.span);
@ -1293,11 +1308,40 @@ pub fn parse_var_with_opt_type(
)
}
}
pub fn expand_to_cell_path(
working_set: &mut StateWorkingSet,
expression: &mut Expression,
var_id: VarId,
) {
if let Expression {
expr: Expr::String(_),
span,
..
} = expression
{
// Re-parse the string as if it were a cell-path
let (new_expression, _err) = parse_full_column_path(working_set, Some(var_id), *span);
*expression = new_expression;
}
}
pub fn parse_row_condition(
working_set: &mut StateWorkingSet,
spans: &[Span],
) -> (Expression, Option<ParseError>) {
parse_math_expression(working_set, spans)
let var_id = working_set.add_variable(b"$it".to_vec(), Type::Unknown);
let (expression, err) = parse_math_expression(working_set, spans, Some(var_id));
let span = span(spans);
(
Expression {
ty: Type::Bool,
span,
expr: Expr::RowCondition(var_id, Box::new(expression)),
},
err,
)
}
pub fn parse_signature(
@ -1995,7 +2039,7 @@ pub fn parse_value(
if let (expr, None) = parse_range(working_set, span) {
return (expr, None);
} else {
return parse_full_column_path(working_set, span);
return parse_full_column_path(working_set, None, span);
}
} else if bytes.starts_with(b"{") {
if matches!(shape, SyntaxShape::Block) || matches!(shape, SyntaxShape::Any) {
@ -2142,6 +2186,7 @@ pub fn parse_operator(
pub fn parse_math_expression(
working_set: &mut StateWorkingSet,
spans: &[Span],
lhs_row_var_id: Option<VarId>,
) -> (Expression, Option<ParseError>) {
// As the expr_stack grows, we increase the required precedence to grow larger
// If, at any time, the operator we're looking at is the same or lower precedence
@ -2200,6 +2245,10 @@ pub fn parse_math_expression(
.pop()
.expect("internal error: expression stack empty");
if let Some(row_var_id) = lhs_row_var_id {
expand_to_cell_path(working_set, &mut lhs, row_var_id);
}
let (result_ty, err) = math_result_type(working_set, &mut lhs, &mut op, &mut rhs);
error = error.or(err);
@ -2230,6 +2279,10 @@ pub fn parse_math_expression(
.pop()
.expect("internal error: expression stack empty");
if let Some(row_var_id) = lhs_row_var_id {
expand_to_cell_path(working_set, &mut lhs, row_var_id);
}
let (result_ty, err) = math_result_type(working_set, &mut lhs, &mut op, &mut rhs);
error = error.or(err);
@ -2256,7 +2309,7 @@ pub fn parse_expression(
match bytes[0] {
b'0' | b'1' | b'2' | b'3' | b'4' | b'5' | b'6' | b'7' | b'8' | b'9' | b'(' | b'{'
| b'[' | b'$' | b'"' | b'\'' | b'-' => parse_math_expression(working_set, spans),
| b'[' | b'$' | b'"' | b'\'' | b'-' => parse_math_expression(working_set, spans, None),
_ => parse_call(working_set, spans, true),
}
}

View File

@ -15,6 +15,7 @@ pub enum Expr {
Call(Box<Call>),
ExternalCall(Vec<u8>, Vec<Vec<u8>>),
Operator(Operator),
RowCondition(VarId, Box<Expression>),
BinaryOp(Box<Expression>, Box<Expression>, Box<Expression>), //lhs, op, rhs
Subexpression(BlockId),
Block(BlockId),

View File

@ -123,6 +123,24 @@ impl EngineState {
None
}
pub fn find_commands_by_prefix(&self, name: &[u8]) -> Vec<Vec<u8>> {
let mut output = vec![];
for scope in self.scope.iter().rev() {
for decl in &scope.decls {
if decl.0.starts_with(name) {
output.push(decl.0.clone());
}
}
}
output
}
pub fn get_span_contents(&self, span: Span) -> &[u8] {
&self.file_contents[span.start..span.end]
}
pub fn get_var(&self, var_id: VarId) -> &Type {
self.vars
.get(var_id)
@ -496,6 +514,24 @@ impl<'a> StateWorkingSet<'a> {
}
}
pub fn find_commands_by_prefix(&self, name: &[u8]) -> Vec<Vec<u8>> {
let mut output = vec![];
for scope in self.delta.scope.iter().rev() {
for decl in &scope.decls {
if decl.0.starts_with(name) {
output.push(decl.0.clone());
}
}
}
let mut permanent = self.permanent_state.find_commands_by_prefix(name);
output.append(&mut permanent);
output
}
pub fn get_block(&self, block_id: BlockId) -> &Block {
let num_permanent_blocks = self.permanent_state.num_blocks();
if block_id < num_permanent_blocks {

View File

@ -277,6 +277,10 @@ impl Value {
Ok(current)
}
pub fn is_true(&self) -> bool {
matches!(self, Value::Bool { val: true, .. })
}
}
impl PartialEq for Value {

View File

@ -1,4 +1,4 @@
use nu_cli::{report_parsing_error, report_shell_error, NuHighlighter};
use nu_cli::{report_parsing_error, report_shell_error, NuCompleter, NuHighlighter};
use nu_command::create_default_context;
use nu_engine::eval_block;
use nu_parser::parse;
@ -6,6 +6,7 @@ use nu_protocol::{
engine::{EngineState, EvaluationContext, StateWorkingSet},
Value,
};
use reedline::DefaultCompletionActionHandler;
#[cfg(test)]
mod tests;
@ -53,6 +54,8 @@ fn main() -> std::io::Result<()> {
} else {
use reedline::{DefaultPrompt, FileBackedHistory, Reedline, Signal};
let completer = NuCompleter::new(engine_state.clone());
let mut line_editor = Reedline::create()?
.with_history(Box::new(FileBackedHistory::with_file(
1000,
@ -60,7 +63,10 @@ fn main() -> std::io::Result<()> {
)?))?
.with_highlighter(Box::new(NuHighlighter {
engine_state: engine_state.clone(),
}));
}))
.with_completion_action_handler(Box::new(
DefaultCompletionActionHandler::default().with_completer(Box::new(completer)),
));
let prompt = DefaultPrompt::new(1);
let mut current_line = 1;

View File

@ -292,3 +292,19 @@ fn row_iteration() -> TestResult {
fn record_iteration() -> TestResult {
run_test("([[name, level]; [aa, 100], [bb, 200]] | each { $it | each { |x| if $x.column == \"level\" { $x.value + 100 } else { $x.value } } }).level", "[200, 300]")
}
#[test]
fn row_condition1() -> TestResult {
run_test(
"([[name, size]; [a, 1], [b, 2], [c, 3]] | where size < 3).name",
"[a, b]",
)
}
#[test]
fn row_condition2() -> TestResult {
run_test(
"[[name, size]; [a, 1], [b, 2], [c, 3]] | where $it.size > 2 | length",
"1",
)
}