From e68d347a75693d0d1947369c1105ee900e9320c3 Mon Sep 17 00:00:00 2001 From: Antoine Stevan <44101798+amtoine@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:46:00 +0200 Subject: [PATCH] allow editing any Nushell `Value` (#56) uses `nuon::from_nuon` and `nuon::to_nuon` to allow the editor to edit any value. --- Cargo.toml | 4 +- README.md | 5 +- src/app.rs | 16 +--- src/edit.rs | 245 +++++++++++++++++++++++++++++++++++++------------ src/handler.rs | 21 +++-- 5 files changed, 207 insertions(+), 84 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 72c2a31..c435cf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,14 +6,12 @@ name = "nu_plugin_explore" anyhow = "1.0.73" console = "0.15.7" crossterm = "0.27.0" +nuon = { git = "https://github.com/nushell/nushell", rev = "55edef5ddaf3d3d55290863446c2dd50c012e9bc", package = "nuon" } 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"] } diff --git a/README.md b/README.md index f457af6..04ed8a2 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,10 @@ in order to help, you can have a look at - [x] show true tables as such - [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] support for editing cells in INSERT mode - [x] string cells - - [ ] other simple cells + - [x] other simple cells + - [x] all the cells - [x] detect if a string is of a particular type, path, URL, ... ## internal diff --git a/src/app.rs b/src/app.rs index 6283211..521bce2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -105,21 +105,11 @@ impl App { self.mode = Mode::Bottom; } - pub(super) fn enter_editor(&mut self) -> Result<(), String> { + pub(super) fn enter_editor(&mut self) { let value = self.value_under_cursor(None); - if matches!(value, Value::String { .. }) { - self.mode = Mode::Insert; - self.editor = Editor::from_value(&value); - - Ok(()) - } else { - // TODO: support more diverse cell edition - Err(format!( - "can only edit string cells, found {}", - value.get_type() - )) - } + self.mode = Mode::Insert; + self.editor = Editor::from_value(&value); } pub(crate) fn value_under_cursor(&self, alternate_cursor: Option) -> Value { diff --git a/src/edit.rs b/src/edit.rs index f461819..a115c6c 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -1,4 +1,5 @@ use crossterm::event::KeyCode; +use nuon::{from_nuon, to_nuon}; use ratatui::{ prelude::Rect, style::Style, @@ -17,6 +18,13 @@ pub struct Editor { width: usize, } +#[derive(Debug, PartialEq)] +pub enum EditorTransition { + Continue, + Quit, + Value(Value), +} + impl Editor { /// set the width of the editor /// @@ -27,7 +35,8 @@ impl Editor { pub(super) fn from_value(value: &Value) -> Self { Self { - buffer: value.to_expanded_string(" ", &nu_protocol::Config::default()), + // NOTE: `value` should be a valid [`Value`] and thus the conversion should never fail + buffer: to_nuon(value, true, None, None, None).unwrap(), cursor_position: (0, 0), width: 0, } @@ -107,7 +116,7 @@ impl Editor { self.delete_char(0); } - pub(super) fn handle_key(&mut self, key: &KeyCode) -> Option> { + pub(super) fn handle_key(&mut self, key: &KeyCode) -> Result { match key { KeyCode::Left => self.move_cursor_left(), KeyCode::Right => self.move_cursor_right(), @@ -116,15 +125,15 @@ impl Editor { KeyCode::Char(c) => self.enter_char(*c), KeyCode::Backspace => self.delete_char_before_cursor(), KeyCode::Delete => self.delete_char_under_cursor(), - KeyCode::Enter => { - let val = Value::string(self.buffer.clone(), Span::unknown()); - return Some(Some(val)); - } - KeyCode::Esc => return Some(None), + KeyCode::Enter => match from_nuon(&self.buffer, Some(Span::unknown())) { + Ok(val) => return Ok(EditorTransition::Value(val)), + Err(err) => return Err(format!("could not convert back from NUON: {}", err)), + }, + KeyCode::Esc => return Ok(EditorTransition::Quit), _ => {} } - None + Ok(EditorTransition::Continue) } pub(super) fn render(&self, frame: &mut Frame, config: &Config) { @@ -171,68 +180,188 @@ mod tests { use crossterm::event::KeyCode; use nu_protocol::Value; - use super::Editor; + use super::{Editor, EditorTransition}; #[test] fn edit_cells() { let mut editor = Editor::default(); editor.set_width(10 + 2); + editor.buffer = r#""""#.to_string(); + // NOTE: for the NUON conversion to work, the test string buffer needs to be wrapped in + // parentheses. + // in order not to make the strokes clunky, the quotes are added in the for loop below and + // are implicite in the strokes below. let strokes = vec![ - (KeyCode::Enter, "", Some(Some(Value::test_string("")))), - (KeyCode::Char('a'), "a", None), - (KeyCode::Char('b'), "ab", None), - (KeyCode::Char('c'), "abc", None), - (KeyCode::Char('d'), "abcd", None), - (KeyCode::Char('e'), "abcde", None), - (KeyCode::Left, "abcde", None), - (KeyCode::Char('f'), "abcdfe", None), - (KeyCode::Left, "abcdfe", None), - (KeyCode::Left, "abcdfe", None), - (KeyCode::Char('g'), "abcgdfe", None), - (KeyCode::Right, "abcgdfe", None), - (KeyCode::Right, "abcgdfe", None), - (KeyCode::Right, "abcgdfe", None), - (KeyCode::Up, "abcgdfe", None), - (KeyCode::Down, "abcgdfe", None), - (KeyCode::Char('h'), "abcgdfeh", None), - (KeyCode::Char('i'), "abcgdfehi", None), - (KeyCode::Char('j'), "abcgdfehij", None), - (KeyCode::Char('k'), "abcgdfehijk", None), - (KeyCode::Char('l'), "abcgdfehijkl", None), - (KeyCode::Up, "abcgdfehijkl", None), - (KeyCode::Char('m'), "abmcgdfehijkl", None), - (KeyCode::Down, "abmcgdfehijkl", None), - (KeyCode::Left, "abmcgdfehijkl", None), - (KeyCode::Char('n'), "abmcgdfehijknl", None), - (KeyCode::Left, "abmcgdfehijknl", None), - (KeyCode::Left, "abmcgdfehijknl", None), - (KeyCode::Left, "abmcgdfehijknl", None), - (KeyCode::Left, "abmcgdfehijknl", None), - (KeyCode::Left, "abmcgdfehijknl", None), - (KeyCode::Char('o'), "abmcgdfeohijknl", None), - (KeyCode::Right, "abmcgdfeohijknl", None), - (KeyCode::Right, "abmcgdfeohijknl", None), ( KeyCode::Enter, + "", + Ok(EditorTransition::Value(Value::test_string(""))), + ), + (KeyCode::Right, "", Ok(EditorTransition::Continue)), + (KeyCode::Char('a'), "a", Ok(EditorTransition::Continue)), + (KeyCode::Char('b'), "ab", Ok(EditorTransition::Continue)), + (KeyCode::Char('c'), "abc", Ok(EditorTransition::Continue)), + (KeyCode::Char('d'), "abcd", Ok(EditorTransition::Continue)), + (KeyCode::Char('e'), "abcde", Ok(EditorTransition::Continue)), + (KeyCode::Left, "abcde", Ok(EditorTransition::Continue)), + (KeyCode::Char('f'), "abcdfe", Ok(EditorTransition::Continue)), + (KeyCode::Left, "abcdfe", Ok(EditorTransition::Continue)), + (KeyCode::Left, "abcdfe", Ok(EditorTransition::Continue)), + ( + KeyCode::Char('g'), + "abcgdfe", + Ok(EditorTransition::Continue), + ), + (KeyCode::Right, "abcgdfe", Ok(EditorTransition::Continue)), + (KeyCode::Right, "abcgdfe", Ok(EditorTransition::Continue)), + (KeyCode::Right, "abcgdfe", Ok(EditorTransition::Continue)), + (KeyCode::Up, "abcgdfe", Ok(EditorTransition::Continue)), + (KeyCode::Down, "abcgdfe", Ok(EditorTransition::Continue)), + ( + KeyCode::Char('h'), + "abcgdfeh", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Char('i'), + "abcgdfehi", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Char('j'), + "abcgdfehij", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Char('k'), + "abcgdfehijk", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Char('l'), + "abcgdfehijkl", + Ok(EditorTransition::Continue), + ), + (KeyCode::Up, "abcgdfehijkl", Ok(EditorTransition::Continue)), + ( + KeyCode::Char('m'), + "abmcgdfehijkl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Down, + "abmcgdfehijkl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Left, + "abmcgdfehijkl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Char('n'), + "abmcgdfehijknl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Left, + "abmcgdfehijknl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Left, + "abmcgdfehijknl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Left, + "abmcgdfehijknl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Left, + "abmcgdfehijknl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Left, + "abmcgdfehijknl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Char('o'), "abmcgdfeohijknl", - Some(Some(Value::test_string("abmcgdfeohijknl"))), - ), - (KeyCode::Right, "abmcgdfeohijknl", None), - (KeyCode::Right, "abmcgdfeohijknl", None), - (KeyCode::Char('p'), "abmcgdfeohijkpnl", None), - (KeyCode::Backspace, "abmcgdfeohijknl", None), - (KeyCode::Backspace, "abmcgdfeohijnl", None), - (KeyCode::Backspace, "abmcgdfeohinl", None), - (KeyCode::Up, "abmcgdfeohinl", None), - (KeyCode::Delete, "amcgdfeohinl", None), - (KeyCode::Delete, "acgdfeohinl", None), - (KeyCode::Delete, "agdfeohinl", None), - (KeyCode::Esc, "agdfeohinl", Some(None)), + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Right, + "abmcgdfeohijknl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Right, + "abmcgdfeohijknl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Enter, + "abmcgdfeohijknl", + Ok(EditorTransition::Value(Value::test_string( + "abmcgdfeohijknl", + ))), + ), + ( + KeyCode::Right, + "abmcgdfeohijknl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Right, + "abmcgdfeohijknl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Char('p'), + "abmcgdfeohijkpnl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Backspace, + "abmcgdfeohijknl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Backspace, + "abmcgdfeohijnl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Backspace, + "abmcgdfeohinl", + Ok(EditorTransition::Continue), + ), + (KeyCode::Up, "abmcgdfeohinl", Ok(EditorTransition::Continue)), + ( + KeyCode::Delete, + "amcgdfeohinl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Delete, + "acgdfeohinl", + Ok(EditorTransition::Continue), + ), + ( + KeyCode::Delete, + "agdfeohinl", + Ok(EditorTransition::Continue), + ), + (KeyCode::Esc, "agdfeohinl", Ok(EditorTransition::Quit)), ( KeyCode::Enter, "agdfeohinl", - Some(Some(Value::test_string("agdfeohinl"))), + Ok(EditorTransition::Value(Value::test_string("agdfeohinl"))), ), ]; @@ -240,7 +369,7 @@ mod tests { let result = editor.handle_key(&key); assert_eq!(result, expected); - assert_eq!(editor.buffer, expected_buffer.to_string()); + assert_eq!(editor.buffer, format!(r#""{}""#, expected_buffer)); } } } diff --git a/src/handler.rs b/src/handler.rs index e7c6805..1379757 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -7,6 +7,7 @@ use nu_protocol::{ use crate::{ app::{App, Mode}, + edit::EditorTransition, navigation::Direction, nu::value::transpose, }; @@ -73,10 +74,8 @@ impl App { } 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)), - } + self.enter_editor(); + return Ok(TransitionResult::Continue); } else if key_event == config.keybindings.peek { self.mode = Mode::Peeking; return Ok(TransitionResult::Continue); @@ -168,15 +167,16 @@ impl App { } match self.editor.handle_key(&key_event.code) { - Some(Some(v)) => { + Ok(EditorTransition::Value(v)) => { self.mode = Mode::Normal; return Ok(TransitionResult::Mutate(v, self.position.clone())); } - Some(None) => { + Ok(EditorTransition::Quit) => { self.mode = Mode::Normal; return Ok(TransitionResult::Continue); } - None => return Ok(TransitionResult::Continue), + Ok(EditorTransition::Continue) => return Ok(TransitionResult::Continue), + Err(err) => return Ok(TransitionResult::Error(err)), } } Mode::Peeking => { @@ -321,7 +321,7 @@ mod tests { let transitions = vec![ (keybindings.insert, false), - (keybindings.quit, true), + (keybindings.quit, false), (keybindings.normal, false), (keybindings.quit, true), (keybindings.peek, false), @@ -331,6 +331,11 @@ mod tests { for (key, exit) in transitions { let mode = app.mode.clone(); + // NOTE: yeah this is a bit clunky... + if app.mode == Mode::Insert { + app.editor.set_width(10); + } + let result = app.handle_key_events(key, 0).unwrap(); if exit {