From 2d7192e3906e89c04dd599ec7b8d1fdf7099e201 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 9 Sep 2021 06:54:27 +1200 Subject: [PATCH 01/90] Add parser README, some parser fixups --- TODO.md | 1 + crates/nu-cli/src/errors.rs | 24 +++++++++ crates/nu-parser/README.md | 99 ++++++++++++++++++++++++++++++++++ crates/nu-parser/src/errors.rs | 2 + crates/nu-parser/src/parser.rs | 44 ++++++++------- crates/nu-protocol/src/ty.rs | 2 + 6 files changed, 153 insertions(+), 19 deletions(-) create mode 100644 crates/nu-parser/README.md diff --git a/TODO.md b/TODO.md index 6ff791e61c..882c710813 100644 --- a/TODO.md +++ b/TODO.md @@ -17,6 +17,7 @@ - [x] Column path - [x] ...rest without calling it rest - [x] Iteration (`each`) over tables +- [ ] Value serialization - [ ] Handling rows with missing columns during a cell path - [ ] Error shortcircuit (stopping on first error) - [ ] ctrl-c support diff --git a/crates/nu-cli/src/errors.rs b/crates/nu-cli/src/errors.rs index a05a6cb238..77ddfd8536 100644 --- a/crates/nu-cli/src/errors.rs +++ b/crates/nu-cli/src/errors.rs @@ -159,6 +159,30 @@ pub fn report_parsing_error( Label::primary(diag_file_id, diag_range).with_message("expected type") ]) } + ParseError::MissingColumns(count, span) => { + let (diag_file_id, diag_range) = convert_span_to_diag(working_set, span)?; + Diagnostic::error() + .with_message("Missing columns") + .with_labels(vec![Label::primary(diag_file_id, diag_range).with_message( + format!( + "expected {} column{}", + count, + if *count == 1 { "" } else { "s" } + ), + )]) + } + ParseError::ExtraColumns(count, span) => { + let (diag_file_id, diag_range) = convert_span_to_diag(working_set, span)?; + Diagnostic::error() + .with_message("Extra columns") + .with_labels(vec![Label::primary(diag_file_id, diag_range).with_message( + format!( + "expected {} column{}", + count, + if *count == 1 { "" } else { "s" } + ), + )]) + } ParseError::TypeMismatch(expected, found, span) => { let (diag_file_id, diag_range) = convert_span_to_diag(working_set, span)?; Diagnostic::error() diff --git a/crates/nu-parser/README.md b/crates/nu-parser/README.md new file mode 100644 index 0000000000..58681f88ba --- /dev/null +++ b/crates/nu-parser/README.md @@ -0,0 +1,99 @@ +# nu-parser, the Nushell parser + +Nushell's parser is a type-directed parser, meaning that the parser will use type information available during parse time to configure the parser. This allows it to handle a broader range of techniques to handle the arguments of a command. + +Nushell's base language is whitespace-separated tokens with the command (Nushell's term for a function) name in the head position: + +``` +head1 arg1 arg2 | head2 +``` + +## Lexing + +The first job of the parser is to a lexical analysis to find where the tokens start and end in the input. This turns the above into: + +``` +, , , , +``` + +At this point, the parser has little to no understanding of the shape of the command or how to parse its arguments. + +## Lite parsing + +As nushell is a language of pipelines, pipes form a key role in both separating commands from each other as well as denoting the flow of information between commands. The lite parse phase, as the name suggests, helps to group the lexed tokens into units. + +The above tokens are converted the following during the lite parse phase: + +``` +Pipeline: + Command #1: + , , + Command #2: + +``` + +## Parsing + +The real magic begins to happen when the parse moves on to the parsing stage. At this point, it traverses the lite parse tree and for each command makes a decision: + +* If the command looks like an internal/external command literal: eg) `foo` or `/usr/bin/ls`, it parses it as an internal or external command +* Otherwise, it parses the command as part of a mathematical expression + +### Types/shapes + +Each command has a shape assigned to each of the arguments in reads in. These shapes help define how the parser will handle the parse. + +For example, if the command is written as: + +```sql +where $x > 10 +``` + +When the parsing happens, the parser will look up the `where` command and find its Signature. The Signature states what flags are allowed and what positional arguments are allowed (both required and optional). Each argument comes with it a Shape that defines how to parse values to get that position. + +In the above example, if the Signature of `where` said that it took three String values, the result would be: + +``` +CallInfo: + Name: `where` + Args: + Expression($x), a String + Expression(>), a String + Expression(10), a String +``` + +Or, the Signature could state that it takes in three positional arguments: a Variable, an Operator, and a Number, which would give: + +``` +CallInfo: + Name: `where` + Args: + Expression($x), a Variable + Expression(>), an Operator + Expression(10), a Number +``` + +Note that in this case, each would be checked at compile time to confirm that the expression has the shape requested. For example, `"foo"` would fail to parse as a Number. + +Finally, some Shapes can consume more than one token. In the above, if the `where` command stated it took in a single required argument, and that the Shape of this argument was a MathExpression, then the parser would treat the remaining tokens as part of the math expression. + +``` +CallInfo: + Name: `where` + Args: + MathExpression: + Op: > + LHS: Expression($x) + RHS: Expression(10) +``` + +When the command runs, it will now be able to evaluate the whole math expression as a single step rather than doing any additional parsing to understand the relationship between the parameters. + +## Making space + +As some Shapes can consume multiple tokens, it's important that the parser allow for multiple Shapes to coexist as peacefully as possible. + +The simplest way it does this is to ensure there is at least one token for each required parameter. If the Signature of the command says that it takes a MathExpression and a Number as two required arguments, then the parser will stop the math parser one token short. This allows the second Shape to consume the final token. + +Another way that the parser makes space is to look for Keyword shapes in the Signature. A Keyword is a word that's special to this command. For example in the `if` command, `else` is a keyword. When it is found in the arguments, the parser will use it as a signpost for where to make space for each Shape. The tokens leading up to the `else` will then feed into the parts of the Signature before the `else`, and the tokens following are consumed by the `else` and the Shapes that follow. + diff --git a/crates/nu-parser/src/errors.rs b/crates/nu-parser/src/errors.rs index bcd388c279..f4c486e0fd 100644 --- a/crates/nu-parser/src/errors.rs +++ b/crates/nu-parser/src/errors.rs @@ -28,4 +28,6 @@ pub enum ParseError { UnknownState(String, Span), IncompleteParser(Span), RestNeedsName(Span), + ExtraColumns(usize, Span), + MissingColumns(usize, Span), } diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 86271281be..57506b7fd2 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1741,13 +1741,13 @@ pub fn parse_list_expression( pub fn parse_table_expression( working_set: &mut StateWorkingSet, - span: Span, + original_span: Span, ) -> (Expression, Option) { - let bytes = working_set.get_span_contents(span); + let bytes = working_set.get_span_contents(original_span); let mut error = None; - let mut start = span.start; - let mut end = span.end; + let mut start = original_span.start; + let mut end = original_span.end; if bytes.starts_with(b"[") { start += 1; @@ -1787,7 +1787,7 @@ pub fn parse_table_expression( ), 1 => { // List - parse_list_expression(working_set, span, &SyntaxShape::Any) + parse_list_expression(working_set, original_span, &SyntaxShape::Any) } _ => { let mut table_headers = vec![]; @@ -1817,9 +1817,27 @@ pub fn parse_table_expression( error = error.or(err); if let Expression { expr: Expr::List(values), + span, .. } = values { + match values.len().cmp(&table_headers.len()) { + std::cmp::Ordering::Less => { + error = error.or_else(|| { + Some(ParseError::MissingColumns(table_headers.len(), span)) + }) + } + std::cmp::Ordering::Equal => {} + std::cmp::Ordering::Greater => { + error = error.or_else(|| { + Some(ParseError::ExtraColumns( + table_headers.len(), + values[table_headers.len()].span, + )) + }) + } + } + rows.push(values); } } @@ -1828,7 +1846,7 @@ pub fn parse_table_expression( Expression { expr: Expr::Table(table_headers, rows), span, - ty: Type::List(Box::new(Type::Unknown)), + ty: Type::Table, }, error, ) @@ -2052,17 +2070,7 @@ pub fn parse_value( } SyntaxShape::Any => { if bytes.starts_with(b"[") { - let shapes = [SyntaxShape::Table]; - for shape in shapes.iter() { - if let (s, None) = parse_value(working_set, span, shape) { - return (s, None); - } - } - parse_value( - working_set, - span, - &SyntaxShape::List(Box::new(SyntaxShape::Any)), - ) + parse_value(working_set, span, &SyntaxShape::Table) } else { let shapes = [ SyntaxShape::Int, @@ -2071,8 +2079,6 @@ pub fn parse_value( SyntaxShape::Filesize, SyntaxShape::Duration, SyntaxShape::Block, - SyntaxShape::Table, - SyntaxShape::List(Box::new(SyntaxShape::Any)), SyntaxShape::String, ]; for shape in shapes.iter() { diff --git a/crates/nu-protocol/src/ty.rs b/crates/nu-protocol/src/ty.rs index bd441c3409..7e6c2aa5e9 100644 --- a/crates/nu-protocol/src/ty.rs +++ b/crates/nu-protocol/src/ty.rs @@ -16,6 +16,7 @@ pub enum Type { Number, Nothing, Record(Vec, Vec), + Table, ValueStream, Unknown, Error, @@ -34,6 +35,7 @@ impl Display for Type { Type::Int => write!(f, "int"), Type::Range => write!(f, "range"), Type::Record(cols, vals) => write!(f, "record<{}, {:?}>", cols.join(", "), vals), + Type::Table => write!(f, "table"), Type::List(l) => write!(f, "list<{}>", l), Type::Nothing => write!(f, "nothing"), Type::Number => write!(f, "number"), From 4ee1776cebdb9c3bace069edd9b5ff301b222c6e Mon Sep 17 00:00:00 2001 From: JT <547158+jntrnr@users.noreply.github.com> Date: Thu, 9 Sep 2021 20:53:24 +1200 Subject: [PATCH 02/90] Update TODO.md --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index 882c710813..d6ba87f447 100644 --- a/TODO.md +++ b/TODO.md @@ -17,6 +17,7 @@ - [x] Column path - [x] ...rest without calling it rest - [x] Iteration (`each`) over tables +- [ ] Row conditions - [ ] Value serialization - [ ] Handling rows with missing columns during a cell path - [ ] Error shortcircuit (stopping on first error) From 56b3f119c0f86cb70a72b71c1d564b684c26e926 Mon Sep 17 00:00:00 2001 From: JT <547158+jntrnr@users.noreply.github.com> Date: Thu, 9 Sep 2021 21:03:12 +1200 Subject: [PATCH 03/90] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c7e39094b..cec4a5df32 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # Engine-q -Engine-q is a smaller project to reimplement some of the core functionality in Nushell. It's still in an alpha state, and there is still a lot to do: please see TODO.md +Engine-q is an experimental project to replace the core functionality in Nushell (parser, engine, protocol). It's still in an alpha state, and there is still a lot to do: please see TODO.md + +If you'd like to help out, come join us on the [discord](https://discord.gg/NtAbbGn) or propose some work in an issue or PR draft. We're currently looking to begin porting Nushell commands to engine-q. From b821b149871f109ff7faf1ea3e2faa9b80471ee9 Mon Sep 17 00:00:00 2001 From: JT Date: Thu, 9 Sep 2021 21:06:55 +1200 Subject: [PATCH 04/90] Add simple completions support --- src/main.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 60da80eb6f..09b78404c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,14 @@ +use std::{arch::x86_64::_CMP_EQ_OQ, cell::RefCell, rc::Rc}; + use nu_cli::{report_parsing_error, report_shell_error, NuHighlighter}; use nu_command::create_default_context; use nu_engine::eval_block; -use nu_parser::parse; +use nu_parser::{flatten_block, parse}; use nu_protocol::{ engine::{EngineState, EvaluationContext, StateWorkingSet}, Value, }; +use reedline::{Completer, DefaultCompletionActionHandler}; #[cfg(test)] mod tests; @@ -53,6 +56,10 @@ fn main() -> std::io::Result<()> { } else { use reedline::{DefaultPrompt, FileBackedHistory, Reedline, Signal}; + let completer = EQCompleter { + engine_state: engine_state.clone(), + }; + let mut line_editor = Reedline::create()? .with_history(Box::new(FileBackedHistory::with_file( 1000, @@ -60,7 +67,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; @@ -143,3 +153,38 @@ fn main() -> std::io::Result<()> { Ok(()) } } + +struct EQCompleter { + engine_state: Rc>, +} + +impl Completer for EQCompleter { + 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 => { + return vec![( + reedline::Span { + start: flat.0.start - offset, + end: flat.0.end - offset, + }, + "hello".into(), + )] + } + _ => {} + } + } + } + + vec![] + } +} From bb6781a3b10d4bae65bdf8758a6ce1a4f284abbb Mon Sep 17 00:00:00 2001 From: JT Date: Fri, 10 Sep 2021 09:47:20 +1200 Subject: [PATCH 05/90] Add row conditions --- crates/nu-command/src/default_context.rs | 6 +- crates/nu-command/src/if_.rs | 2 +- crates/nu-command/src/lib.rs | 1 + crates/nu-command/src/where_.rs | 92 ++++++++++++++++++++++++ crates/nu-engine/src/eval.rs | 1 + crates/nu-parser/src/flatten.rs | 1 + crates/nu-parser/src/parser.rs | 89 ++++++++++++++++++----- crates/nu-protocol/src/ast/expr.rs | 1 + crates/nu-protocol/src/value/mod.rs | 4 ++ src/main.rs | 4 +- src/tests.rs | 16 +++++ 11 files changed, 195 insertions(+), 22 deletions(-) create mode 100644 crates/nu-command/src/where_.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index fbaaaf537b..c3f41be6f4 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -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> { let engine_state = Rc::new(RefCell::new(EngineState::new())); @@ -33,6 +35,8 @@ pub fn create_default_context() -> Rc> { 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)); diff --git a/crates/nu-command/src/if_.rs b/crates/nu-command/src/if_.rs index bb785c4c2c..2b8df48671 100644 --- a/crates/nu-command/src/if_.rs +++ b/crates/nu-command/src/if_.rs @@ -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 { diff --git a/crates/nu-command/src/lib.rs b/crates/nu-command/src/lib.rs index 3e7d99e7d7..e9c29e17bd 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -10,6 +10,7 @@ mod if_; mod length; mod let_; mod let_env; +mod where_; pub use alias::Alias; pub use benchmark::Benchmark; diff --git a/crates/nu-command/src/where_.rs b/crates/nu-command/src/where_.rs new file mode 100644 index 0000000000..b876277bb0 --- /dev/null +++ b/crates/nu-command/src/where_.rs @@ -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 { + 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 }) + } + } + } + } +} diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index d04c393484..b3da15c87a 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -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 }), diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index 7439da6181..1f92794546 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -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)) } diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 57506b7fd2..0bea2fe27f 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -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, span: Span, ) -> (Expression, Option) { // 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) { - 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, ) -> (Expression, Option) { // 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), } } diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index ac29cd24ba..5a873631e8 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -15,6 +15,7 @@ pub enum Expr { Call(Box), ExternalCall(Vec, Vec>), Operator(Operator), + RowCondition(VarId, Box), BinaryOp(Box, Box, Box), //lhs, op, rhs Subexpression(BlockId), Block(BlockId), diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index d02f1ce2d4..23c1b89a90 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -277,6 +277,10 @@ impl Value { Ok(current) } + + pub fn is_true(&self) -> bool { + matches!(self, Value::Bool { val: true, .. }) + } } impl PartialEq for Value { diff --git a/src/main.rs b/src/main.rs index 09b78404c2..a8015245c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{arch::x86_64::_CMP_EQ_OQ, cell::RefCell, rc::Rc}; +use std::{cell::RefCell, rc::Rc}; use nu_cli::{report_parsing_error, report_shell_error, NuHighlighter}; use nu_command::create_default_context; @@ -164,7 +164,7 @@ impl Completer for EQCompleter { 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 (output, _err) = parse(&mut working_set, Some("completer"), line.as_bytes(), false); let flattened = flatten_block(&working_set, &output); diff --git a/src/tests.rs b/src/tests.rs index d5cb6d3f04..df619f2b91 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -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", + ) +} From f7333ebe588dee1d7ee7d80b1bb057151f3af037 Mon Sep 17 00:00:00 2001 From: JT Date: Fri, 10 Sep 2021 09:47:57 +1200 Subject: [PATCH 06/90] Check box --- TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index d6ba87f447..f0112bfb54 100644 --- a/TODO.md +++ b/TODO.md @@ -17,7 +17,7 @@ - [x] Column path - [x] ...rest without calling it rest - [x] Iteration (`each`) over tables -- [ ] Row conditions +- [x] Row conditions - [ ] Value serialization - [ ] Handling rows with missing columns during a cell path - [ ] Error shortcircuit (stopping on first error) From abda6f148c4e1924b7e477f371a221da0884fdc6 Mon Sep 17 00:00:00 2001 From: JT Date: Fri, 10 Sep 2021 10:09:40 +1200 Subject: [PATCH 07/90] Finish up completions --- TODO.md | 1 + crates/nu-cli/src/completions.rs | 54 +++++++++++++++++++ crates/nu-cli/src/lib.rs | 2 + crates/nu-protocol/src/engine/engine_state.rs | 36 +++++++++++++ src/main.rs | 47 ++-------------- 5 files changed, 97 insertions(+), 43 deletions(-) create mode 100644 crates/nu-cli/src/completions.rs diff --git a/TODO.md b/TODO.md index f0112bfb54..92049f2ac8 100644 --- a/TODO.md +++ b/TODO.md @@ -18,6 +18,7 @@ - [x] ...rest without calling it rest - [x] Iteration (`each`) over tables - [x] Row conditions +- [x] Simple completions - [ ] Value serialization - [ ] Handling rows with missing columns during a cell path - [ ] Error shortcircuit (stopping on first error) diff --git a/crates/nu-cli/src/completions.rs b/crates/nu-cli/src/completions.rs new file mode 100644 index 0000000000..45b9b17bd0 --- /dev/null +++ b/crates/nu-cli/src/completions.rs @@ -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>, +} + +impl NuCompleter { + pub fn new(engine_state: Rc>) -> 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![] + } +} diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index a8c602012b..111a74c961 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -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; diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 80d437a57c..a9c307fc49 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -123,6 +123,24 @@ impl EngineState { None } + pub fn find_commands_by_prefix(&self, name: &[u8]) -> Vec> { + 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> { + 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 { diff --git a/src/main.rs b/src/main.rs index a8015245c9..32e0fb4c6c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,12 @@ -use std::{cell::RefCell, rc::Rc}; - -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::{flatten_block, parse}; +use nu_parser::parse; use nu_protocol::{ engine::{EngineState, EvaluationContext, StateWorkingSet}, Value, }; -use reedline::{Completer, DefaultCompletionActionHandler}; +use reedline::DefaultCompletionActionHandler; #[cfg(test)] mod tests; @@ -56,9 +54,7 @@ fn main() -> std::io::Result<()> { } else { use reedline::{DefaultPrompt, FileBackedHistory, Reedline, Signal}; - let completer = EQCompleter { - engine_state: engine_state.clone(), - }; + let completer = NuCompleter::new(engine_state.clone()); let mut line_editor = Reedline::create()? .with_history(Box::new(FileBackedHistory::with_file( @@ -153,38 +149,3 @@ fn main() -> std::io::Result<()> { Ok(()) } } - -struct EQCompleter { - engine_state: Rc>, -} - -impl Completer for EQCompleter { - 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 => { - return vec![( - reedline::Span { - start: flat.0.start - offset, - end: flat.0.end - offset, - }, - "hello".into(), - )] - } - _ => {} - } - } - } - - vec![] - } -} From 16baf5e16ac967e091b96a25daa1a3c611bb0532 Mon Sep 17 00:00:00 2001 From: JT Date: Fri, 10 Sep 2021 13:06:44 +1200 Subject: [PATCH 08/90] Add a very silly ls --- Cargo.lock | 7 +++++++ crates/nu-command/Cargo.toml | 5 ++++- crates/nu-command/src/default_context.rs | 4 +++- crates/nu-command/src/lib.rs | 2 ++ crates/nu-protocol/src/value/mod.rs | 7 +++++++ 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8948993d4c..d992443a04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "instant" version = "0.1.10" @@ -276,6 +282,7 @@ dependencies = [ name = "nu-command" version = "0.1.0" dependencies = [ + "glob", "nu-engine", "nu-protocol", ] diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 6cc6ae2f63..9ff9bd274f 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -7,4 +7,7 @@ edition = "2018" [dependencies] nu-protocol = { path = "../nu-protocol" } -nu-engine = { path = "../nu-engine" } \ No newline at end of file +nu-engine = { path = "../nu-engine" } + +# Potential dependencies for extras +glob = "0.3.0" \ No newline at end of file diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index c3f41be6f4..ed0b0fe8c3 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -6,7 +6,7 @@ use nu_protocol::{ }; use crate::{ - where_::Where, Alias, Benchmark, BuildString, Def, Do, Each, For, If, Length, Let, LetEnv, + where_::Where, Alias, Benchmark, BuildString, Def, Do, Each, For, If, Length, Let, LetEnv, Ls, }; pub fn create_default_context() -> Rc> { @@ -43,6 +43,8 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(Length)); + working_set.add_decl(Box::new(Ls)); + let sig = Signature::build("exit"); working_set.add_decl(sig.predeclare()); let sig = Signature::build("vars"); diff --git a/crates/nu-command/src/lib.rs b/crates/nu-command/src/lib.rs index e9c29e17bd..a5e036576d 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -10,6 +10,7 @@ mod if_; mod length; mod let_; mod let_env; +mod ls; mod where_; pub use alias::Alias; @@ -24,3 +25,4 @@ pub use if_::If; pub use length::Length; pub use let_::Let; pub use let_env::LetEnv; +pub use ls::Ls; diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 23c1b89a90..c78e106f51 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -278,6 +278,13 @@ impl Value { Ok(current) } + pub fn string(s: &str, span: Span) -> Value { + Value::String { + val: s.into(), + span, + } + } + pub fn is_true(&self) -> bool { matches!(self, Value::Bool { val: true, .. }) } From c1194b3d1e86ecf0ecbfa4696158f4e03857c7d3 Mon Sep 17 00:00:00 2001 From: JT Date: Fri, 10 Sep 2021 13:09:54 +1200 Subject: [PATCH 09/90] Add a very silly ls --- crates/nu-command/src/ls.rs | 93 +++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 crates/nu-command/src/ls.rs diff --git a/crates/nu-command/src/ls.rs b/crates/nu-command/src/ls.rs new file mode 100644 index 0000000000..c8b00a8fb5 --- /dev/null +++ b/crates/nu-command/src/ls.rs @@ -0,0 +1,93 @@ +use nu_engine::eval_expression; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{IntoValueStream, Signature, SyntaxShape, Value}; + +pub struct Ls; + +//NOTE: this is not a real implementation :D. It's just a simple one to test with until we port the real one. +impl Command for Ls { + fn name(&self) -> &str { + "ls" + } + + fn usage(&self) -> &str { + "List the files in a directory." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("ls").optional( + "pattern", + SyntaxShape::GlobPattern, + "the glob pattern to use", + ) + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result { + let pattern = if let Some(expr) = call.positional.get(0) { + let result = eval_expression(context, expr)?; + result.as_string()? + } else { + "*".into() + }; + + let call_span = call.head; + let glob = glob::glob(&pattern).unwrap(); + + Ok(Value::Stream { + stream: glob + .into_iter() + .map(move |x| match x { + Ok(path) => match std::fs::symlink_metadata(&path) { + Ok(metadata) => { + let is_file = metadata.is_file(); + let is_dir = metadata.is_dir(); + let filesize = metadata.len(); + + Value::Record { + cols: vec!["name".into(), "type".into(), "size".into()], + vals: vec![ + Value::String { + val: path.to_string_lossy().to_string(), + span: call_span, + }, + if is_file { + Value::string("file", call_span) + } else if is_dir { + Value::string("dir", call_span) + } else { + Value::Nothing { span: call_span } + }, + Value::Int { + val: filesize as i64, + span: call_span, + }, + ], + span: call_span, + } + } + Err(_) => Value::Record { + cols: vec!["name".into(), "type".into(), "size".into()], + vals: vec![ + Value::String { + val: path.to_string_lossy().to_string(), + span: call_span, + }, + Value::Nothing { span: call_span }, + Value::Nothing { span: call_span }, + ], + span: call_span, + }, + }, + _ => Value::Nothing { span: call_span }, + }) + .into_value_stream(), + span: call_span, + }) + } +} From 26d50ebcd56ba076ee801eeb5b22c3d6e8a36a25 Mon Sep 17 00:00:00 2001 From: JT Date: Fri, 10 Sep 2021 14:27:12 +1200 Subject: [PATCH 10/90] Add a very silly table --- Cargo.lock | 37 + Cargo.toml | 1 + crates/nu-command/Cargo.toml | 1 + crates/nu-command/src/default_context.rs | 3 + crates/nu-command/src/lib.rs | 2 + crates/nu-command/src/table.rs | 125 +++ crates/nu-protocol/src/value/mod.rs | 7 + crates/nu-table/.gitignore | 22 + crates/nu-table/Cargo.toml | 18 + crates/nu-table/src/lib.rs | 5 + crates/nu-table/src/main.rs | 86 ++ crates/nu-table/src/table.rs | 1259 ++++++++++++++++++++++ crates/nu-table/src/wrap.rs | 274 +++++ 13 files changed, 1840 insertions(+) create mode 100644 crates/nu-command/src/table.rs create mode 100644 crates/nu-table/.gitignore create mode 100644 crates/nu-table/Cargo.toml create mode 100644 crates/nu-table/src/lib.rs create mode 100644 crates/nu-table/src/main.rs create mode 100644 crates/nu-table/src/table.rs create mode 100644 crates/nu-table/src/wrap.rs diff --git a/Cargo.lock b/Cargo.lock index d992443a04..d5d1e70c79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -148,6 +157,7 @@ dependencies = [ "nu-engine", "nu-parser", "nu-protocol", + "nu-table", "pretty_assertions", "reedline", "tempfile", @@ -285,6 +295,7 @@ dependencies = [ "glob", "nu-engine", "nu-protocol", + "nu-table", ] [[package]] @@ -310,6 +321,15 @@ dependencies = [ "codespan-reporting", ] +[[package]] +name = "nu-table" +version = "0.36.0" +dependencies = [ + "nu-ansi-term", + "regex", + "unicode-width", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -494,12 +514,29 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "remove_dir_all" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index 225d8c4f8a..ccbaff85e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ nu-command = { path="./crates/nu-command" } nu-engine = { path="./crates/nu-engine" } nu-parser = { path="./crates/nu-parser" } nu-protocol = { path = "./crates/nu-protocol" } +nu-table = { path = "./crates/nu-table" } # mimalloc = { version = "*", default-features = false } diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 9ff9bd274f..574848b3a8 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" [dependencies] nu-protocol = { path = "../nu-protocol" } nu-engine = { path = "../nu-engine" } +nu-table = { path = "../nu-table" } # Potential dependencies for extras glob = "0.3.0" \ No newline at end of file diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index ed0b0fe8c3..f9a61a77b9 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -7,6 +7,7 @@ use nu_protocol::{ use crate::{ where_::Where, Alias, Benchmark, BuildString, Def, Do, Each, For, If, Length, Let, LetEnv, Ls, + Table, }; pub fn create_default_context() -> Rc> { @@ -45,6 +46,8 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(Ls)); + working_set.add_decl(Box::new(Table)); + let sig = Signature::build("exit"); working_set.add_decl(sig.predeclare()); let sig = Signature::build("vars"); diff --git a/crates/nu-command/src/lib.rs b/crates/nu-command/src/lib.rs index a5e036576d..2b7a3cac0a 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -11,6 +11,7 @@ mod length; mod let_; mod let_env; mod ls; +mod table; mod where_; pub use alias::Alias; @@ -26,3 +27,4 @@ pub use length::Length; pub use let_::Let; pub use let_env::LetEnv; pub use ls::Ls; +pub use table::Table; diff --git a/crates/nu-command/src/table.rs b/crates/nu-command/src/table.rs new file mode 100644 index 0000000000..45109cec3f --- /dev/null +++ b/crates/nu-command/src/table.rs @@ -0,0 +1,125 @@ +use std::collections::HashMap; + +use nu_protocol::ast::{Call, PathMember}; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{Signature, Span, Value}; +use nu_table::StyledString; + +pub struct Table; + +//NOTE: this is not a real implementation :D. It's just a simple one to test with until we port the real one. +impl Command for Table { + fn name(&self) -> &str { + "table" + } + + fn usage(&self) -> &str { + "Render the table." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("table") + } + + fn run( + &self, + _context: &EvaluationContext, + call: &Call, + input: Value, + ) -> Result { + match input { + Value::List { vals, .. } => { + let table = convert_to_table(vals); + + if let Some(table) = table { + let result = nu_table::draw_table(&table, 80, &HashMap::new()); + + Ok(Value::String { + val: result, + span: call.head, + }) + } else { + Ok(Value::Nothing { span: call.head }) + } + } + Value::Stream { stream, .. } => { + let table = convert_to_table(stream); + + if let Some(table) = table { + let result = nu_table::draw_table(&table, 80, &HashMap::new()); + + Ok(Value::String { + val: result, + span: call.head, + }) + } else { + Ok(Value::Nothing { span: call.head }) + } + } + x => Ok(x), + } + } +} + +fn convert_to_table(iter: impl IntoIterator) -> Option { + let mut iter = iter.into_iter().peekable(); + + if let Some(first) = iter.peek() { + let mut headers = first.columns(); + headers.insert(0, "#".into()); + + let mut data = vec![]; + + for (row_num, item) in iter.enumerate() { + let mut row = vec![row_num.to_string()]; + + for header in headers.iter().skip(1) { + let result = item.clone().follow_cell_path(&[PathMember::String { + val: header.into(), + span: Span::unknown(), + }]); + + match result { + Ok(value) => row.push(value.into_string()), + Err(_) => row.push(String::new()), + } + } + + data.push(row); + } + + Some(nu_table::Table { + headers: headers + .into_iter() + .map(|x| StyledString { + contents: x, + style: nu_table::TextStyle::default_header(), + }) + .collect(), + data: data + .into_iter() + .map(|x| { + x.into_iter() + .enumerate() + .map(|(col, y)| { + if col == 0 { + StyledString { + contents: y, + style: nu_table::TextStyle::default_header(), + } + } else { + StyledString { + contents: y, + style: nu_table::TextStyle::basic_left(), + } + } + }) + .collect::>() + }) + .collect(), + theme: nu_table::Theme::rounded(), + }) + } else { + None + } +} diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index c78e106f51..094ca2d590 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -288,6 +288,13 @@ impl Value { pub fn is_true(&self) -> bool { matches!(self, Value::Bool { val: true, .. }) } + + pub fn columns(&self) -> Vec { + match self { + Value::Record { cols, .. } => cols.clone(), + _ => vec![], + } + } } impl PartialEq for Value { diff --git a/crates/nu-table/.gitignore b/crates/nu-table/.gitignore new file mode 100644 index 0000000000..4c234e523b --- /dev/null +++ b/crates/nu-table/.gitignore @@ -0,0 +1,22 @@ +/target +/scratch +**/*.rs.bk +history.txt +tests/fixtures/nuplayground +crates/*/target + +# Debian/Ubuntu +debian/.debhelper/ +debian/debhelper-build-stamp +debian/files +debian/nu.substvars +debian/nu/ + +# macOS junk +.DS_Store + +# JetBrains' IDE items +.idea/* + +# VSCode's IDE items +.vscode/* diff --git a/crates/nu-table/Cargo.toml b/crates/nu-table/Cargo.toml new file mode 100644 index 0000000000..e831ec3f16 --- /dev/null +++ b/crates/nu-table/Cargo.toml @@ -0,0 +1,18 @@ +[package] +authors = ["The Nu Project Contributors"] +description = "Nushell table printing" +edition = "2018" +license = "MIT" +name = "nu-table" +version = "0.36.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "table" +path = "src/main.rs" + +[dependencies] +nu-ansi-term = "0.36.0" + +regex = "1.4" +unicode-width = "0.1.8" diff --git a/crates/nu-table/src/lib.rs b/crates/nu-table/src/lib.rs new file mode 100644 index 0000000000..661d7ddde7 --- /dev/null +++ b/crates/nu-table/src/lib.rs @@ -0,0 +1,5 @@ +mod table; +mod wrap; + +pub use table::{draw_table, StyledString, Table, TextStyle, Theme}; +pub use wrap::Alignment; diff --git a/crates/nu-table/src/main.rs b/crates/nu-table/src/main.rs new file mode 100644 index 0000000000..638582a1fa --- /dev/null +++ b/crates/nu-table/src/main.rs @@ -0,0 +1,86 @@ +use nu_table::{draw_table, StyledString, Table, TextStyle, Theme}; +use std::collections::HashMap; + +fn main() { + let args: Vec<_> = std::env::args().collect(); + let mut width = 0; + + if args.len() > 1 { + // Width in terminal characters + width = args[1].parse::().expect("Need a width in columns"); + } + + if width < 4 { + println!("Width must be greater than or equal to 4, setting width to 80"); + width = 80; + } + + // The mocked up table data + let (table_headers, row_data) = make_table_data(); + // The table headers + let headers = vec_of_str_to_vec_of_styledstr(&table_headers, true); + // The table rows + let rows = vec_of_str_to_vec_of_styledstr(&row_data, false); + // The table itself + let table = Table::new(headers, vec![rows; 3], Theme::rounded()); + // FIXME: Config isn't available from here so just put these here to compile + let color_hm: HashMap = HashMap::new(); + // Capture the table as a string + let output_table = draw_table(&table, width, &color_hm); + // Draw the table + println!("{}", output_table) +} + +fn make_table_data() -> (Vec<&'static str>, Vec<&'static str>) { + let table_headers = vec![ + "category", + "description", + "emoji", + "ios_version", + "unicode_version", + "aliases", + "tags", + "category2", + "description2", + "emoji2", + "ios_version2", + "unicode_version2", + "aliases2", + "tags2", + ]; + + let row_data = vec![ + "Smileys & Emotion", + "grinning face", + "😀", + "6", + "6.1", + "grinning", + "smile", + "Smileys & Emotion", + "grinning face", + "😀", + "6", + "6.1", + "grinning", + "smile", + ]; + + (table_headers, row_data) +} + +fn vec_of_str_to_vec_of_styledstr(data: &[&str], is_header: bool) -> Vec { + let mut v = vec![]; + + for x in data { + if is_header { + v.push(StyledString::new( + String::from(*x), + TextStyle::default_header(), + )) + } else { + v.push(StyledString::new(String::from(*x), TextStyle::basic_left())) + } + } + v +} diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs new file mode 100644 index 0000000000..817c59ef99 --- /dev/null +++ b/crates/nu-table/src/table.rs @@ -0,0 +1,1259 @@ +use crate::wrap::{column_width, split_sublines, wrap, Alignment, Subline, WrappedCell}; +use nu_ansi_term::{Color, Style}; +use std::collections::HashMap; +use std::fmt::Write; + +enum SeparatorPosition { + Top, + Middle, + Bottom, +} + +#[derive(Debug)] +pub struct Table { + pub headers: Vec, + pub data: Vec>, + pub theme: Theme, +} + +#[derive(Debug, Clone)] +pub struct StyledString { + pub contents: String, + pub style: TextStyle, +} + +impl StyledString { + pub fn new(contents: String, style: TextStyle) -> StyledString { + StyledString { contents, style } + } + + pub fn set_style(&mut self, style: TextStyle) { + self.style = style; + } +} + +#[derive(Debug, Clone, Copy)] +pub struct TextStyle { + pub alignment: Alignment, + pub color_style: Option