From aceec44288b0ec6c5873865c23797e87f148625c Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 15:01:21 +0200 Subject: [PATCH 01/29] don't panic when the config is invalid --- src/lib.rs | 3 +-- src/main.rs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index eaa16f4..5d14270 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,8 +24,7 @@ use handler::{handle_key_events, TransitionResult}; use tui::Tui; pub fn explore(config: &Value, input: Value) -> Result { - let config = Config::from_value(config.clone()) - .expect("Could not convert config value to an actual config"); + let config = Config::from_value(config.clone())?; let mut tui = Tui::new( Terminal::new(CrosstermBackend::new(io::stderr()))?, diff --git a/src/main.rs b/src/main.rs index 96e8917..badfd6d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use nu_plugin::{ SimplePluginCommand, }; use nu_plugin_explore::explore; -use nu_protocol::{Example, LabeledError, Record, ShellError, Signature, Span, Type, Value}; +use nu_protocol::{Example, LabeledError, Record, Signature, Span, Type, Value}; struct ExplorePlugin; @@ -75,8 +75,8 @@ impl SimplePluginCommand for Explore { let foreground = engine.enter_foreground()?; let value = explore(config, input.clone()).map_err(|err| { - match err.downcast_ref::() { - Some(shell_error) => LabeledError::from(shell_error.clone()), + match err.downcast_ref::() { + Some(err) => err.clone(), None => LabeledError::new("unexpected internal error").with_label( "could not transform error into ShellError, there was another kind of crash...", call.head, From 9e41b09a2ba5ba8c92672af4a8f32177664678a7 Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 15:09:30 +0200 Subject: [PATCH 02/29] update examples --- src/main.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index badfd6d..b1d4aa7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,12 +39,13 @@ impl SimplePluginCommand for Explore { fn examples(&self) -> Vec { vec![ Example { - example: "open Cargo.toml | explore", + example: "open Cargo.toml | nu_plugin_explore", description: "explore the Cargo.toml file of this project", result: None, }, Example { - example: r#"$nu | explore {show_cell_path: false, layout: "compact"}"#, + example: r#"$env.config.plugins.explore = { show_cell_path: false, layout: "compact" } + $nu | nu_plugin_explore"#, description: "explore `$nu` and set some config options", result: None, }, From 82c479d3447ae4b8568617a3c8a3c56314f1a89d Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 15:11:44 +0200 Subject: [PATCH 03/29] update the README --- README.md | 61 ++++++++++++------------------------------------------- 1 file changed, 13 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 2cb9855..f457af6 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,8 @@ A fast *interactive explorer* tool for *structured data* inspired by [`nu-explor - [*the idea behind an explorer*](#the-idea-behind-an-explorer) - [*why not `nu-explore`?*](#why-not-nu-explore) - [*installation*](#installation) - - [*building from source*](#building-from-source) - - [*installing manually*](#installing-manually) - [*using `nupm install` (recommended)*](#using-nupm-install-recommended) + - [*building from source*](#building-from-source) - [*usage*](#usage) - [*demo*](#demo) - [*configuration*](#configuration) @@ -42,21 +41,16 @@ will come very handy in a day-to-day basis for me at least :) so here we are... LET'S GO :muscle: # installation -## requirements -> **Note** -> this is the development version of `nu_plugin_explore`, thus it does not require Nushell to be -> installed with a stable version. - -let's setup the Nushell dependencies locally, because `nu-plugin` and `nu-protocol` are not release -in version `0.xx.1`, only the stable `0.xx.0` :open_mouth: - -- clone the [Nushell repository][nushell/nushell] somewhere -- setup the dependencies -```shell -make dev-deps +## using `nupm install` (recommended) +- download [nushell/nupm](https://github.com/nushell/nupm) +- load the `nupm` module +```nushell +use /path/to/nupm/ +``` +- run the install process +```nushell +nupm install --path . ``` - -there are three ways to do it: ## building from source - build the plugin ```shell @@ -71,35 +65,6 @@ make register > **Note** > alternatively, you can use directly `make install` -## installing manually -- define the install root, e.g. `$env.CARGO_HOME` or `/some/where/plugins/` -```nushell -let install_root: path = ... -``` -- build and install the plugin -```nushell -cargo install --path . --root $install_root -``` -- register the plugin in [Nushell] -```nushell -nu --commands $"register ($install_root | path join "bin" $name)" -``` -- do not forget to restart [Nushell] - -## using `nupm install` (recommended) -> **Warning** -> this is a very alpha software - -- download [nushell/nupm](https://github.com/nushell/nupm) -- load the `nupm` module -```nushell -use /path/to/nupm/ -``` -- run the install process -```nushell -nupm install --path . -``` - # usage - get some help ```nushell @@ -117,7 +82,7 @@ open Cargo.toml | nu_plugin_explore ## default configuration you can find it in [`default.nuon`](./examples/config/default.nuon). -you can copy-paste it in your `config.nu` and set `$env.explore_config` to it: +you can copy-paste it in your `config.nu` and set `$env.config.plugins.explore` to it: ```nushell $env.config.plugins.explore = { # content of the default config @@ -157,8 +122,8 @@ in order to help, you can have a look at - [ ] when going into a file or URL, open it - [x] give different colors to names and type - [x] show true tables as such -- [ ] get the config from `$env.config` => can parse configuration from CLI -- [ ] add check for the config to make sure it's valid +- [x] get the config from `$env.config` => can parse configuration from CLI +- [x] add check for the config to make sure it's valid - [ ] support for editing cells in INSERT mode - [x] string cells - [ ] other simple cells From d35040c6998560202352f88656b467c804630ca1 Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 15:25:05 +0200 Subject: [PATCH 04/29] implement `Table::to_msg` --- src/nu/value.rs | 23 +++++++++++++++++++++++ src/ui.rs | 23 +---------------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/nu/value.rs b/src/nu/value.rs index 857476b..71dab52 100644 --- a/src/nu/value.rs +++ b/src/nu/value.rs @@ -17,6 +17,29 @@ pub(crate) enum Table { NotAList, } +impl Table { + pub(crate) fn to_msg(&self) -> Option { + match self { + Table::Empty => None, + Table::RowNotARecord(i, t) => Some(format!("row $.{} is not a record: {}", i, t)), + Table::RowIncompatibleLen(i, l, e) => Some(format!( + "row $.{} has incompatible length with first row: expected {} found {}", + i, e, l + )), + Table::RowIncompatibleType(i, k, t, e) => Some(format!( + "cell $.{}.{} has incompatible type with first row: expected {} found {}", + i, k, e, t + )), + Table::RowInvalidKey(i, k, ks) => Some(format!( + "row $.{} does not contain key '{}': list of keys {:?}", + i, k, ks + )), + Table::NotAList => None, + Table::IsValid => None, + } + } +} + pub(crate) fn mutate_value_cell(value: &Value, cell_path: &CellPath, cell: &Value) -> Value { if cell_path.members.is_empty() { return cell.clone(); diff --git a/src/ui.rs b/src/ui.rs index 857ae77..7e2789f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -253,28 +253,7 @@ fn render_data(frame: &mut Frame, app: &mut App, config: &Config) { frame.size().height - 1 }; if !is_a_table { - let msg = match table_type { - crate::nu::value::Table::Empty => None, - crate::nu::value::Table::RowNotARecord(i, t) => { - Some(format!("row $.{} is not a record: {}", i, t)) - } - crate::nu::value::Table::RowIncompatibleLen(i, l, e) => Some(format!( - "row $.{} has incompatible length with first row: expected {} found {}", - i, e, l - )), - crate::nu::value::Table::RowIncompatibleType(i, k, t, e) => Some(format!( - "cell $.{}.{} has incompatible type with first row: expected {} found {}", - i, k, e, t - )), - crate::nu::value::Table::RowInvalidKey(i, k, ks) => Some(format!( - "row $.{} does not contain key '{}': list of keys {:?}", - i, k, ks - )), - crate::nu::value::Table::NotAList => None, - crate::nu::value::Table::IsValid => unreachable!(), - }; - - if let Some(msg) = msg { + if let Some(msg) = table_type.to_msg() { data_frame_height -= 1; frame.render_widget( Paragraph::new(msg).alignment(Alignment::Right).style( From 7e13bc18ddd6eeb4dbc1827ef8cb840e6d759812 Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 15:54:45 +0200 Subject: [PATCH 05/29] have `nu::value::mutate_value_cell` return `Option` this also adds some comments and documentation. --- src/handler.rs | 2 +- src/lib.rs | 1 + src/nu/value.rs | 98 ++++++++++++++++++++++++++++++++----------------- 3 files changed, 67 insertions(+), 34 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 0c62579..27963a5 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -617,7 +617,7 @@ mod tests { if let TransitionResult::Mutate(cell, path) = handle_key_events(key, &mut app, &config, 0).unwrap() { - app.value = crate::nu::value::mutate_value_cell(&app.value, &path, &cell) + app.value = crate::nu::value::mutate_value_cell(&app.value, &path, &cell).unwrap() } assert!( diff --git a/src/lib.rs b/src/lib.rs index 5d14270..feda9d6 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,6 +56,7 @@ pub fn explore(config: &Value, input: Value) -> Result { TransitionResult::Mutate(cell, path) => { app.value = crate::nu::value::mutate_value_cell(&app.value, &path, &cell) + .unwrap() } TransitionResult::Error(error) => { tui.draw(&mut app, &config, Some(&error))?; diff --git a/src/nu/value.rs b/src/nu/value.rs index 71dab52..9073390 100644 --- a/src/nu/value.rs +++ b/src/nu/value.rs @@ -40,9 +40,17 @@ impl Table { } } -pub(crate) fn mutate_value_cell(value: &Value, cell_path: &CellPath, cell: &Value) -> Value { +/// mutate the input `value`, changing the _value_ at `cell_path` into the `cell` argument +/// +/// > **Note** +/// > returns [`None`] if the `cell_path` is not valid in `value`. +pub(crate) fn mutate_value_cell( + value: &Value, + cell_path: &CellPath, + cell: &Value, +) -> Option { if cell_path.members.is_empty() { - return cell.clone(); + return Some(cell.clone()); } if value @@ -50,31 +58,32 @@ pub(crate) fn mutate_value_cell(value: &Value, cell_path: &CellPath, cell: &Valu .follow_cell_path(&cell_path.members, false) .is_err() { - return value.clone(); + return None; } let mut cell_path = cell_path.clone(); - // NOTE: cell_path.members cannot be empty thanks to the guard above + // NOTE: `cell_path.members` cannot be empty because the last branch of the match bellot + // does not call `aux` recursively let first = cell_path.members.first().unwrap(); - match value { + let res = match value { Value::List { vals, .. } => { let id = match first { PathMember::Int { val, .. } => *val, - _ => panic!("first cell path element should be an int"), + _ => unreachable!(), }; cell_path.members.remove(0); let mut vals = vals.clone(); - vals[id] = mutate_value_cell(&vals[id], &cell_path, cell); + vals[id] = mutate_value_cell(&vals[id], &cell_path, cell).unwrap(); Value::list(vals, Span::unknown()) } Value::Record { val: rec, .. } => { let col = match first { PathMember::String { val, .. } => val.clone(), - _ => panic!("first cell path element should be an string"), + _ => unreachable!(), }; cell_path.members.remove(0); @@ -87,7 +96,7 @@ pub(crate) fn mutate_value_cell(value: &Value, cell_path: &CellPath, cell: &Valu .enumerate() .map(|(i, v)| { if i == id { - mutate_value_cell(&v, &cell_path, cell) + mutate_value_cell(&v, &cell_path, cell).unwrap() } else { v } @@ -95,12 +104,17 @@ pub(crate) fn mutate_value_cell(value: &Value, cell_path: &CellPath, cell: &Valu .collect(); Value::record( + // NOTE: this cannot fail because `cols` and `vals` have the same length by + // contruction, they have been constructed by iterating over `rec.columns()` + // and `rec.values()` respectively, both of which have the same length Record::from_raw_cols_vals(cols, vals, Span::unknown(), Span::unknown()).unwrap(), Span::unknown(), ) } _ => cell.clone(), - } + }; + + Some(res) } pub(crate) fn is_table(value: &Value) -> Table { @@ -770,68 +784,67 @@ mod tests { Value::test_string("foo"), vec![], Value::test_string("bar"), - Value::test_string("bar"), + Some(Value::test_string("bar")), ), // list -> simple value ( list.clone(), vec![], Value::test_nothing(), - Value::test_nothing(), + Some(Value::test_nothing()), ), // record -> simple value ( record.clone(), vec![], Value::test_nothing(), - Value::test_nothing(), + Some(Value::test_nothing()), ), // mutate a list element with simple value ( list.clone(), vec![PM::I(0)], Value::test_int(0), - Value::test_list(vec![ + Some(Value::test_list(vec![ Value::test_int(0), Value::test_int(2), Value::test_int(3), - ]), + ])), ), // mutate a list element with complex value ( list.clone(), vec![PM::I(1)], record.clone(), - Value::test_list(vec![Value::test_int(1), record.clone(), Value::test_int(3)]), - ), - // invalid list index -> do not mutate - ( - list.clone(), - vec![PM::I(5)], - Value::test_int(0), - list.clone(), + Some(Value::test_list(vec![ + Value::test_int(1), + record.clone(), + Value::test_int(3), + ])), ), + // invalid list index + (list.clone(), vec![PM::I(5)], Value::test_int(0), None), // mutate a record field with a simple value ( record.clone(), vec![PM::S("a")], Value::test_nothing(), - Value::test_record(record! { + Some(Value::test_record(record! { "a" => Value::test_nothing(), "b" => Value::test_int(2), "c" => Value::test_int(3), - }), + })), ), // mutate a record field with a complex value ( record.clone(), vec![PM::S("c")], list.clone(), - Value::test_record(record! { + Some(Value::test_record(record! { "a" => Value::test_int(1), "b" => Value::test_int(2), "c" => list.clone(), - }), + })), ), // mutate a deeply-nested list element ( @@ -840,9 +853,9 @@ mod tests { ])])]), vec![PM::I(0), PM::I(0), PM::I(0)], Value::test_string("bar"), - Value::test_list(vec![Value::test_list(vec![Value::test_list(vec![ - Value::test_string("bar"), - ])])]), + Some(Value::test_list(vec![Value::test_list(vec![ + Value::test_list(vec![Value::test_string("bar")]), + ])])), ), // mutate a deeply-nested record field ( @@ -855,13 +868,26 @@ mod tests { }), vec![PM::S("a"), PM::S("b"), PM::S("c")], Value::test_string("bar"), - Value::test_record(record! { + Some(Value::test_record(record! { "a" => Value::test_record(record! { "b" => Value::test_record(record! { "c" => Value::test_string("bar"), }), }), + })), + ), + // try to mutate bad cell path + ( + Value::test_record(record! { + "a" => Value::test_record(record! { + "b" => Value::test_record(record! { + "c" => Value::test_string("foo"), + }), + }), }), + vec![PM::S("a"), PM::I(0), PM::S("c")], + Value::test_string("bar"), + None, ), ]; @@ -878,8 +904,14 @@ mod tests { default_value_repr(&value), PM::as_cell_path(&members), default_value_repr(&cell), - default_value_repr(&expected), - default_value_repr(&result) + default_value_repr(&match expected.clone() { + Some(v) => v, + None => Value::test_nothing(), + }), + default_value_repr(&match result.clone() { + Some(v) => v, + None => Value::test_nothing(), + }), ); } } From e7ee973fb4b97001e89b71661abaeed0dcfdc37e Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 16:05:30 +0200 Subject: [PATCH 06/29] add documentation to `nu::value::Table` --- src/nu/value.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/nu/value.rs b/src/nu/value.rs index 9073390..ea6d454 100644 --- a/src/nu/value.rs +++ b/src/nu/value.rs @@ -8,12 +8,21 @@ use nu_protocol::{ #[derive(Debug, PartialEq)] pub(crate) enum Table { + /// value is a list but with no items in it Empty, + /// row at index {0} should be a record but is a {1} RowNotARecord(usize, Type), + /// row at index {0} should have same length as first row, {2}, but has + /// length {1} RowIncompatibleLen(usize, usize, usize), + /// row at index {0} and subkey {1} has type {2} but was expected to have + /// the same type as first row {3} RowIncompatibleType(usize, String, Type, Type), + /// row at index {0} contains an invalid key {1} but expected one of {2} RowInvalidKey(usize, String, Vec), + /// value is a valid table IsValid, + /// valis is not even a list NotAList, } From 2d0cdc4677498d02af4b9090ce47b36bd2b6881f Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 16:12:41 +0200 Subject: [PATCH 07/29] refactor "is table" tests --- src/nu/value.rs | 159 +++++++++++++++++++++--------------------------- 1 file changed, 71 insertions(+), 88 deletions(-) diff --git a/src/nu/value.rs b/src/nu/value.rs index ea6d454..7d88f5f 100644 --- a/src/nu/value.rs +++ b/src/nu/value.rs @@ -927,7 +927,7 @@ mod tests { #[test] fn is_a_table() { - let table = Value::test_list(vec![ + let simple_table = Value::test_list(vec![ Value::test_record(record! { "a" => Value::test_string("foo"), "b" => Value::test_int(1), @@ -937,13 +937,6 @@ mod tests { "b" => Value::test_int(2), }), ]); - assert_eq!( - is_table(&table), - Table::IsValid, - "{} should be a table", - default_value_repr(&table) - ); - let table_with_out_of_order_columns = Value::test_list(vec![ Value::test_record(record! { "b" => Value::test_int(1), @@ -954,13 +947,6 @@ mod tests { "b" => Value::test_int(2), }), ]); - assert_eq!( - is_table(&table_with_out_of_order_columns), - Table::IsValid, - "{} should be a table", - default_value_repr(&table_with_out_of_order_columns) - ); - let table_with_nulls = Value::test_list(vec![ Value::test_record(record! { "a" => Value::test_nothing(), @@ -971,13 +957,6 @@ mod tests { "b" => Value::test_int(2), }), ]); - assert_eq!( - is_table(&table_with_nulls), - Table::IsValid, - "{} should be a table", - default_value_repr(&table_with_nulls) - ); - let table_with_number_colum = Value::test_list(vec![ Value::test_record(record! { "a" => Value::test_string("foo"), @@ -988,85 +967,89 @@ mod tests { "b" => Value::test_float(2.34), }), ]); - assert_eq!( - is_table(&table_with_number_colum), - Table::IsValid, - "{} should be a table", - default_value_repr(&table_with_number_colum) - ); + for table in [ + simple_table, + table_with_out_of_order_columns, + table_with_nulls, + table_with_number_colum, + ] { + assert_eq!( + is_table(&table), + Table::IsValid, + "{} should be a table", + default_value_repr(&table) + ); + } - let not_a_table_missing_field = Value::test_list(vec![ - Value::test_record(record! { - "a" => Value::test_string("a"), - }), - Value::test_record(record! { - "a" => Value::test_string("a"), - "b" => Value::test_int(1), - }), - ]); - assert_eq!( - is_table(¬_a_table_missing_field), + let not_a_table_missing_field = ( + Value::test_list(vec![ + Value::test_record(record! { + "a" => Value::test_string("a"), + }), + Value::test_record(record! { + "a" => Value::test_string("a"), + "b" => Value::test_int(1), + }), + ]), Table::RowIncompatibleLen(1, 2, 1), - "{} should not be a table", - default_value_repr(¬_a_table_missing_field) ); - - let not_a_table_incompatible_types = Value::test_list(vec![ - Value::test_record(record! { - "a" => Value::test_string("a"), - "b" => Value::test_int(1), - }), - Value::test_record(record! { - "a" => Value::test_string("a"), - "b" => Value::test_list(vec![Value::test_int(1)]), - }), - ]); - assert_eq!( - is_table(¬_a_table_incompatible_types), + let not_a_table_incompatible_types = ( + Value::test_list(vec![ + Value::test_record(record! { + "a" => Value::test_string("a"), + "b" => Value::test_int(1), + }), + Value::test_record(record! { + "a" => Value::test_string("a"), + "b" => Value::test_list(vec![Value::test_int(1)]), + }), + ]), Table::RowIncompatibleType( 1, "b".to_string(), Type::List(Box::new(Type::Int)), - Type::Int + Type::Int, ), - "{} should not be a table", - default_value_repr(¬_a_table_incompatible_types) ); - - assert_eq!(is_table(&Value::test_int(0)), Table::NotAList); - - assert_eq!(is_table(&Value::test_list(vec![])), Table::Empty); - - let not_a_table_row_not_record = Value::test_list(vec![ - Value::test_record(record! { - "a" => Value::test_string("a"), - "b" => Value::test_int(1), - }), - Value::test_int(0), - ]); - assert_eq!( - is_table(¬_a_table_row_not_record), + let not_a_table_row_not_record = ( + Value::test_list(vec![ + Value::test_record(record! { + "a" => Value::test_string("a"), + "b" => Value::test_int(1), + }), + Value::test_int(0), + ]), Table::RowNotARecord(1, Type::Int), - "{} should not be a table", - default_value_repr(¬_a_table_row_not_record) ); - - let not_a_table_row_invalid_key = Value::test_list(vec![ - Value::test_record(record! { - "a" => Value::test_string("a"), - "b" => Value::test_int(1), - }), - Value::test_record(record! { - "a" => Value::test_string("a"), - "c" => Value::test_int(2), - }), - ]); - assert_eq!( - is_table(¬_a_table_row_invalid_key), + let not_a_table_row_invalid_key = ( + Value::test_list(vec![ + Value::test_record(record! { + "a" => Value::test_string("a"), + "b" => Value::test_int(1), + }), + Value::test_record(record! { + "a" => Value::test_string("a"), + "c" => Value::test_int(2), + }), + ]), Table::RowInvalidKey(1, "b".into(), vec!["a".into(), "c".into()]), - "{} should not be a table", - default_value_repr(¬_a_table_row_invalid_key) ); + for (not_a_table, expected) in [ + not_a_table_missing_field, + not_a_table_incompatible_types, + not_a_table_row_not_record, + not_a_table_row_invalid_key, + ] { + assert_eq!( + is_table(¬_a_table), + expected, + "{} should not be a table", + default_value_repr(¬_a_table) + ); + } + + assert_eq!(is_table(&Value::test_int(0)), Table::NotAList); + assert_eq!(is_table(&Value::test_list(vec![])), Table::Empty); } #[test] From b60cf62a344ec84f720f2ec51c98290f7aae717e Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 16:22:26 +0200 Subject: [PATCH 08/29] add some notes to `nu::value::transpose` --- src/nu/value.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/nu/value.rs b/src/nu/value.rs index 7d88f5f..42b3e15 100644 --- a/src/nu/value.rs +++ b/src/nu/value.rs @@ -224,6 +224,7 @@ pub(crate) fn is_table(value: &Value) -> Table { /// assert equal (open Cargo.toml | transpose | transpose) (open Cargo.toml) /// } /// ``` +// WARNING: some _unwraps_ haven't been proven to be safe in this function pub(crate) fn transpose(value: &Value) -> Value { if matches!(is_table(value), Table::IsValid) { let value_rows = match value { @@ -231,6 +232,7 @@ pub(crate) fn transpose(value: &Value) -> Value { _ => return value.clone(), }; + // NOTE: because `value` is a valid table, it's first row is guaranteed to be a record let first_row = value_rows[0].as_record().unwrap(); let full_columns = (1..=(first_row.len())) @@ -250,6 +252,8 @@ pub(crate) fn transpose(value: &Value) -> Value { .collect(); return Value::record( + // NOTE: `cols` and `value_rows` have the same length by contruction + // because they have been created by iterating over `value_rows` Record::from_raw_cols_vals(cols, vals, Span::unknown(), Span::unknown()) .unwrap(), Span::unknown(), @@ -263,6 +267,8 @@ pub(crate) fn transpose(value: &Value) -> Value { for i in 0..(first_row.len() - 1) { rows.push(Value::record( + // NOTE: `cols` and `value_rows` have the same length by contruction + // because `cols` has been created by iterating over `value_rows` Record::from_raw_cols_vals( cols.clone(), value_rows @@ -292,6 +298,7 @@ pub(crate) fn transpose(value: &Value) -> Value { } rows.push(Value::record( + // NOTE: `cols` and `vs` have the same length by construction Record::from_raw_cols_vals(cols, vs, Span::unknown(), Span::unknown()).unwrap(), Span::unknown(), )); From cebf2083eaacdfbd455c26630406463195a02bfa Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 16:37:48 +0200 Subject: [PATCH 09/29] add NOTEs to `config::parsing` --- src/config/parsing.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/config/parsing.rs b/src/config/parsing.rs index eeb9e97..a18b34b 100755 --- a/src/config/parsing.rs +++ b/src/config/parsing.rs @@ -266,6 +266,7 @@ pub fn try_key(value: &Value, cell_path: &[&str]) -> Result, La { #[allow(clippy::iter_nth_zero)] return Ok(Some(KeyEvent::new( + // NOTE: this `unwrap` cannot fail because the length of `x` is `5` KeyCode::Char(x.to_string().chars().nth(3).unwrap()), KeyModifiers::CONTROL, ))); @@ -284,6 +285,7 @@ pub fn try_key(value: &Value, cell_path: &[&str]) -> Result, La #[allow(clippy::iter_nth_zero)] Ok(Some(KeyEvent::new( + // NOTE: this `unwrap` cannot fail because the length of `x` is `1` KeyCode::Char(x.to_string().chars().nth(0).unwrap()), KeyModifiers::NONE, ))) From cc69d86750b9a46e49f4b6a9672ba9cb1a7b1712 Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 16:39:43 +0200 Subject: [PATCH 10/29] make `config::parsing::try_color` not public --- src/config/parsing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/parsing.rs b/src/config/parsing.rs index a18b34b..c8a4b8d 100755 --- a/src/config/parsing.rs +++ b/src/config/parsing.rs @@ -131,7 +131,7 @@ pub fn try_modifier(value: &Value, cell_path: &[&str]) -> Result Result, LabeledError> { +fn try_color(value: &Value, cell_path: &[&str]) -> Result, LabeledError> { match follow_cell_path(value, cell_path) { Some(Value::String { val, .. }) => match val.as_str() { "reset" => Ok(Some(Color::Reset)), From 54beb7c9d08666f0af4571b99260655b48be87ec Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 16:43:02 +0200 Subject: [PATCH 11/29] return None from `try_fg_bg_colors` if invalid path --- src/config/parsing.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/config/parsing.rs b/src/config/parsing.rs index c8a4b8d..5eb7f6c 100755 --- a/src/config/parsing.rs +++ b/src/config/parsing.rs @@ -214,7 +214,11 @@ pub fn try_fg_bg_colors( cell_path: &[&str], default: &BgFgColorConfig, ) -> Result, LabeledError> { - let cell = follow_cell_path(value, cell_path).unwrap(); + let cell = match follow_cell_path(value, cell_path) { + Some(c) => c, + None => return Ok(None), + }; + let columns = match &cell { Value::Record { val: rec, .. } => rec.columns().collect::>(), x => return Err(invalid_type(x, cell_path, "record")), From 8a49d94c930cb219d4cf23b971bfe455b0a8eeca Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 16:52:10 +0200 Subject: [PATCH 12/29] add NOTE to `Config::from_value` --- src/config/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/config/mod.rs b/src/config/mod.rs index dd28a4f..db25ccf 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -257,6 +257,10 @@ impl Default for Config { } impl Config { + // NOTE: all the _unwraps_ called on the output of [`parsing::follow_cell_path`] are safe + // because they all follow the same cell path as the parsing branch they are in, e.g. + // `follow_cell_path(&value, &["colors", "line_numbers"])` is only found in the "colors" and + // "line_numbers" branch of the parsing. pub fn from_value(value: Value) -> Result { let mut config = Config::default(); From 05db92d4c0d2a8c034b5a15299f262bb9f0f34a0 Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 16:52:19 +0200 Subject: [PATCH 13/29] refactor imports in `config/mod.rs` --- src/config/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index db25ccf..8d11570 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -11,12 +11,10 @@ use nu_protocol::{LabeledError, Span, Value}; mod parsing; use parsing::{ - follow_cell_path, invalid_field, invalid_type, try_bool, try_fg_bg_colors, try_key, try_layout, - try_modifier, try_string, + follow_cell_path, invalid_field, invalid_type, positive_integer, try_bool, try_fg_bg_colors, + try_int, try_key, try_layout, try_modifier, try_string, }; -use self::parsing::{positive_integer, try_int}; - /// the configuration for the status bar colors in all [`crate::app::Mode`]s #[derive(Clone, PartialEq, Debug)] pub struct StatusBarColorConfig { From b30fde6730b4180b442f0538b1416baefa83a7f1 Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 16 Apr 2024 16:56:11 +0200 Subject: [PATCH 14/29] move `repr_key` from `config` to `handler` --- src/config/mod.rs | 26 ++------------------------ src/handler.rs | 28 +++++++++++++++++++++++++--- src/ui.rs | 7 +++++-- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 8d11570..0f8703b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -750,37 +750,15 @@ impl Config { } } -/// represent a [`KeyEvent`] as a simple string -pub fn repr_key(key: &KeyEvent) -> String { - let code = match key.code { - KeyCode::Char(c) => c.to_string(), - KeyCode::Left => char::from_u32(0x2190).unwrap().into(), - KeyCode::Up => char::from_u32(0x2191).unwrap().into(), - KeyCode::Right => char::from_u32(0x2192).unwrap().into(), - KeyCode::Down => char::from_u32(0x2193).unwrap().into(), - KeyCode::Esc => "".into(), - KeyCode::Enter => char::from_u32(0x23ce).unwrap().into(), - KeyCode::Backspace => char::from_u32(0x232b).unwrap().into(), - KeyCode::Delete => char::from_u32(0x2326).unwrap().into(), - _ => "??".into(), - }; - - match key.modifiers { - KeyModifiers::NONE => code, - KeyModifiers::CONTROL => format!("", code), - _ => "??".into(), - } -} - // TODO: add proper assert error messages #[cfg(test)] mod tests { use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use nu_protocol::{record, Record, Value}; - use crate::nu::value::from_nuon; + use crate::{handler::repr_key, nu::value::from_nuon}; - use super::{repr_key, Config}; + use super::Config; #[test] fn keycode_representation() { diff --git a/src/handler.rs b/src/handler.rs index 27963a5..f202cfd 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,4 +1,4 @@ -use crossterm::event::{KeyCode, KeyEvent}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use nu_protocol::{ ast::{CellPath, PathMember}, @@ -223,6 +223,28 @@ pub fn handle_key_events( Ok(TransitionResult::Continue) } +/// represent a [`KeyEvent`] as a simple string +pub fn repr_key(key: &KeyEvent) -> String { + let code = match key.code { + KeyCode::Char(c) => c.to_string(), + KeyCode::Left => char::from_u32(0x2190).unwrap().into(), + KeyCode::Up => char::from_u32(0x2191).unwrap().into(), + KeyCode::Right => char::from_u32(0x2192).unwrap().into(), + KeyCode::Down => char::from_u32(0x2193).unwrap().into(), + KeyCode::Esc => "".into(), + KeyCode::Enter => char::from_u32(0x23ce).unwrap().into(), + KeyCode::Backspace => char::from_u32(0x232b).unwrap().into(), + KeyCode::Delete => char::from_u32(0x2326).unwrap().into(), + _ => "??".into(), + }; + + match key.modifiers { + KeyModifiers::NONE => code, + KeyModifiers::CONTROL => format!("", code), + _ => "??".into(), + } +} + #[cfg(test)] mod tests { use crossterm::event::KeyEvent; @@ -231,10 +253,10 @@ mod tests { record, Span, Value, }; - use super::{handle_key_events, App, TransitionResult}; + use super::{handle_key_events, repr_key, App, TransitionResult}; use crate::{ app::Mode, - config::{repr_key, Config}, + config::Config, nu::cell_path::{to_path_member_vec, PM}, }; diff --git a/src/ui.rs b/src/ui.rs index 7e2789f..f52c89c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,7 +1,10 @@ //! the module responsible for rendering the TUI -use crate::nu::{strings::SpecialString, value::is_table}; +use crate::{ + config::Layout, + handler::repr_key, + nu::{strings::SpecialString, value::is_table}, +}; -use super::config::{repr_key, Layout}; use super::{App, Config, Mode}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use nu_protocol::ast::PathMember; From bd698d38819ca1e6af9077f97f18d2ff730abb24 Mon Sep 17 00:00:00 2001 From: amtoine Date: Thu, 18 Apr 2024 15:11:12 +0200 Subject: [PATCH 15/29] don't have executable source files --- src/app.rs | 0 src/config/parsing.rs | 0 src/edit.rs | 0 src/lib.rs | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 src/app.rs mode change 100755 => 100644 src/config/parsing.rs mode change 100755 => 100644 src/edit.rs mode change 100755 => 100644 src/lib.rs diff --git a/src/app.rs b/src/app.rs old mode 100755 new mode 100644 diff --git a/src/config/parsing.rs b/src/config/parsing.rs old mode 100755 new mode 100644 diff --git a/src/edit.rs b/src/edit.rs old mode 100755 new mode 100644 diff --git a/src/lib.rs b/src/lib.rs old mode 100755 new mode 100644 From 4fbc6b3d227cd8774ddd4f459ef356fad98844a4 Mon Sep 17 00:00:00 2001 From: amtoine Date: Thu, 18 Apr 2024 15:23:48 +0200 Subject: [PATCH 16/29] refactor TUI modules in `src/tui/` --- src/lib.rs | 3 +-- src/{ => tui}/event.rs | 0 src/{tui.rs => tui/mod.rs} | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) rename src/{ => tui}/event.rs (100%) rename src/{tui.rs => tui/mod.rs} (96%) diff --git a/src/lib.rs b/src/lib.rs index feda9d6..67c339b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,6 @@ mod app; mod config; mod edit; -mod event; mod handler; mod navigation; mod nu; @@ -19,8 +18,8 @@ use nu_protocol::{Span, Value}; use app::{App, Mode}; use config::Config; -use event::{Event, EventHandler}; use handler::{handle_key_events, TransitionResult}; +use tui::event::{Event, EventHandler}; use tui::Tui; pub fn explore(config: &Value, input: Value) -> Result { diff --git a/src/event.rs b/src/tui/event.rs similarity index 100% rename from src/event.rs rename to src/tui/event.rs diff --git a/src/tui.rs b/src/tui/mod.rs similarity index 96% rename from src/tui.rs rename to src/tui/mod.rs index c02726b..8eade31 100644 --- a/src/tui.rs +++ b/src/tui/mod.rs @@ -7,10 +7,10 @@ use ratatui::Terminal; use std::io; use std::panic; -use crate::app::App; -use crate::config::Config; -use crate::event::EventHandler; -use crate::ui; +pub(crate) mod event; + +use crate::{app::App, config::Config, ui}; +use event::EventHandler; /// Representation of a terminal user interface. /// From 060ed63ef8b2a230d0e46459a71b86de29efd003 Mon Sep 17 00:00:00 2001 From: amtoine Date: Thu, 18 Apr 2024 15:30:18 +0200 Subject: [PATCH 17/29] refactor imports in lib --- src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 67c339b..26f093c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,8 +19,10 @@ use nu_protocol::{Span, Value}; use app::{App, Mode}; use config::Config; use handler::{handle_key_events, TransitionResult}; -use tui::event::{Event, EventHandler}; -use tui::Tui; +use tui::{ + event::{Event, EventHandler}, + Tui, +}; pub fn explore(config: &Value, input: Value) -> Result { let config = Config::from_value(config.clone())?; From 70bb8bfd42317c561cbd3d0fab4d3ffa2b2d05c6 Mon Sep 17 00:00:00 2001 From: amtoine Date: Thu, 18 Apr 2024 15:30:25 +0200 Subject: [PATCH 18/29] take `value: &Value` in `Config::from_value` this removes a clone in `explore`. --- src/config/mod.rs | 117 +++++++++++++++++++++++----------------------- src/lib.rs | 2 +- 2 files changed, 59 insertions(+), 60 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 0f8703b..1030d78 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -259,28 +259,28 @@ impl Config { // because they all follow the same cell path as the parsing branch they are in, e.g. // `follow_cell_path(&value, &["colors", "line_numbers"])` is only found in the "colors" and // "line_numbers" branch of the parsing. - pub fn from_value(value: Value) -> Result { + pub fn from_value(value: &Value) -> Result { let mut config = Config::default(); for column in value.columns() { match column.as_str() { "show_cell_path" => { - if let Some(val) = try_bool(&value, &["show_cell_path"])? { + if let Some(val) = try_bool(value, &["show_cell_path"])? { config.show_cell_path = val } } "show_table_header" => { - if let Some(val) = try_bool(&value, &["show_table_header"])? { + if let Some(val) = try_bool(value, &["show_table_header"])? { config.show_table_header = val } } "layout" => { - if let Some(val) = try_layout(&value, &["layout"])? { + if let Some(val) = try_layout(value, &["layout"])? { config.layout = val } } "margin" => { - if let Some(val) = try_int(&value, &["margin"])? { + if let Some(val) = try_int(value, &["margin"])? { if val < 0 { return Err(positive_integer(val, &["margin"], Span::unknown())); } @@ -288,17 +288,17 @@ impl Config { } } "number" => { - if let Some(val) = try_bool(&value, &["number"])? { + if let Some(val) = try_bool(value, &["number"])? { config.number = val } } "relativenumber" => { - if let Some(val) = try_bool(&value, &["relativenumber"])? { + if let Some(val) = try_bool(value, &["relativenumber"])? { config.relativenumber = val } } "colors" => { - let cell = follow_cell_path(&value, &["colors"]).unwrap(); + let cell = follow_cell_path(value, &["colors"]).unwrap(); let columns = match &cell { Value::Record { val: rec, .. } => rec.columns().collect::>(), x => return Err(invalid_type(x, &["colors"], "record")), @@ -307,7 +307,7 @@ impl Config { for column in columns { match column.as_str() { "normal" => { - let cell = follow_cell_path(&value, &["colors", "normal"]).unwrap(); + let cell = follow_cell_path(value, &["colors", "normal"]).unwrap(); let columns = match &cell { Value::Record { val: rec, .. } => { rec.columns().collect::>() @@ -325,7 +325,7 @@ impl Config { match column.as_str() { "name" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "normal", "name"], &config.colors.normal.name, )? { @@ -334,7 +334,7 @@ impl Config { } "data" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "normal", "data"], &config.colors.normal.data, )? { @@ -343,7 +343,7 @@ impl Config { } "shape" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "normal", "shape"], &config.colors.normal.shape, )? { @@ -361,7 +361,7 @@ impl Config { } "selected" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "selected"], &config.colors.selected, )? { @@ -370,21 +370,21 @@ impl Config { } "selected_symbol" => { if let Some(val) = - try_string(&value, &["colors", "selected_symbol"])? + try_string(value, &["colors", "selected_symbol"])? { config.colors.selected_symbol = val } } "selected_modifier" => { if let Some(val) = - try_modifier(&value, &["colors", "selected_modifier"])? + try_modifier(value, &["colors", "selected_modifier"])? { config.colors.selected_modifier = val } } "status_bar" => { let cell = - follow_cell_path(&value, &["colors", "status_bar"]).unwrap(); + follow_cell_path(value, &["colors", "status_bar"]).unwrap(); let columns = match &cell { Value::Record { val: rec, .. } => { rec.columns().collect::>() @@ -402,7 +402,7 @@ impl Config { match column.as_str() { "normal" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "status_bar", "normal"], &config.colors.status_bar.normal, )? { @@ -411,7 +411,7 @@ impl Config { } "insert" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "status_bar", "insert"], &config.colors.status_bar.insert, )? { @@ -420,7 +420,7 @@ impl Config { } "peek" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "status_bar", "peek"], &config.colors.status_bar.peek, )? { @@ -429,7 +429,7 @@ impl Config { } "bottom" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "status_bar", "bottom"], &config.colors.status_bar.bottom, )? { @@ -446,7 +446,7 @@ impl Config { } } "editor" => { - let cell = follow_cell_path(&value, &["colors", "editor"]).unwrap(); + let cell = follow_cell_path(value, &["colors", "editor"]).unwrap(); let columns = match &cell { Value::Record { val: rec, .. } => { rec.columns().collect::>() @@ -464,7 +464,7 @@ impl Config { match column.as_str() { "frame" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "editor", "frame"], &config.colors.editor.frame, )? { @@ -473,7 +473,7 @@ impl Config { } "buffer" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "editor", "buffer"], &config.colors.editor.buffer, )? { @@ -491,7 +491,7 @@ impl Config { } "warning" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "warning"], &config.colors.warning, )? { @@ -500,7 +500,7 @@ impl Config { } "line_numbers" => { let cell = - follow_cell_path(&value, &["colors", "line_numbers"]).unwrap(); + follow_cell_path(value, &["colors", "line_numbers"]).unwrap(); let columns = match &cell { Value::Record { val: rec, .. } => { rec.columns().collect::>() @@ -518,7 +518,7 @@ impl Config { match column.as_str() { "normal" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "line_numbers", "normal"], &config.colors.line_numbers.normal, )? { @@ -527,7 +527,7 @@ impl Config { } "selected" => { if let Some(val) = try_fg_bg_colors( - &value, + value, &["colors", "line_numbers", "selected"], &config.colors.line_numbers.selected, )? { @@ -548,7 +548,7 @@ impl Config { } } "keybindings" => { - let cell = follow_cell_path(&value, &["keybindings"]).unwrap(); + let cell = follow_cell_path(value, &["keybindings"]).unwrap(); let columns = match &cell { Value::Record { val: rec, .. } => rec.columns().collect::>(), x => return Err(invalid_type(x, &["keybindings"], "record")), @@ -557,22 +557,22 @@ impl Config { for column in columns { match column.as_str() { "quit" => { - if let Some(val) = try_key(&value, &["keybindings", "quit"])? { + if let Some(val) = try_key(value, &["keybindings", "quit"])? { config.keybindings.quit = val } } "insert" => { - if let Some(val) = try_key(&value, &["keybindings", "insert"])? { + if let Some(val) = try_key(value, &["keybindings", "insert"])? { config.keybindings.insert = val } } "normal" => { - if let Some(val) = try_key(&value, &["keybindings", "normal"])? { + if let Some(val) = try_key(value, &["keybindings", "normal"])? { config.keybindings.normal = val } } "navigation" => { - let cell = follow_cell_path(&value, &["keybindings", "navigation"]) + let cell = follow_cell_path(value, &["keybindings", "navigation"]) .unwrap(); let columns = match &cell { Value::Record { val: rec, .. } => { @@ -591,7 +591,7 @@ impl Config { match column.as_str() { "up" => { if let Some(val) = try_key( - &value, + value, &["keybindings", "navigation", "up"], )? { config.keybindings.navigation.up = val @@ -599,7 +599,7 @@ impl Config { } "down" => { if let Some(val) = try_key( - &value, + value, &["keybindings", "navigation", "down"], )? { config.keybindings.navigation.down = val @@ -607,7 +607,7 @@ impl Config { } "left" => { if let Some(val) = try_key( - &value, + value, &["keybindings", "navigation", "left"], )? { config.keybindings.navigation.left = val @@ -615,7 +615,7 @@ impl Config { } "right" => { if let Some(val) = try_key( - &value, + value, &["keybindings", "navigation", "right"], )? { config.keybindings.navigation.right = val @@ -623,7 +623,7 @@ impl Config { } "half_page_up" => { if let Some(val) = try_key( - &value, + value, &["keybindings", "navigation", "half_page_up"], )? { config.keybindings.navigation.half_page_up = val @@ -631,7 +631,7 @@ impl Config { } "half_page_down" => { if let Some(val) = try_key( - &value, + value, &["keybindings", "navigation", "half_page_down"], )? { config.keybindings.navigation.half_page_down = val @@ -639,7 +639,7 @@ impl Config { } "goto_top" => { if let Some(val) = try_key( - &value, + value, &["keybindings", "navigation", "goto_top"], )? { config.keybindings.navigation.goto_top = val @@ -647,7 +647,7 @@ impl Config { } "goto_bottom" => { if let Some(val) = try_key( - &value, + value, &["keybindings", "navigation", "goto_bottom"], )? { config.keybindings.navigation.goto_bottom = val @@ -655,7 +655,7 @@ impl Config { } "goto_line" => { if let Some(val) = try_key( - &value, + value, &["keybindings", "navigation", "goto_line"], )? { config.keybindings.navigation.goto_line = val @@ -671,13 +671,13 @@ impl Config { } } "peek" => { - if let Some(val) = try_key(&value, &["keybindings", "peek"])? { + if let Some(val) = try_key(value, &["keybindings", "peek"])? { config.keybindings.peek = val } } "peeking" => { let cell = - follow_cell_path(&value, &["keybindings", "peeking"]).unwrap(); + follow_cell_path(value, &["keybindings", "peeking"]).unwrap(); let columns = match &cell { Value::Record { val: rec, .. } => { rec.columns().collect::>() @@ -695,14 +695,14 @@ impl Config { match column.as_str() { "all" => { if let Some(val) = - try_key(&value, &["keybindings", "peeking", "all"])? + try_key(value, &["keybindings", "peeking", "all"])? { config.keybindings.peeking.all = val } } "cell_path" => { if let Some(val) = try_key( - &value, + value, &["keybindings", "peeking", "cell_path"], )? { config.keybindings.peeking.cell_path = val @@ -710,17 +710,16 @@ impl Config { } "under" => { if let Some(val) = try_key( - &value, + value, &["keybindings", "peeking", "under"], )? { config.keybindings.peeking.under = val } } "view" => { - if let Some(val) = try_key( - &value, - &["keybindings", "peeking", "view"], - )? { + if let Some(val) = + try_key(value, &["keybindings", "peeking", "view"])? + { config.keybindings.peeking.view = val } } @@ -734,7 +733,7 @@ impl Config { } } "transpose" => { - if let Some(val) = try_key(&value, &["keybindings", "tranpose"])? { + if let Some(val) = try_key(value, &["keybindings", "tranpose"])? { config.keybindings.transpose = val } } @@ -777,7 +776,7 @@ mod tests { #[test] fn parse_invalid_config() { assert_eq!( - Config::from_value(Value::test_string("x")), + Config::from_value(&Value::test_string("x")), Ok(Config::default()) ); } @@ -785,7 +784,7 @@ mod tests { #[test] fn parse_empty_config() { assert_eq!( - Config::from_value(Value::test_record(Record::new())), + Config::from_value(&Value::test_record(Record::new())), Ok(Config::default()) ); } @@ -795,7 +794,7 @@ mod tests { let value = Value::test_record(record! { "x" => Value::test_nothing() }); - let result = Config::from_value(value); + let result = Config::from_value(&value); assert!(result.is_err()); let error = result.err().unwrap(); assert!(error.labels[0].text.contains("not a valid config field")); @@ -805,7 +804,7 @@ mod tests { "foo" => Value::test_nothing() }) }); - let result = Config::from_value(value); + let result = Config::from_value(&value); assert!(result.is_err()); let error = result.err().unwrap(); assert!(error.labels[0].text.contains("not a valid config field")); @@ -816,7 +815,7 @@ mod tests { let value = Value::test_record(record! { "show_cell_path" => Value::test_bool(true) }); - assert_eq!(Config::from_value(value), Ok(Config::default())); + assert_eq!(Config::from_value(&value), Ok(Config::default())); let value = Value::test_record(record! { "show_cell_path" => Value::test_bool(false) @@ -825,7 +824,7 @@ mod tests { show_cell_path: false, ..Default::default() }; - assert_eq!(Config::from_value(value), Ok(expected)); + assert_eq!(Config::from_value(&value), Ok(expected)); let value = Value::test_record(record! { "keybindings" => Value::test_record(record!{ @@ -837,7 +836,7 @@ mod tests { let mut expected = Config::default(); expected.keybindings.navigation.up = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE); - assert_eq!(Config::from_value(value), Ok(expected)); + assert_eq!(Config::from_value(&value), Ok(expected)); } #[test] @@ -845,7 +844,7 @@ mod tests { assert_eq!( Config::default(), Config::from_value( - from_nuon(include_str!("../../examples/config/default.nuon")).unwrap() + &from_nuon(include_str!("../../examples/config/default.nuon")).unwrap() ) .unwrap() ) diff --git a/src/lib.rs b/src/lib.rs index 26f093c..6bcbe70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ use tui::{ }; pub fn explore(config: &Value, input: Value) -> Result { - let config = Config::from_value(config.clone())?; + let config = Config::from_value(config)?; let mut tui = Tui::new( Terminal::new(CrosstermBackend::new(io::stderr()))?, From 9c74d5f759e257f4ce0d18c9de591709f2445a21 Mon Sep 17 00:00:00 2001 From: amtoine Date: Thu, 18 Apr 2024 15:36:58 +0200 Subject: [PATCH 19/29] documentation for `app::Mode` --- src/app.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4a30ef1..4dcf119 100644 --- a/src/app.rs +++ b/src/app.rs @@ -9,13 +9,17 @@ use crate::edit::Editor; /// the mode in which the application is #[derive(Clone, Debug, PartialEq)] pub enum Mode { - /// the NORMAL mode is the *navigation* mode, where the user can move around in the data + /// the *navigation* mode, where the user can move around in the data Normal, - /// the INSERT mode lets the user edit cells of the structured data + /// lets the user edit cells of the structured data Insert, - /// the PEEKING mode lets the user *peek* data out of the application, to be reused later + /// lets the user *peek* data out of the application, to be reused later Peeking, + /// indicates that the user has arrived to the very bottom of the nested data, i.e. there is + /// nothing more to the right Bottom, + /// waits for more keys to perform an action, e.g. jumping to a line or motion repetition that + /// both require to enter a number before the actual action Waiting(usize), } From 9637869d3b057de9e6264f963920f6898243dc20 Mon Sep 17 00:00:00 2001 From: amtoine Date: Thu, 18 Apr 2024 15:38:00 +0200 Subject: [PATCH 20/29] derive default for `app::Mode` --- src/app.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4dcf119..e4619a1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,9 +7,10 @@ use nu_protocol::{ use crate::edit::Editor; /// the mode in which the application is -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Default)] pub enum Mode { /// the *navigation* mode, where the user can move around in the data + #[default] Normal, /// lets the user edit cells of the structured data Insert, @@ -23,12 +24,6 @@ pub enum Mode { Waiting(usize), } -impl Default for Mode { - fn default() -> Self { - Self::Normal - } -} - impl std::fmt::Display for Mode { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let repr = match self { From 9d07e543da592a9e378f1814695b13da563b8771 Mon Sep 17 00:00:00 2001 From: amtoine Date: Thu, 18 Apr 2024 15:43:36 +0200 Subject: [PATCH 21/29] derive `Default` for `edit::Editor` this also hides `edit::Editor.buffer` --- src/edit.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/edit.rs b/src/edit.rs index 170d01e..2177d9e 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -10,23 +10,13 @@ use nu_protocol::{Span, Value}; use crate::config::Config; +#[derive(Default)] pub struct Editor { - pub buffer: String, + buffer: String, cursor_position: (usize, usize), width: usize, } -#[allow(clippy::derivable_impls)] -impl Default for Editor { - fn default() -> Self { - Self { - buffer: String::new(), - cursor_position: (0, 0), - width: 0, - } - } -} - impl Editor { /// set the width of the editor /// From d46bba291615e919ab8f632309016563feb6235b Mon Sep 17 00:00:00 2001 From: amtoine Date: Thu, 18 Apr 2024 15:51:26 +0200 Subject: [PATCH 22/29] make `navigation` an implementation of `app::App` --- src/handler.rs | 24 ++-- src/navigation.rs | 292 +++++++++++++++++++++++----------------------- 2 files changed, 160 insertions(+), 156 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index f202cfd..a7a6055 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -8,7 +8,7 @@ use nu_protocol::{ use crate::{ app::{App, Mode}, config::Config, - navigation::{self, Direction}, + navigation::Direction, nu::value::transpose, }; @@ -56,17 +56,17 @@ pub fn handle_key_events( return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.navigation.half_page_down { // TODO: add a margin to the bottom - navigation::go_up_or_down_in_data(app, Direction::Down(half_page)); + app.go_up_or_down_in_data(Direction::Down(half_page)); return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.navigation.half_page_up { // TODO: add a margin to the top - navigation::go_up_or_down_in_data(app, Direction::Up(half_page)); + app.go_up_or_down_in_data(Direction::Up(half_page)); return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.navigation.goto_bottom { - navigation::go_up_or_down_in_data(app, Direction::Bottom); + app.go_up_or_down_in_data(Direction::Bottom); return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.navigation.goto_top { - navigation::go_up_or_down_in_data(app, Direction::Top); + app.go_up_or_down_in_data(Direction::Top); return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.quit { return Ok(TransitionResult::Quit); @@ -79,16 +79,16 @@ pub fn handle_key_events( app.mode = Mode::Peeking; return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.navigation.down { - navigation::go_up_or_down_in_data(app, Direction::Down(1)); + app.go_up_or_down_in_data(Direction::Down(1)); return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.navigation.up { - navigation::go_up_or_down_in_data(app, Direction::Up(1)); + app.go_up_or_down_in_data(Direction::Up(1)); return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.navigation.right { - navigation::go_deeper_in_data(app); + app.go_deeper_in_data(); return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.navigation.left { - navigation::go_back_in_data(app); + app.go_back_in_data(); return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.transpose { let mut path = app.position.clone(); @@ -146,15 +146,15 @@ pub fn handle_key_events( return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.navigation.down { app.mode = Mode::Normal; - navigation::go_up_or_down_in_data(app, Direction::Down(n)); + app.go_up_or_down_in_data(Direction::Down(n)); return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.navigation.up { app.mode = Mode::Normal; - navigation::go_up_or_down_in_data(app, Direction::Up(n)); + app.go_up_or_down_in_data(Direction::Up(n)); return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.navigation.goto_line { app.mode = Mode::Normal; - navigation::go_up_or_down_in_data(app, Direction::At(n.saturating_sub(1))); + app.go_up_or_down_in_data(Direction::At(n.saturating_sub(1))); return Ok(TransitionResult::Continue); } } diff --git a/src/navigation.rs b/src/navigation.rs index 330be38..b90ad59 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -17,164 +17,168 @@ pub enum Direction { At(usize), } -/// go up or down in the data -/// -/// depending on the direction (see [`Direction`]), this function will -/// - early return if the user is already at the bottom => this is to avoid the confusing following -/// situation: you are at the bottom of the data, looking at one item in a list, without this early -/// return, you'd be able to scroll the list without seeing it as a whole... confusing, right? -/// - cycle the list indices or the record column names => the index / column will wrap around -/// -/// > :bulb: **Note** -/// > this function will only modify the last element of the state's *cell path* either by -/// > - not doing anything -/// > - poping the last element to know where we are and then pushing back the new element -pub(super) fn go_up_or_down_in_data(app: &mut App, direction: Direction) { - if app.is_at_bottom() { - return; - } - - let current = app - .position - .members - .pop() - .unwrap_or_else(|| panic!("unexpected error: position is empty")); - - let cell = app - .value - .clone() - .follow_cell_path(&app.position.members, false) - .unwrap_or_else(|_| { - panic!( - "unexpected error when following {:?} in {}", - app.position.members, - app.value - .to_expanded_string(" ", &nu_protocol::Config::default()) - ) - }); - - match cell { - Value::List { vals, .. } => { - let new = match current { - PathMember::Int { - val, - span, - optional, - } => PathMember::Int { - val: if vals.is_empty() { - val - } else { - match direction { - Direction::Up(step) => val.saturating_sub(step).max(0), - Direction::Down(step) => val.saturating_add(step).min(vals.len() - 1), - Direction::Top => 0, - Direction::Bottom => vals.len() - 1, - Direction::At(id) => id.min(vals.len() - 1), - } - }, - span, - optional, - }, - _ => panic!("current should be an integer path member"), - }; - app.position.members.push(new); +impl App { + /// go up or down in the data + /// + /// depending on the direction (see [`Direction`]), this function will + /// - early return if the user is already at the bottom => this is to avoid the confusing following + /// situation: you are at the bottom of the data, looking at one item in a list, without this early + /// return, you'd be able to scroll the list without seeing it as a whole... confusing, right? + /// - cycle the list indices or the record column names => the index / column will wrap around + /// + /// > :bulb: **Note** + /// > this function will only modify the last element of the state's *cell path* either by + /// > - not doing anything + /// > - poping the last element to know where we are and then pushing back the new element + pub(super) fn go_up_or_down_in_data(&mut self, direction: Direction) { + if self.is_at_bottom() { + return; } - Value::Record { val: rec, .. } => { - let new = match current { - PathMember::String { - val, - span, - optional, - } => { - let cols = rec.columns().cloned().collect::>(); - PathMember::String { - val: if cols.is_empty() { - "".into() + let current = self + .position + .members + .pop() + .unwrap_or_else(|| panic!("unexpected error: position is empty")); + + let cell = self + .value + .clone() + .follow_cell_path(&self.position.members, false) + .unwrap_or_else(|_| { + panic!( + "unexpected error when following {:?} in {}", + self.position.members, + self.value + .to_expanded_string(" ", &nu_protocol::Config::default()) + ) + }); + + match cell { + Value::List { vals, .. } => { + let new = match current { + PathMember::Int { + val, + span, + optional, + } => PathMember::Int { + val: if vals.is_empty() { + val } else { - let index = rec.columns().position(|x| x == &val).unwrap(); - let new_index = match direction { - Direction::Up(step) => index.saturating_sub(step).max(0), + match direction { + Direction::Up(step) => val.saturating_sub(step).max(0), Direction::Down(step) => { - index.saturating_add(step).min(cols.len() - 1) + val.saturating_add(step).min(vals.len() - 1) } Direction::Top => 0, - Direction::Bottom => cols.len() - 1, - Direction::At(id) => id.min(cols.len() - 1), - }; - - cols[new_index].clone() + Direction::Bottom => vals.len() - 1, + Direction::At(id) => id.min(vals.len() - 1), + } }, span, optional, + }, + _ => panic!("current should be an integer path member"), + }; + self.position.members.push(new); + } + Value::Record { val: rec, .. } => { + let new = match current { + PathMember::String { + val, + span, + optional, + } => { + let cols = rec.columns().cloned().collect::>(); + + PathMember::String { + val: if cols.is_empty() { + "".into() + } else { + let index = rec.columns().position(|x| x == &val).unwrap(); + let new_index = match direction { + Direction::Up(step) => index.saturating_sub(step).max(0), + Direction::Down(step) => { + index.saturating_add(step).min(cols.len() - 1) + } + Direction::Top => 0, + Direction::Bottom => cols.len() - 1, + Direction::At(id) => id.min(cols.len() - 1), + }; + + cols[new_index].clone() + }, + span, + optional, + } } - } - _ => panic!("current should be an string path member"), - }; - app.position.members.push(new); + _ => panic!("current should be an string path member"), + }; + self.position.members.push(new); + } + _ => {} } - _ => {} } -} -/// go one level deeper in the data -/// -/// > :bulb: **Note** -/// > this function will -/// > - push a new *cell path* member to the state if there is more depth ahead -/// > - mark the state as *at the bottom* if the value at the new depth is of a simple type -pub(super) fn go_deeper_in_data(app: &mut App) { - let cell = app - .value - .clone() - .follow_cell_path(&app.position.members, false) - .unwrap_or_else(|_| { - panic!( - "unexpected error when following {:?} in {}", - app.position.members, - app.value - .to_expanded_string(" ", &nu_protocol::Config::default()) - ) - }); - - match cell { - Value::List { vals, .. } => app.position.members.push(PathMember::Int { - val: 0, - span: Span::unknown(), - optional: vals.is_empty(), - }), - Value::Record { val: rec, .. } => { - let cols = rec.columns().cloned().collect::>(); - - app.position.members.push(PathMember::String { - val: cols.first().unwrap_or(&"".to_string()).into(), + /// go one level deeper in the data + /// + /// > :bulb: **Note** + /// > this function will + /// > - push a new *cell path* member to the state if there is more depth ahead + /// > - mark the state as *at the bottom* if the value at the new depth is of a simple type + pub(super) fn go_deeper_in_data(&mut self) { + let cell = self + .value + .clone() + .follow_cell_path(&self.position.members, false) + .unwrap_or_else(|_| { + panic!( + "unexpected error when following {:?} in {}", + self.position.members, + self.value + .to_expanded_string(" ", &nu_protocol::Config::default()) + ) + }); + + match cell { + Value::List { vals, .. } => self.position.members.push(PathMember::Int { + val: 0, span: Span::unknown(), - optional: cols.is_empty(), - }) + optional: vals.is_empty(), + }), + Value::Record { val: rec, .. } => { + let cols = rec.columns().cloned().collect::>(); + + self.position.members.push(PathMember::String { + val: cols.first().unwrap_or(&"".to_string()).into(), + span: Span::unknown(), + optional: cols.is_empty(), + }) + } + _ => self.hit_bottom(), } - _ => app.hit_bottom(), - } - app.rendering_tops.push(0); -} + self.rendering_tops.push(0); + } -/// pop one level of depth from the data -/// -/// > :bulb: **Note** -/// > - the state is always marked as *not at the bottom* -/// > - the state *cell path* can have it's last member popped if possible -pub(super) fn go_back_in_data(app: &mut App) { - if !app.is_at_bottom() & (app.position.members.len() > 1) { - app.position.members.pop(); + /// pop one level of depth from the data + /// + /// > :bulb: **Note** + /// > - the state is always marked as *not at the bottom* + /// > - the state *cell path* can have it's last member popped if possible + pub(super) fn go_back_in_data(&mut self) { + if !self.is_at_bottom() & (self.position.members.len() > 1) { + self.position.members.pop(); + } + self.mode = Mode::Normal; + self.rendering_tops.pop(); } - app.mode = Mode::Normal; - app.rendering_tops.pop(); } // TODO: add proper assert error messages #[cfg(test)] mod tests { - use super::{go_back_in_data, go_deeper_in_data, go_up_or_down_in_data, Direction}; + use super::Direction; use crate::app::App; use nu_protocol::{ast::PathMember, record, Span, Value}; @@ -219,7 +223,7 @@ mod tests { (Direction::At(2), 2), ]; for (direction, id) in sequence { - go_up_or_down_in_data(&mut app, direction); + app.go_up_or_down_in_data(direction); let expected = vec![test_int_pathmember(id)]; assert_eq!(app.position.members, expected); } @@ -250,7 +254,7 @@ mod tests { (Direction::At(2), "c"), ]; for (direction, id) in sequence { - go_up_or_down_in_data(&mut app, direction); + app.go_up_or_down_in_data(direction); let expected = vec![test_string_pathmember(id)]; assert_eq!(app.position.members, expected); } @@ -266,11 +270,11 @@ mod tests { let mut expected = vec![test_int_pathmember(0)]; assert_eq!(app.position.members, expected); - go_deeper_in_data(&mut app); + app.go_deeper_in_data(); expected.push(test_string_pathmember("a")); assert_eq!(app.position.members, expected); - go_deeper_in_data(&mut app); + app.go_deeper_in_data(); expected.push(test_int_pathmember(0)); assert_eq!(app.position.members, expected); } @@ -282,7 +286,7 @@ mod tests { assert!(!app.is_at_bottom()); - go_deeper_in_data(&mut app); + app.go_deeper_in_data(); assert!(app.is_at_bottom()); } @@ -301,18 +305,18 @@ mod tests { let mut expected = app.position.members.clone(); - go_back_in_data(&mut app); + app.go_back_in_data(); assert_eq!(app.position.members, expected); - go_back_in_data(&mut app); + app.go_back_in_data(); expected.pop(); assert_eq!(app.position.members, expected); - go_back_in_data(&mut app); + app.go_back_in_data(); expected.pop(); assert_eq!(app.position.members, expected); - go_back_in_data(&mut app); + app.go_back_in_data(); assert_eq!(app.position.members, expected); } } From d191f7e23be1b89f5e0bc3ff272ffe5022d13b78 Mon Sep 17 00:00:00 2001 From: amtoine Date: Thu, 18 Apr 2024 16:06:14 +0200 Subject: [PATCH 23/29] refactor "following data in app" --- src/app.rs | 23 ++++++++++++++++++----- src/handler.rs | 18 +++--------------- src/navigation.rs | 30 ++---------------------------- src/ui.rs | 15 ++------------- 4 files changed, 25 insertions(+), 61 deletions(-) diff --git a/src/app.rs b/src/app.rs index e4619a1..3fdb555 100644 --- a/src/app.rs +++ b/src/app.rs @@ -102,11 +102,7 @@ impl App { } pub(super) fn enter_editor(&mut self) -> Result<(), String> { - let value = self - .value - .clone() - .follow_cell_path(&self.position.members, false) - .unwrap(); + let value = self.value_under_cursor(None); if matches!(value, Value::String { .. }) { self.mode = Mode::Insert; @@ -121,4 +117,21 @@ impl App { )) } } + + pub(crate) fn value_under_cursor(&self, alternate_cursor: Option) -> Value { + self.value + .clone() + .follow_cell_path( + &alternate_cursor.unwrap_or(self.position.clone()).members, + false, + ) + .unwrap_or_else(|_| { + panic!( + "unexpected error when following {:?} in {}", + self.position.members, + self.value + .to_expanded_string(" ", &nu_protocol::Config::default()) + ) + }) + } } diff --git a/src/handler.rs b/src/handler.rs index a7a6055..5596083 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -186,17 +186,9 @@ pub fn handle_key_events( return Ok(TransitionResult::Return(app.value.clone())); } else if key_event == config.keybindings.peeking.view { app.position.members.pop(); - return Ok(TransitionResult::Return( - app.value - .clone() - .follow_cell_path(&app.position.members, false)?, - )); + return Ok(TransitionResult::Return(app.value_under_cursor(None))); } else if key_event == config.keybindings.peeking.under { - return Ok(TransitionResult::Return( - app.value - .clone() - .follow_cell_path(&app.position.members, false)?, - )); + return Ok(TransitionResult::Return(app.value_under_cursor(None))); } else if key_event == config.keybindings.peeking.cell_path { return Ok(TransitionResult::Return(Value::cell_path( app.position.clone(), @@ -211,11 +203,7 @@ pub fn handle_key_events( app.mode = Mode::Normal; return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.peek { - return Ok(TransitionResult::Return( - app.value - .clone() - .follow_cell_path(&app.position.members, false)?, - )); + return Ok(TransitionResult::Return(app.value_under_cursor(None))); } } } diff --git a/src/navigation.rs b/src/navigation.rs index b90ad59..b1c416e 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -41,20 +41,7 @@ impl App { .pop() .unwrap_or_else(|| panic!("unexpected error: position is empty")); - let cell = self - .value - .clone() - .follow_cell_path(&self.position.members, false) - .unwrap_or_else(|_| { - panic!( - "unexpected error when following {:?} in {}", - self.position.members, - self.value - .to_expanded_string(" ", &nu_protocol::Config::default()) - ) - }); - - match cell { + match self.value_under_cursor(None) { Value::List { vals, .. } => { let new = match current { PathMember::Int { @@ -127,20 +114,7 @@ impl App { /// > - push a new *cell path* member to the state if there is more depth ahead /// > - mark the state as *at the bottom* if the value at the new depth is of a simple type pub(super) fn go_deeper_in_data(&mut self) { - let cell = self - .value - .clone() - .follow_cell_path(&self.position.members, false) - .unwrap_or_else(|_| { - panic!( - "unexpected error when following {:?} in {}", - self.position.members, - self.value - .to_expanded_string(" ", &nu_protocol::Config::default()) - ) - }); - - match cell { + match self.value_under_cursor(None) { Value::List { vals, .. } => self.position.members.push(PathMember::Int { val: 0, span: Span::unknown(), diff --git a/src/ui.rs b/src/ui.rs index f52c89c..c07d060 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -7,7 +7,7 @@ use crate::{ use super::{App, Config, Mode}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use nu_protocol::ast::PathMember; +use nu_protocol::ast::{CellPath, PathMember}; use nu_protocol::{Record, Type, Value}; use ratatui::{ prelude::{Alignment, Constraint, Rect}, @@ -234,18 +234,7 @@ fn render_data(frame: &mut Frame, app: &mut App, config: &Config) { None }; - let value = app - .value - .clone() - .follow_cell_path(&data_path, false) - .unwrap_or_else(|_| { - panic!( - "unexpected error when following {:?} in {}", - app.position.members, - app.value - .to_expanded_string(" ", &nu_protocol::Config::default()) - ) - }); + let value = app.value_under_cursor(Some(CellPath { members: data_path })); let table_type = is_table(&value); let is_a_table = matches!(table_type, crate::nu::value::Table::IsValid); From 75de095e2c52b580c79f2fe87fbeac8396ac48cd Mon Sep 17 00:00:00 2001 From: amtoine Date: Thu, 18 Apr 2024 16:10:06 +0200 Subject: [PATCH 24/29] remove clones and unwraps from navigation --- src/navigation.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/navigation.rs b/src/navigation.rs index b1c416e..058dd42 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -35,11 +35,8 @@ impl App { return; } - let current = self - .position - .members - .pop() - .unwrap_or_else(|| panic!("unexpected error: position is empty")); + // NOTE: this should never fail by construction + let current = self.position.members.pop().unwrap(); match self.value_under_cursor(None) { Value::List { vals, .. } => { @@ -76,12 +73,13 @@ impl App { span, optional, } => { - let cols = rec.columns().cloned().collect::>(); + let cols = rec.columns().collect::>(); PathMember::String { val: if cols.is_empty() { "".into() } else { + // NOTE: this should never fail let index = rec.columns().position(|x| x == &val).unwrap(); let new_index = match direction { Direction::Up(step) => index.saturating_sub(step).max(0), @@ -93,7 +91,7 @@ impl App { Direction::At(id) => id.min(cols.len() - 1), }; - cols[new_index].clone() + cols[new_index].to_string() }, span, optional, From 7b91dfbea90395593bf31f98f3e84037fcbc57ee Mon Sep 17 00:00:00 2001 From: amtoine Date: Thu, 18 Apr 2024 16:14:58 +0200 Subject: [PATCH 25/29] clean handler --- src/handler.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 5596083..7e24818 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -94,11 +94,11 @@ pub fn handle_key_events( let mut path = app.position.clone(); path.members.pop(); - let view = app.value.clone().follow_cell_path(&path.members, false)?; + let view = app.value_under_cursor(Some(path.clone())); let transpose = transpose(&view); if transpose != view { - match transpose.clone() { + match &transpose { Value::Record { val: rec, .. } => { let cols = rec.columns().cloned().collect::>(); From 5fb10d324814e70a0d757538c22603ce685a0f13 Mon Sep 17 00:00:00 2001 From: amtoine Date: Fri, 19 Apr 2024 10:01:00 +0200 Subject: [PATCH 26/29] isolate Makefile rules --- Makefile | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 7b4fd2f..d192eaf 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,24 @@ CLIPPY_OPTIONS="-D warnings" -.PHONY: all check test fmt doc build register install clean -DEFAULT: check lock test +.PHONY: fmt-check fmt check lock clippy test doc build register install clean +DEFAULT: fmt-check check lock clippy test -check: +fmt-check: cargo fmt --all --verbose -- --check --verbose +fmt: + cargo fmt --all --verbose + +check: cargo check --workspace --lib --tests - cargo clippy --workspace -- "${CLIPPY_OPTIONS}" lock: check ./.github/workflows/scripts/check-cargo-lock.sh -test: check - cargo test --workspace +clippy: + cargo clippy --workspace -- "${CLIPPY_OPTIONS}" -fmt: - cargo fmt --all --verbose +test: + cargo test --workspace doc: cargo doc --document-private-items --no-deps --open From e67399e38f2bcd7dd55df711f6c4122a61a378e4 Mon Sep 17 00:00:00 2001 From: amtoine Date: Fri, 19 Apr 2024 11:02:26 +0200 Subject: [PATCH 27/29] make `handle_key_events` an impl of `app::App` --- src/handler.rs | 350 +++++++++++++++++++++++++------------------------ src/lib.rs | 5 +- 2 files changed, 179 insertions(+), 176 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 7e24818..68334ef 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -29,186 +29,190 @@ impl TransitionResult { } } -/// Handles the key events and updates the state of [`App`]. -#[allow(clippy::collapsible_if)] -pub fn handle_key_events( - key_event: KeyEvent, - app: &mut App, - config: &Config, - half_page: usize, -) -> Result { - match app.mode { - Mode::Normal => { - if key_event.code.ge(&KeyCode::Char('0')) && key_event.code.le(&KeyCode::Char('9')) { - app.mode = Mode::Waiting(match key_event.code { - KeyCode::Char('0') => 0, - KeyCode::Char('1') => 1, - KeyCode::Char('2') => 2, - KeyCode::Char('3') => 3, - KeyCode::Char('4') => 4, - KeyCode::Char('5') => 5, - KeyCode::Char('6') => 6, - KeyCode::Char('7') => 7, - KeyCode::Char('8') => 8, - KeyCode::Char('9') => 9, - _ => unreachable!(), - }); - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.navigation.half_page_down { - // TODO: add a margin to the bottom - app.go_up_or_down_in_data(Direction::Down(half_page)); - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.navigation.half_page_up { - // TODO: add a margin to the top - app.go_up_or_down_in_data(Direction::Up(half_page)); - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.navigation.goto_bottom { - app.go_up_or_down_in_data(Direction::Bottom); - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.navigation.goto_top { - app.go_up_or_down_in_data(Direction::Top); - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.quit { - return Ok(TransitionResult::Quit); - } else if key_event == config.keybindings.insert { - match app.enter_editor() { - Ok(_) => return Ok(TransitionResult::Continue), - Err(err) => return Ok(TransitionResult::Error(err)), - } - } else if key_event == config.keybindings.peek { - app.mode = Mode::Peeking; - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.navigation.down { - app.go_up_or_down_in_data(Direction::Down(1)); - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.navigation.up { - app.go_up_or_down_in_data(Direction::Up(1)); - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.navigation.right { - app.go_deeper_in_data(); - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.navigation.left { - app.go_back_in_data(); - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.transpose { - let mut path = app.position.clone(); - path.members.pop(); - - let view = app.value_under_cursor(Some(path.clone())); - let transpose = transpose(&view); - - if transpose != view { - match &transpose { - Value::Record { val: rec, .. } => { - let cols = rec.columns().cloned().collect::>(); - - // NOTE: app.position.members should never be empty by construction - *app.position.members.last_mut().unwrap() = PathMember::String { - val: cols.first().unwrap_or(&"".to_string()).to_string(), - span: Span::unknown(), - optional: cols.is_empty(), - }; - } - _ => { - // NOTE: app.position.members should never be empty by construction - *app.position.members.last_mut().unwrap() = PathMember::Int { - val: 0, - span: Span::unknown(), - optional: false, - }; +impl App { + /// Handles the key events and updates the state of [`App`]. + #[allow(clippy::collapsible_if)] + pub fn handle_key_events( + &mut self, + key_event: KeyEvent, + config: &Config, + half_page: usize, + ) -> Result { + match self.mode { + Mode::Normal => { + if key_event.code.ge(&KeyCode::Char('0')) && key_event.code.le(&KeyCode::Char('9')) + { + self.mode = Mode::Waiting(match key_event.code { + KeyCode::Char('0') => 0, + KeyCode::Char('1') => 1, + KeyCode::Char('2') => 2, + KeyCode::Char('3') => 3, + KeyCode::Char('4') => 4, + KeyCode::Char('5') => 5, + KeyCode::Char('6') => 6, + KeyCode::Char('7') => 7, + KeyCode::Char('8') => 8, + KeyCode::Char('9') => 9, + _ => unreachable!(), + }); + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.navigation.half_page_down { + // TODO: add a margin to the bottom + self.go_up_or_down_in_data(Direction::Down(half_page)); + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.navigation.half_page_up { + // TODO: add a margin to the top + self.go_up_or_down_in_data(Direction::Up(half_page)); + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.navigation.goto_bottom { + self.go_up_or_down_in_data(Direction::Bottom); + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.navigation.goto_top { + self.go_up_or_down_in_data(Direction::Top); + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.quit { + return Ok(TransitionResult::Quit); + } else if key_event == config.keybindings.insert { + match self.enter_editor() { + Ok(_) => return Ok(TransitionResult::Continue), + Err(err) => return Ok(TransitionResult::Error(err)), + } + } else if key_event == config.keybindings.peek { + self.mode = Mode::Peeking; + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.navigation.down { + self.go_up_or_down_in_data(Direction::Down(1)); + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.navigation.up { + self.go_up_or_down_in_data(Direction::Up(1)); + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.navigation.right { + self.go_deeper_in_data(); + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.navigation.left { + self.go_back_in_data(); + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.transpose { + let mut path = self.position.clone(); + path.members.pop(); + + let view = self.value_under_cursor(Some(path.clone())); + let transpose = transpose(&view); + + if transpose != view { + match &transpose { + Value::Record { val: rec, .. } => { + let cols = rec.columns().cloned().collect::>(); + + // NOTE: app.position.members should never be empty by construction + *self.position.members.last_mut().unwrap() = PathMember::String { + val: cols.first().unwrap_or(&"".to_string()).to_string(), + span: Span::unknown(), + optional: cols.is_empty(), + }; + } + _ => { + // NOTE: app.position.members should never be empty by construction + *self.position.members.last_mut().unwrap() = PathMember::Int { + val: 0, + span: Span::unknown(), + optional: false, + }; + } } + return Ok(TransitionResult::Mutate(transpose, path)); } - return Ok(TransitionResult::Mutate(transpose, path)); - } - return Ok(TransitionResult::Continue); - } - } - Mode::Waiting(n) => { - if key_event.code.ge(&KeyCode::Char('0')) && key_event.code.le(&KeyCode::Char('9')) { - let u = match key_event.code { - KeyCode::Char('0') => 0, - KeyCode::Char('1') => 1, - KeyCode::Char('2') => 2, - KeyCode::Char('3') => 3, - KeyCode::Char('4') => 4, - KeyCode::Char('5') => 5, - KeyCode::Char('6') => 6, - KeyCode::Char('7') => 7, - KeyCode::Char('8') => 8, - KeyCode::Char('9') => 9, - _ => unreachable!(), - }; - app.mode = Mode::Waiting(n * 10 + u); - return Ok(TransitionResult::Continue); - } else if key_event.code == KeyCode::Esc { - app.mode = Mode::Normal; - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.navigation.down { - app.mode = Mode::Normal; - app.go_up_or_down_in_data(Direction::Down(n)); - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.navigation.up { - app.mode = Mode::Normal; - app.go_up_or_down_in_data(Direction::Up(n)); - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.navigation.goto_line { - app.mode = Mode::Normal; - app.go_up_or_down_in_data(Direction::At(n.saturating_sub(1))); - return Ok(TransitionResult::Continue); + return Ok(TransitionResult::Continue); + } } - } - Mode::Insert => { - if key_event == config.keybindings.normal { - app.mode = Mode::Normal; - return Ok(TransitionResult::Continue); + Mode::Waiting(n) => { + if key_event.code.ge(&KeyCode::Char('0')) && key_event.code.le(&KeyCode::Char('9')) + { + let u = match key_event.code { + KeyCode::Char('0') => 0, + KeyCode::Char('1') => 1, + KeyCode::Char('2') => 2, + KeyCode::Char('3') => 3, + KeyCode::Char('4') => 4, + KeyCode::Char('5') => 5, + KeyCode::Char('6') => 6, + KeyCode::Char('7') => 7, + KeyCode::Char('8') => 8, + KeyCode::Char('9') => 9, + _ => unreachable!(), + }; + self.mode = Mode::Waiting(n * 10 + u); + return Ok(TransitionResult::Continue); + } else if key_event.code == KeyCode::Esc { + self.mode = Mode::Normal; + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.navigation.down { + self.mode = Mode::Normal; + self.go_up_or_down_in_data(Direction::Down(n)); + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.navigation.up { + self.mode = Mode::Normal; + self.go_up_or_down_in_data(Direction::Up(n)); + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.navigation.goto_line { + self.mode = Mode::Normal; + self.go_up_or_down_in_data(Direction::At(n.saturating_sub(1))); + return Ok(TransitionResult::Continue); + } } + Mode::Insert => { + if key_event == config.keybindings.normal { + self.mode = Mode::Normal; + return Ok(TransitionResult::Continue); + } - match app.editor.handle_key(&key_event.code) { - Some(Some(v)) => { - app.mode = Mode::Normal; - return Ok(TransitionResult::Mutate(v, app.position.clone())); + match self.editor.handle_key(&key_event.code) { + Some(Some(v)) => { + self.mode = Mode::Normal; + return Ok(TransitionResult::Mutate(v, self.position.clone())); + } + Some(None) => { + self.mode = Mode::Normal; + return Ok(TransitionResult::Continue); + } + None => return Ok(TransitionResult::Continue), } - Some(None) => { - app.mode = Mode::Normal; + } + Mode::Peeking => { + if key_event == config.keybindings.quit { + return Ok(TransitionResult::Quit); + } else if key_event == config.keybindings.normal { + self.mode = Mode::Normal; return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.peeking.all { + return Ok(TransitionResult::Return(self.value.clone())); + } else if key_event == config.keybindings.peeking.view { + self.position.members.pop(); + return Ok(TransitionResult::Return(self.value_under_cursor(None))); + } else if key_event == config.keybindings.peeking.under { + return Ok(TransitionResult::Return(self.value_under_cursor(None))); + } else if key_event == config.keybindings.peeking.cell_path { + return Ok(TransitionResult::Return(Value::cell_path( + self.position.clone(), + Span::unknown(), + ))); } - None => return Ok(TransitionResult::Continue), } - } - Mode::Peeking => { - if key_event == config.keybindings.quit { - return Ok(TransitionResult::Quit); - } else if key_event == config.keybindings.normal { - app.mode = Mode::Normal; - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.peeking.all { - return Ok(TransitionResult::Return(app.value.clone())); - } else if key_event == config.keybindings.peeking.view { - app.position.members.pop(); - return Ok(TransitionResult::Return(app.value_under_cursor(None))); - } else if key_event == config.keybindings.peeking.under { - return Ok(TransitionResult::Return(app.value_under_cursor(None))); - } else if key_event == config.keybindings.peeking.cell_path { - return Ok(TransitionResult::Return(Value::cell_path( - app.position.clone(), - Span::unknown(), - ))); - } - } - Mode::Bottom => { - if key_event == config.keybindings.quit { - return Ok(TransitionResult::Quit); - } else if key_event == config.keybindings.navigation.left { - app.mode = Mode::Normal; - return Ok(TransitionResult::Continue); - } else if key_event == config.keybindings.peek { - return Ok(TransitionResult::Return(app.value_under_cursor(None))); + Mode::Bottom => { + if key_event == config.keybindings.quit { + return Ok(TransitionResult::Quit); + } else if key_event == config.keybindings.navigation.left { + self.mode = Mode::Normal; + return Ok(TransitionResult::Continue); + } else if key_event == config.keybindings.peek { + return Ok(TransitionResult::Return(self.value_under_cursor(None))); + } } } - } - Ok(TransitionResult::Continue) + Ok(TransitionResult::Continue) + } } /// represent a [`KeyEvent`] as a simple string @@ -241,7 +245,7 @@ mod tests { record, Span, Value, }; - use super::{handle_key_events, repr_key, App, TransitionResult}; + use super::{repr_key, App, TransitionResult}; use crate::{ app::Mode, config::Config, @@ -294,7 +298,7 @@ mod tests { for (key, expected_mode) in transitions { let mode = app.mode.clone(); - let result = handle_key_events(key, &mut app, &config, 0).unwrap(); + let result = app.handle_key_events(key, &config, 0).unwrap(); assert!( !result.is_quit(), @@ -333,7 +337,7 @@ mod tests { for (key, exit) in transitions { let mode = app.mode.clone(); - let result = handle_key_events(key, &mut app, &config, 0).unwrap(); + let result = app.handle_key_events(key, &config, 0).unwrap(); if exit { assert!( @@ -436,7 +440,7 @@ mod tests { for (key, cell_path, bottom) in transitions { let expected = to_path_member_vec(&cell_path); - handle_key_events(key, &mut app, &config, 0).unwrap(); + app.handle_key_events(key, &config, 0).unwrap(); if bottom { assert!( @@ -471,7 +475,7 @@ mod tests { for (key, exit, expected) in transitions { let mode = app.mode.clone(); - let result = handle_key_events(key, &mut app, config, 0).unwrap(); + let result = app.handle_key_events(key, config, 0).unwrap(); if exit { assert!( @@ -625,7 +629,7 @@ mod tests { for (key, cell_path) in transitions { let expected = to_path_member_vec(&cell_path); if let TransitionResult::Mutate(cell, path) = - handle_key_events(key, &mut app, &config, 0).unwrap() + app.handle_key_events(key, &config, 0).unwrap() { app.value = crate::nu::value::mutate_value_cell(&app.value, &path, &cell).unwrap() } diff --git a/src/lib.rs b/src/lib.rs index 6bcbe70..83478f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ use nu_protocol::{Span, Value}; use app::{App, Mode}; use config::Config; -use handler::{handle_key_events, TransitionResult}; +use handler::TransitionResult; use tui::{ event::{Event, EventHandler}, Tui, @@ -46,9 +46,8 @@ pub fn explore(config: &Value, input: Value) -> Result { Event::Tick => app.tick(), Event::Key(key_event) => { if key_event.kind == KeyEventKind::Press { - match handle_key_events( + match app.handle_key_events( key_event, - &mut app, &config, (tui.size()?.height as usize - 5) / 2, )? { From da3fc51b831fb11f9d60ada0fd181443cf5d0da0 Mon Sep 17 00:00:00 2001 From: amtoine Date: Fri, 19 Apr 2024 11:18:25 +0200 Subject: [PATCH 28/29] embed the configuration in the `app::App` --- src/app.rs | 12 ++++++++- src/edit.rs | 2 +- src/handler.rs | 66 +++++++++++++++++++++----------------------------- src/lib.rs | 14 +++-------- src/tui/mod.rs | 6 ++--- src/ui.rs | 19 +++++++++------ 6 files changed, 58 insertions(+), 61 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3fdb555..6283211 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,7 +4,7 @@ use nu_protocol::{ Span, Value, }; -use crate::edit::Editor; +use crate::{config::Config, edit::Editor}; /// the mode in which the application is #[derive(Clone, Debug, PartialEq, Default)] @@ -37,6 +37,7 @@ impl std::fmt::Display for Mode { } } +#[derive(Clone)] /// the complete state of the application pub struct App { /// the full current path in the data @@ -49,6 +50,8 @@ pub struct App { pub editor: Editor, /// the value that is being explored pub value: Value, + /// the configuration for the app + pub config: Config, } impl Default for App { @@ -59,6 +62,7 @@ impl Default for App { mode: Mode::default(), editor: Editor::default(), value: Value::default(), + config: Config::default(), } } } @@ -134,4 +138,10 @@ impl App { ) }) } + + pub(crate) fn with_config(&self, config: Config) -> Self { + let mut app = self.clone(); + app.config = config; + app + } } diff --git a/src/edit.rs b/src/edit.rs index 2177d9e..f461819 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -10,7 +10,7 @@ use nu_protocol::{Span, Value}; use crate::config::Config; -#[derive(Default)] +#[derive(Default, Clone)] pub struct Editor { buffer: String, cursor_position: (usize, usize), diff --git a/src/handler.rs b/src/handler.rs index 68334ef..e7c6805 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -7,7 +7,6 @@ use nu_protocol::{ use crate::{ app::{App, Mode}, - config::Config, navigation::Direction, nu::value::transpose, }; @@ -35,9 +34,10 @@ impl App { pub fn handle_key_events( &mut self, key_event: KeyEvent, - config: &Config, half_page: usize, ) -> Result { + let config = &self.config; + match self.mode { Mode::Normal => { if key_event.code.ge(&KeyCode::Char('0')) && key_event.code.le(&KeyCode::Char('9')) @@ -277,11 +277,8 @@ mod tests { #[test] fn switch_modes() { - let config = Config::default(); - let keybindings = config.clone().keybindings; - - let value = Value::test_string("foo"); - let mut app = App::from_value(value); + let mut app = App::from_value(Value::test_string("foo")); + let keybindings = app.config.clone().keybindings; assert!(app.mode == Mode::Normal); @@ -298,7 +295,7 @@ mod tests { for (key, expected_mode) in transitions { let mode = app.mode.clone(); - let result = app.handle_key_events(key, &config, 0).unwrap(); + let result = app.handle_key_events(key, 0).unwrap(); assert!( !result.is_quit(), @@ -319,11 +316,8 @@ mod tests { #[test] fn quit() { - let config = Config::default(); - let keybindings = config.clone().keybindings; - - let value = test_value(); - let mut app = App::from_value(value); + let mut app = App::from_value(test_value()); + let keybindings = app.config.clone().keybindings; let transitions = vec![ (keybindings.insert, false), @@ -337,7 +331,7 @@ mod tests { for (key, exit) in transitions { let mode = app.mode.clone(); - let result = app.handle_key_events(key, &config, 0).unwrap(); + let result = app.handle_key_events(key, 0).unwrap(); if exit { assert!( @@ -375,11 +369,8 @@ mod tests { #[test] fn navigate_the_data() { - let config = Config::default(); - let nav = config.clone().keybindings.navigation; - - let value = test_value(); - let mut app = App::from_value(value.clone()); + let mut app = App::from_value(test_value()); + let nav = app.config.clone().keybindings.navigation; assert!(!app.is_at_bottom()); assert_eq!(app.position.members, to_path_member_vec(&[PM::S("l")])); @@ -440,7 +431,7 @@ mod tests { for (key, cell_path, bottom) in transitions { let expected = to_path_member_vec(&cell_path); - app.handle_key_events(key, &config, 0).unwrap(); + app.handle_key_events(key, 0).unwrap(); if bottom { assert!( @@ -467,15 +458,15 @@ mod tests { fn run_peeking_scenario( transitions: Vec<(KeyEvent, bool, Option)>, - config: &Config, + config: Config, value: Value, ) { - let mut app = App::from_value(value); + let mut app = App::from_value(value).with_config(config); for (key, exit, expected) in transitions { let mode = app.mode.clone(); - let result = app.handle_key_events(key, config, 0).unwrap(); + let result = app.handle_key_events(key, 0).unwrap(); if exit { assert!( @@ -534,13 +525,13 @@ mod tests { (keybindings.peek, false, None), (keybindings.peeking.all, true, Some(value.clone())), ]; - run_peeking_scenario(peek_all_from_top, &config, value.clone()); + run_peeking_scenario(peek_all_from_top, config.clone(), value.clone()); let peek_current_from_top = vec![ (keybindings.peek, false, None), (keybindings.peeking.view, true, Some(value.clone())), ]; - run_peeking_scenario(peek_current_from_top, &config, value.clone()); + run_peeking_scenario(peek_current_from_top, config.clone(), value.clone()); let go_in_the_data_and_peek_all_and_current = vec![ (keybindings.navigation.down, false, None), @@ -558,7 +549,7 @@ mod tests { ]; run_peeking_scenario( go_in_the_data_and_peek_all_and_current, - &config, + config.clone(), value.clone(), ); @@ -569,7 +560,7 @@ mod tests { (keybindings.peeking.all, true, Some(value.clone())), (keybindings.peeking.under, true, Some(Value::test_int(1))), ]; - run_peeking_scenario(go_in_the_data_and_peek_under, &config, value.clone()); + run_peeking_scenario(go_in_the_data_and_peek_under, config.clone(), value.clone()); let go_in_the_data_and_peek_cell_path = vec![ (keybindings.navigation.down, false, None), // on {r: {a: 1, b: 2}} @@ -594,27 +585,28 @@ mod tests { })), ), ]; - run_peeking_scenario(go_in_the_data_and_peek_cell_path, &config, value.clone()); + run_peeking_scenario( + go_in_the_data_and_peek_cell_path, + config.clone(), + value.clone(), + ); let peek_at_the_bottom = vec![ (keybindings.navigation.right, false, None), // on l: ["my", "list", "elements"], (keybindings.navigation.right, false, None), // on "my" (keybindings.peek, true, Some(Value::test_string("my"))), ]; - run_peeking_scenario(peek_at_the_bottom, &config, value); + run_peeking_scenario(peek_at_the_bottom, config.clone(), value); } #[test] fn transpose_the_data() { - let config = Config::default(); - let kmap = config.clone().keybindings; - - let value = Value::test_record(record!( + let mut app = App::from_value(Value::test_record(record!( "a" => Value::test_int(1), "b" => Value::test_int(2), "c" => Value::test_int(3), - )); - let mut app = App::from_value(value.clone()); + ))); + let kmap = app.config.clone().keybindings; assert!(!app.is_at_bottom()); assert_eq!(app.position.members, to_path_member_vec(&[PM::S("a")])); @@ -628,9 +620,7 @@ mod tests { for (key, cell_path) in transitions { let expected = to_path_member_vec(&cell_path); - if let TransitionResult::Mutate(cell, path) = - app.handle_key_events(key, &config, 0).unwrap() - { + if let TransitionResult::Mutate(cell, path) = app.handle_key_events(key, 0).unwrap() { app.value = crate::nu::value::mutate_value_cell(&app.value, &path, &cell).unwrap() } diff --git a/src/lib.rs b/src/lib.rs index 83478f1..8fb48c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,32 +25,26 @@ use tui::{ }; pub fn explore(config: &Value, input: Value) -> Result { - let config = Config::from_value(config)?; - let mut tui = Tui::new( Terminal::new(CrosstermBackend::new(io::stderr()))?, EventHandler::new(250), ); tui.init()?; - let mut app = App::from_value(input); + let mut app = App::from_value(input).with_config(Config::from_value(config)?); loop { if app.mode == Mode::Insert { app.editor.set_width(tui.size()?.width as usize) } - tui.draw(&mut app, &config, None)?; + tui.draw(&mut app, None)?; match tui.events.next()? { Event::Tick => app.tick(), Event::Key(key_event) => { if key_event.kind == KeyEventKind::Press { - match app.handle_key_events( - key_event, - &config, - (tui.size()?.height as usize - 5) / 2, - )? { + match app.handle_key_events(key_event, (tui.size()?.height as usize - 5) / 2)? { TransitionResult::Quit => break, TransitionResult::Continue => {} TransitionResult::Mutate(cell, path) => { @@ -59,7 +53,7 @@ pub fn explore(config: &Value, input: Value) -> Result { .unwrap() } TransitionResult::Error(error) => { - tui.draw(&mut app, &config, Some(&error))?; + tui.draw(&mut app, Some(&error))?; loop { if let Event::Key(_) = tui.events.next()? { break; diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 8eade31..b8b82e5 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -9,7 +9,7 @@ use std::panic; pub(crate) mod event; -use crate::{app::App, config::Config, ui}; +use crate::{app::App, ui}; use event::EventHandler; /// Representation of a terminal user interface. @@ -54,9 +54,9 @@ impl Tui { /// /// [`Draw`]: tui::Terminal::draw /// [`rendering`]: crate::ui:render - pub fn draw(&mut self, app: &mut App, config: &Config, error: Option<&str>) -> Result<()> { + pub fn draw(&mut self, app: &mut App, error: Option<&str>) -> Result<()> { self.terminal - .draw(|frame| ui::render_ui(frame, app, config, error))?; + .draw(|frame| ui::render_ui(frame, app, error))?; Ok(()) } diff --git a/src/ui.rs b/src/ui.rs index c07d060..5246a42 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -5,7 +5,7 @@ use crate::{ nu::{strings::SpecialString, value::is_table}, }; -use super::{App, Config, Mode}; +use super::{App, Mode}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use nu_protocol::ast::{CellPath, PathMember}; use nu_protocol::{Record, Type, Value}; @@ -20,19 +20,19 @@ use ratatui::{ }; /// render the whole ui -pub(super) fn render_ui(frame: &mut Frame, app: &mut App, config: &Config, error: Option<&str>) { - render_data(frame, app, config); - if config.show_cell_path { +pub(super) fn render_ui(frame: &mut Frame, app: &mut App, error: Option<&str>) { + render_data(frame, app); + if app.config.show_cell_path { render_cell_path(frame, app); } match error { Some(err) => render_error(frame, err), None => { - render_status_bar(frame, app, config); + render_status_bar(frame, app); if app.mode == Mode::Insert { - app.editor.render(frame, config); + app.editor.render(frame, &app.config); } } } @@ -226,7 +226,9 @@ fn repr_table(table: &[Record]) -> (Vec, Vec, Vec>) /// /// the data will be rendered on top of the bar, and on top of the cell path in case /// [`crate::config::Config::show_cell_path`] is set to `true`. -fn render_data(frame: &mut Frame, app: &mut App, config: &Config) { +fn render_data(frame: &mut Frame, app: &mut App) { + let config = &app.config; + let mut data_path = app.position.members.clone(); let current = if !app.is_at_bottom() { data_path.pop() @@ -624,7 +626,8 @@ fn render_cell_path(frame: &mut Frame, app: &App) { /// ```text /// ||PEEKING ... to NORMAL | a to peek all | c to peek current view | u to peek under cursor | q to quit|| /// ``` -fn render_status_bar(frame: &mut Frame, app: &App, config: &Config) { +fn render_status_bar(frame: &mut Frame, app: &App) { + let config = &app.config; let bottom_bar_rect = Rect::new(0, frame.size().height - 1, frame.size().width, 1); let bg_style = match app.mode { From eae82ef679020e583004bd5d93cedd4f5cc054d9 Mon Sep 17 00:00:00 2001 From: amtoine Date: Wed, 17 Apr 2024 20:01:51 +0200 Subject: [PATCH 29/29] use `nuon::from_nuon` from nushell/nushell#12553 --- Cargo.lock | 40 ++-- Cargo.toml | 10 +- nupm.nuon | 2 +- src/config/mod.rs | 4 +- src/nu/value.rs | 470 +--------------------------------------------- 5 files changed, 38 insertions(+), 488 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef4f6dd..f66e748 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -994,7 +994,7 @@ dependencies = [ [[package]] name = "nu-engine" version = "0.92.3" -source = "git+https://github.com/nushell/nushell?rev=c06ef201b72b3cbe901820417106b7d65c6f01e1#c06ef201b72b3cbe901820417106b7d65c6f01e1" +source = "git+https://github.com/nushell/nushell?rev=55edef5ddaf3d3d55290863446c2dd50c012e9bc#55edef5ddaf3d3d55290863446c2dd50c012e9bc" dependencies = [ "nu-glob", "nu-path", @@ -1005,12 +1005,12 @@ dependencies = [ [[package]] name = "nu-glob" version = "0.92.3" -source = "git+https://github.com/nushell/nushell?rev=c06ef201b72b3cbe901820417106b7d65c6f01e1#c06ef201b72b3cbe901820417106b7d65c6f01e1" +source = "git+https://github.com/nushell/nushell?rev=55edef5ddaf3d3d55290863446c2dd50c012e9bc#55edef5ddaf3d3d55290863446c2dd50c012e9bc" [[package]] name = "nu-parser" version = "0.92.3" -source = "git+https://github.com/nushell/nushell?rev=c06ef201b72b3cbe901820417106b7d65c6f01e1#c06ef201b72b3cbe901820417106b7d65c6f01e1" +source = "git+https://github.com/nushell/nushell?rev=55edef5ddaf3d3d55290863446c2dd50c012e9bc#55edef5ddaf3d3d55290863446c2dd50c012e9bc" dependencies = [ "bytesize", "chrono", @@ -1025,7 +1025,7 @@ dependencies = [ [[package]] name = "nu-path" version = "0.92.3" -source = "git+https://github.com/nushell/nushell?rev=c06ef201b72b3cbe901820417106b7d65c6f01e1#c06ef201b72b3cbe901820417106b7d65c6f01e1" +source = "git+https://github.com/nushell/nushell?rev=55edef5ddaf3d3d55290863446c2dd50c012e9bc#55edef5ddaf3d3d55290863446c2dd50c012e9bc" dependencies = [ "dirs-next", "omnipath", @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "nu-plugin" version = "0.92.3" -source = "git+https://github.com/nushell/nushell?rev=c06ef201b72b3cbe901820417106b7d65c6f01e1#c06ef201b72b3cbe901820417106b7d65c6f01e1" +source = "git+https://github.com/nushell/nushell?rev=55edef5ddaf3d3d55290863446c2dd50c012e9bc#55edef5ddaf3d3d55290863446c2dd50c012e9bc" dependencies = [ "bincode", "interprocess", @@ -1058,7 +1058,7 @@ dependencies = [ [[package]] name = "nu-protocol" version = "0.92.3" -source = "git+https://github.com/nushell/nushell?rev=c06ef201b72b3cbe901820417106b7d65c6f01e1#c06ef201b72b3cbe901820417106b7d65c6f01e1" +source = "git+https://github.com/nushell/nushell?rev=55edef5ddaf3d3d55290863446c2dd50c012e9bc#55edef5ddaf3d3d55290863446c2dd50c012e9bc" dependencies = [ "byte-unit", "chrono", @@ -1080,7 +1080,7 @@ dependencies = [ [[package]] name = "nu-system" version = "0.92.3" -source = "git+https://github.com/nushell/nushell?rev=c06ef201b72b3cbe901820417106b7d65c6f01e1#c06ef201b72b3cbe901820417106b7d65c6f01e1" +source = "git+https://github.com/nushell/nushell?rev=55edef5ddaf3d3d55290863446c2dd50c012e9bc#55edef5ddaf3d3d55290863446c2dd50c012e9bc" dependencies = [ "chrono", "libc", @@ -1098,7 +1098,7 @@ dependencies = [ [[package]] name = "nu-utils" version = "0.92.3" -source = "git+https://github.com/nushell/nushell?rev=c06ef201b72b3cbe901820417106b7d65c6f01e1#c06ef201b72b3cbe901820417106b7d65c6f01e1" +source = "git+https://github.com/nushell/nushell?rev=55edef5ddaf3d3d55290863446c2dd50c012e9bc#55edef5ddaf3d3d55290863446c2dd50c012e9bc" dependencies = [ "crossterm_winapi", "log", @@ -1113,14 +1113,14 @@ dependencies = [ [[package]] name = "nu_plugin_explore" -version = "0.92.3-c06ef201b72b3cbe901820417106b7d65c6f01e1" +version = "0.92.3-55edef5ddaf3d3d55290863446c2dd50c012e9bc" dependencies = [ "anyhow", "console", "crossterm", - "nu-parser", "nu-plugin", "nu-protocol", + "nuon", "ratatui", "url", ] @@ -1144,6 +1144,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "nuon" +version = "0.92.3" +source = "git+https://github.com/nushell/nushell?rev=55edef5ddaf3d3d55290863446c2dd50c012e9bc#55edef5ddaf3d3d55290863446c2dd50c012e9bc" +dependencies = [ + "fancy-regex", + "nu-engine", + "nu-parser", + "nu-protocol", + "once_cell", +] + [[package]] name = "omnipath" version = "0.1.6" @@ -1503,9 +1515,9 @@ dependencies = [ [[package]] name = "rmp" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ "byteorder", "num-traits", @@ -1514,9 +1526,9 @@ dependencies = [ [[package]] name = "rmp-serde" -version = "1.1.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" +checksum = "938a142ab806f18b88a97b0dea523d39e0fd730a064b035726adcfc58a8a5188" dependencies = [ "byteorder", "rmp", diff --git a/Cargo.toml b/Cargo.toml index 5b7647d..72c2a31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,12 +6,14 @@ name = "nu_plugin_explore" anyhow = "1.0.73" console = "0.15.7" crossterm = "0.27.0" -nu-plugin = { git = "https://github.com/nushell/nushell", rev = "c06ef201b72b3cbe901820417106b7d65c6f01e1", package = "nu-plugin" } -nu-protocol = { git = "https://github.com/nushell/nushell", rev = "c06ef201b72b3cbe901820417106b7d65c6f01e1", package = "nu-protocol", features = ["plugin"] } -nu-parser = { git = "https://github.com/nushell/nushell", rev = "c06ef201b72b3cbe901820417106b7d65c6f01e1", package = "nu-parser" } +nu-plugin = { git = "https://github.com/nushell/nushell", rev = "55edef5ddaf3d3d55290863446c2dd50c012e9bc", package = "nu-plugin" } +nu-protocol = { git = "https://github.com/nushell/nushell", rev = "55edef5ddaf3d3d55290863446c2dd50c012e9bc", package = "nu-protocol", features = ["plugin"] } ratatui = "0.26.1" url = "2.4.0" +[dev-dependencies] +nuon = { git = "https://github.com/nushell/nushell", rev = "55edef5ddaf3d3d55290863446c2dd50c012e9bc", package = "nuon" } + [target.'cfg(target_os = "macos")'.dependencies] crossterm = { version = "0.27.0", features = ["use-dev-tty"] } @@ -21,4 +23,4 @@ bench = false [package] edition = "2021" name = "nu_plugin_explore" -version = "0.92.3-c06ef201b72b3cbe901820417106b7d65c6f01e1" +version = "0.92.3-55edef5ddaf3d3d55290863446c2dd50c012e9bc" diff --git a/nupm.nuon b/nupm.nuon index 0f837fd..6836a50 100644 --- a/nupm.nuon +++ b/nupm.nuon @@ -1,6 +1,6 @@ { name: nu_plugin_explore, - version: "0.92.3-c06ef201b72b3cbe901820417106b7d65c6f01e1", + version: "0.92.3-55edef5ddaf3d3d55290863446c2dd50c012e9bc" description: "A fast structured data explorer for Nushell.", license: LICENSE, type: custom diff --git a/src/config/mod.rs b/src/config/mod.rs index 1030d78..6e82135 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -755,7 +755,7 @@ mod tests { use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use nu_protocol::{record, Record, Value}; - use crate::{handler::repr_key, nu::value::from_nuon}; + use crate::handler::repr_key; use super::Config; @@ -844,7 +844,7 @@ mod tests { assert_eq!( Config::default(), Config::from_value( - &from_nuon(include_str!("../../examples/config/default.nuon")).unwrap() + &nuon::from_nuon(include_str!("../../examples/config/default.nuon"), None).unwrap() ) .unwrap() ) diff --git a/src/nu/value.rs b/src/nu/value.rs index 42b3e15..1cb20a4 100644 --- a/src/nu/value.rs +++ b/src/nu/value.rs @@ -1,9 +1,8 @@ -use std::{collections::HashMap, sync::Arc}; +use std::collections::HashMap; use nu_protocol::{ - ast::{CellPath, Expr, Expression, PathMember, RecordItem}, - engine::{EngineState, StateWorkingSet}, - record, Range, Record, ShellError, Span, Type, Unit, Value, + ast::{CellPath, PathMember}, + record, Record, Span, Type, Value, }; #[derive(Debug, PartialEq)] @@ -326,448 +325,6 @@ pub(crate) fn transpose(value: &Value) -> Value { } } -#[inline(always)] -fn convert_to_value( - expr: Expression, - span: Span, - original_text: &str, -) -> Result { - match expr.expr { - Expr::BinaryOp(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "binary operators not supported in nuon".into(), - span: expr.span, - }), - Expr::UnaryNot(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "unary operators not supported in nuon".into(), - span: expr.span, - }), - Expr::Block(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "blocks not supported in nuon".into(), - span: expr.span, - }), - Expr::Closure(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "closures not supported in nuon".into(), - span: expr.span, - }), - Expr::Binary(val) => Ok(Value::binary(val, span)), - Expr::Bool(val) => Ok(Value::bool(val, span)), - Expr::Call(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "calls not supported in nuon".into(), - span: expr.span, - }), - Expr::CellPath(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "subexpressions and cellpaths not supported in nuon".into(), - span: expr.span, - }), - Expr::DateTime(dt) => Ok(Value::date(dt, span)), - Expr::ExternalCall(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "calls not supported in nuon".into(), - span: expr.span, - }), - Expr::Filepath(val, _) => Ok(Value::string(val, span)), - Expr::Directory(val, _) => Ok(Value::string(val, span)), - Expr::Float(val) => Ok(Value::float(val, span)), - Expr::FullCellPath(full_cell_path) => { - if !full_cell_path.tail.is_empty() { - Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "subexpressions and cellpaths not supported in nuon".into(), - span: expr.span, - }) - } else { - convert_to_value(full_cell_path.head, span, original_text) - } - } - - Expr::Garbage => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "extra tokens in input file".into(), - span: expr.span, - }), - Expr::GlobPattern(val, _) => Ok(Value::string(val, span)), - Expr::ImportPattern(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "imports not supported in nuon".into(), - span: expr.span, - }), - Expr::Overlay(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "overlays not supported in nuon".into(), - span: expr.span, - }), - Expr::Int(val) => Ok(Value::int(val, span)), - Expr::Keyword(kw, ..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: format!("{} not supported in nuon", String::from_utf8_lossy(&kw)), - span: expr.span, - }), - Expr::List(vals) => { - let mut output = vec![]; - for val in vals { - output.push(convert_to_value(val, span, original_text)?); - } - - Ok(Value::list(output, span)) - } - Expr::MatchBlock(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "match blocks not supported in nuon".into(), - span: expr.span, - }), - Expr::Nothing => Ok(Value::nothing(span)), - Expr::Operator(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "operators not supported in nuon".into(), - span: expr.span, - }), - Expr::Range(from, next, to, operator) => { - let from = if let Some(f) = from { - convert_to_value(*f, span, original_text)? - } else { - Value::nothing(expr.span) - }; - - let next = if let Some(s) = next { - convert_to_value(*s, span, original_text)? - } else { - Value::nothing(expr.span) - }; - - let to = if let Some(t) = to { - convert_to_value(*t, span, original_text)? - } else { - Value::nothing(expr.span) - }; - - Ok(Value::range( - Range::new(from, next, to, operator.inclusion, expr.span)?, - expr.span, - )) - } - Expr::Record(key_vals) => { - let mut record = Record::with_capacity(key_vals.len()); - let mut key_spans = Vec::with_capacity(key_vals.len()); - - for key_val in key_vals { - match key_val { - RecordItem::Pair(key, val) => { - let key_str = match key.expr { - Expr::String(key_str) => key_str, - _ => { - return Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "only strings can be keys".into(), - span: key.span, - }) - } - }; - - if let Some(i) = record.index_of(&key_str) { - return Err(ShellError::ColumnDefinedTwice { - col_name: key_str, - second_use: key.span, - first_use: key_spans[i], - }); - } else { - key_spans.push(key.span); - record.push(key_str, convert_to_value(val, span, original_text)?); - } - } - RecordItem::Spread(_, inner) => { - return Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "spread operator not supported in nuon".into(), - span: inner.span, - }); - } - } - } - - Ok(Value::record(record, span)) - } - Expr::RowCondition(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "row conditions not supported in nuon".into(), - span: expr.span, - }), - Expr::Signature(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "signatures not supported in nuon".into(), - span: expr.span, - }), - Expr::Spread(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "spread operator not supported in nuon".into(), - span: expr.span, - }), - Expr::String(s) => Ok(Value::string(s, span)), - Expr::StringInterpolation(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "string interpolation not supported in nuon".into(), - span: expr.span, - }), - Expr::Subexpression(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "subexpressions not supported in nuon".into(), - span: expr.span, - }), - Expr::Table(mut headers, cells) => { - let mut cols = vec![]; - - let mut output = vec![]; - - for key in headers.iter_mut() { - let key_str = match &mut key.expr { - Expr::String(key_str) => key_str, - _ => { - return Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "only strings can be keys".into(), - span: expr.span, - }) - } - }; - - if let Some(idx) = cols.iter().position(|existing| existing == key_str) { - return Err(ShellError::ColumnDefinedTwice { - col_name: key_str.clone(), - second_use: key.span, - first_use: headers[idx].span, - }); - } else { - cols.push(std::mem::take(key_str)); - } - } - - for row in cells { - if cols.len() != row.len() { - return Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "table has mismatched columns".into(), - span: expr.span, - }); - } - - let record = cols - .iter() - .zip(row) - .map(|(col, cell)| { - convert_to_value(cell, span, original_text).map(|val| (col.clone(), val)) - }) - .collect::>()?; - - output.push(Value::record(record, span)); - } - - Ok(Value::list(output, span)) - } - Expr::ValueWithUnit(val, unit) => { - let size = match val.expr { - Expr::Int(val) => val, - _ => { - return Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "non-integer unit value".into(), - span: expr.span, - }) - } - }; - - match unit.item { - Unit::Byte => Ok(Value::filesize(size, span)), - Unit::Kilobyte => Ok(Value::filesize(size * 1000, span)), - Unit::Megabyte => Ok(Value::filesize(size * 1000 * 1000, span)), - Unit::Gigabyte => Ok(Value::filesize(size * 1000 * 1000 * 1000, span)), - Unit::Terabyte => Ok(Value::filesize(size * 1000 * 1000 * 1000 * 1000, span)), - Unit::Petabyte => Ok(Value::filesize( - size * 1000 * 1000 * 1000 * 1000 * 1000, - span, - )), - Unit::Exabyte => Ok(Value::filesize( - size * 1000 * 1000 * 1000 * 1000 * 1000 * 1000, - span, - )), - - Unit::Kibibyte => Ok(Value::filesize(size * 1024, span)), - Unit::Mebibyte => Ok(Value::filesize(size * 1024 * 1024, span)), - Unit::Gibibyte => Ok(Value::filesize(size * 1024 * 1024 * 1024, span)), - Unit::Tebibyte => Ok(Value::filesize(size * 1024 * 1024 * 1024 * 1024, span)), - Unit::Pebibyte => Ok(Value::filesize( - size * 1024 * 1024 * 1024 * 1024 * 1024, - span, - )), - Unit::Exbibyte => Ok(Value::filesize( - size * 1024 * 1024 * 1024 * 1024 * 1024 * 1024, - span, - )), - - Unit::Nanosecond => Ok(Value::duration(size, span)), - Unit::Microsecond => Ok(Value::duration(size * 1000, span)), - Unit::Millisecond => Ok(Value::duration(size * 1000 * 1000, span)), - Unit::Second => Ok(Value::duration(size * 1000 * 1000 * 1000, span)), - Unit::Minute => Ok(Value::duration(size * 1000 * 1000 * 1000 * 60, span)), - Unit::Hour => Ok(Value::duration(size * 1000 * 1000 * 1000 * 60 * 60, span)), - Unit::Day => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24) { - Some(val) => Ok(Value::duration(val, span)), - None => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "day duration too large".into(), - msg: "day duration too large".into(), - span: expr.span, - }), - }, - - Unit::Week => match size.checked_mul(1000 * 1000 * 1000 * 60 * 60 * 24 * 7) { - Some(val) => Ok(Value::duration(val, span)), - None => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "week duration too large".into(), - msg: "week duration too large".into(), - span: expr.span, - }), - }, - } - } - Expr::Var(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "variables not supported in nuon".into(), - span: expr.span, - }), - Expr::VarDecl(..) => Err(ShellError::OutsideSpannedLabeledError { - src: original_text.to_string(), - error: "Error when loading".into(), - msg: "variable declarations not supported in nuon".into(), - span: expr.span, - }), - } -} - -#[allow(dead_code)] // this is only used in tests for `config` -pub(crate) fn from_nuon(input: &str) -> Result { - let engine_state = EngineState::default(); - - let mut working_set = StateWorkingSet::new(&engine_state); - - let mut block = nu_parser::parse(&mut working_set, None, input.as_bytes(), false); - - if let Some(pipeline) = block.pipelines.get(1) { - if let Some(element) = pipeline.elements.first() { - return Err(ShellError::GenericError { - error: "error when loading nuon text".into(), - msg: "could not load nuon text".into(), - span: None, - help: None, - inner: vec![ShellError::OutsideSpannedLabeledError { - src: input.to_string(), - error: "error when loading".into(), - msg: "excess values when loading".into(), - span: element.expr.span, - }], - }); - } else { - return Err(ShellError::GenericError { - error: "error when loading nuon text".into(), - msg: "could not load nuon text".into(), - span: None, - help: None, - inner: vec![ShellError::GenericError { - error: "error when loading".into(), - msg: "excess values when loading".into(), - span: None, - help: None, - inner: vec![], - }], - }); - } - } - - let expr = if block.pipelines.is_empty() { - Expression { - expr: Expr::Nothing, - span: Span::unknown(), - custom_completion: None, - ty: Type::Nothing, - } - } else { - let mut pipeline = Arc::make_mut(&mut block).pipelines.remove(0); - - if let Some(expr) = pipeline.elements.get(1) { - return Err(ShellError::GenericError { - error: "error when loading nuon text".into(), - msg: "could not load nuon text".into(), - span: None, - help: None, - inner: vec![ShellError::OutsideSpannedLabeledError { - src: input.to_string(), - error: "error when loading".into(), - msg: "detected a pipeline in nuon file".into(), - span: expr.expr.span, - }], - }); - } - - if pipeline.elements.is_empty() { - Expression { - expr: Expr::Nothing, - span: Span::unknown(), - custom_completion: None, - ty: Type::Nothing, - } - } else { - pipeline.elements.remove(0).expr - } - }; - - if let Some(err) = working_set.parse_errors.first() { - return Err(ShellError::GenericError { - error: "error when parsing nuon text".into(), - msg: "could not parse nuon text".into(), - span: None, - help: None, - inner: vec![ShellError::OutsideSpannedLabeledError { - src: input.to_string(), - error: "error when parsing".into(), - msg: err.to_string(), - span: err.span(), - }], - }); - } - - convert_to_value(expr, Span::unknown(), input) -} - #[cfg(test)] mod tests { use super::{is_table, mutate_value_cell}; @@ -1150,25 +707,4 @@ mod tests { Value::test_list(vec![Value::test_int(1), Value::test_int(2)]) ); } - - #[test] - fn from_nuon() { - assert_eq!(super::from_nuon(""), Ok(Value::test_nothing())); - assert_eq!(super::from_nuon("{}"), Ok(Value::test_record(record!()))); - assert_eq!( - super::from_nuon("{a: 123}"), - Ok(Value::test_record(record!("a" =>Value::test_int(123)))) - ); - assert_eq!( - super::from_nuon("[1, 2, 3]"), - Ok(Value::test_list(vec![ - Value::test_int(1), - Value::test_int(2), - Value::test_int(3) - ])), - ); - assert!(super::from_nuon("{invalid").is_err()); - - assert!(super::from_nuon(include_str!("../../examples/config/default.nuon")).is_ok()); - } }