diff --git a/Cargo.lock b/Cargo.lock index 8948993d4c..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", @@ -164,6 +174,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,8 +292,10 @@ dependencies = [ name = "nu-command" version = "0.1.0" dependencies = [ + "glob", "nu-engine", "nu-protocol", + "nu-table", ] [[package]] @@ -303,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" @@ -487,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/TODO.md b/TODO.md index d6ba87f447..92049f2ac8 100644 --- a/TODO.md +++ b/TODO.md @@ -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) 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-command/Cargo.toml b/crates/nu-command/Cargo.toml index 6cc6ae2f63..574848b3a8 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -7,4 +7,8 @@ edition = "2018" [dependencies] nu-protocol = { path = "../nu-protocol" } -nu-engine = { path = "../nu-engine" } \ No newline at end of file +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 fbaaaf537b..f9a61a77b9 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -5,7 +5,10 @@ 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, Ls, + Table, +}; pub fn create_default_context() -> Rc> { let engine_state = Rc::new(RefCell::new(EngineState::new())); @@ -33,12 +36,18 @@ 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)); working_set.add_decl(Box::new(Length)); + 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/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..2b7a3cac0a 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -10,6 +10,9 @@ mod if_; mod length; mod let_; mod let_env; +mod ls; +mod table; +mod where_; pub use alias::Alias; pub use benchmark::Benchmark; @@ -23,3 +26,5 @@ pub use if_::If; 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/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, + }) + } +} 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-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 119f14c3e5..fd8260c2d0 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -871,7 +871,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) } } @@ -942,7 +942,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); } @@ -977,7 +977,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); } @@ -1067,6 +1067,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 @@ -1077,10 +1078,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; @@ -1105,27 +1106,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), @@ -1139,7 +1155,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); @@ -1313,11 +1328,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( @@ -2015,7 +2059,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) { @@ -2162,6 +2206,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 @@ -2220,6 +2265,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); @@ -2250,6 +2299,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); @@ -2276,7 +2329,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/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/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index d02f1ce2d4..094ca2d590 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -277,6 +277,24 @@ 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, .. }) + } + + 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