From e2395739032dc028ed2c8b801f9072ca2befd618 Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Mon, 17 Oct 2022 01:27:22 -0400 Subject: [PATCH 1/6] Shift Arrow Selection Fixed issues where shift + arrow keys would not select text. This creates an issue with cursor movements after using shift + arrow keys. Further cursor movements retain the selection mode and allow selecting. Cursor movements should clear the active selection. However, this would invalidate the ability for selecting via marking selection (v). Selection marking will be refactored, which will fix this inconsistency. --- abacus-ui/src/data/editor_data.rs | 40 +++++++++++-------------------- abacus-ui/src/editor.rs | 1 - 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/abacus-ui/src/data/editor_data.rs b/abacus-ui/src/data/editor_data.rs index 09a1e53..eb69629 100644 --- a/abacus-ui/src/data/editor_data.rs +++ b/abacus-ui/src/data/editor_data.rs @@ -201,15 +201,8 @@ impl EditorData { } pub fn select_up(&mut self) { - let line_idx = self.content.char_to_line(self.cursor_pos); - - if line_idx > 0 { - let start_of_current_line = self.content.line_to_char(line_idx); - let line_pos = self.cursor_pos - start_of_current_line; - let up_line_start = self.content.line_to_char(line_idx - 1); - let up_line = self.content.line(line_idx - 1); - self.cursor_pos = up_line_start + line_pos.min(up_line.len_chars() - 1); - } + self.start_selection(); + self.cursor_up(); } pub fn cursor_down(&mut self) { @@ -225,17 +218,8 @@ impl EditorData { } pub fn select_down(&mut self) { - //12 -> 11 - let line_idx = self.content.char_to_line(self.cursor_pos); - if line_idx < self.content.len_lines() - 1 { - let start_of_current_line = self.content.line_to_char(line_idx); - let line_pos = self.cursor_pos - start_of_current_line; - let start_of_next_line = self.content.line_to_char(line_idx + 1) + 1; - let next_line_len = self.content.line(line_idx + 1).len_chars(); - - self.cursor_pos = - (start_of_next_line + line_pos.min(next_line_len)).min(start_of_next_line); - } + self.start_selection(); + self.cursor_down(); } pub fn cursor_right(&mut self) { @@ -287,15 +271,13 @@ impl EditorData { } pub fn select_left(&mut self) { - if self.cursor_pos > 0 { - self.cursor_pos -= 1 - } + self.start_selection(); + self.cursor_left(); } pub fn select_right(&mut self) { - if self.cursor_pos < self.content.len_chars() { - self.cursor_pos += 1; - } + self.start_selection(); + self.cursor_right(); } pub fn current_line(&self) -> ropey::RopeSlice { @@ -310,6 +292,12 @@ impl EditorData { .unwrap() } + pub fn start_selection(&mut self) { + if self.selection_pos.is_none() { + self.selection_pos = Some(self.cursor_pos); + } + } + pub fn current_line_index(&self) -> usize { self.content.char_to_line(self.cursor_pos) } diff --git a/abacus-ui/src/editor.rs b/abacus-ui/src/editor.rs index 5be335d..182c381 100644 --- a/abacus-ui/src/editor.rs +++ b/abacus-ui/src/editor.rs @@ -414,7 +414,6 @@ impl Widget for AbacusEditor { druid::keyboard_types::Key::ArrowUp if e.mods.shift() => { data.select_up(); - data.deselect(); } druid::keyboard_types::Key::ArrowDown if e.mods.shift() => { data.select_down(); From 3b6f7cdbf4e6096e02ca2ec58d9ea8b52fc07b5c Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Mon, 17 Oct 2022 01:57:56 -0400 Subject: [PATCH 2/6] Implemented Change Word Change word forward and backward were implemented in the editor_data and bound in the editor to "cw" and "cb" respectively. Scan word forward and scan word backward now utilize a shared function that gets the scan position. This is now also used for delete word forward/backward, so they can share logic Updated keybinds in README --- abacus-ui/README.org | 3 ++ abacus-ui/src/data/editor_data.rs | 63 +++++++++++++++++++++++-------- abacus-ui/src/editor.rs | 12 ++++++ 3 files changed, 62 insertions(+), 16 deletions(-) diff --git a/abacus-ui/README.org b/abacus-ui/README.org index dea358a..e1db715 100644 --- a/abacus-ui/README.org +++ b/abacus-ui/README.org @@ -32,6 +32,9 @@ In normal mode the cursor is a block and functional keybinds can be used for mov | w | Scan forward a word | | v | Mark selection | | x | Delete current character or selection | +| dd | Delete the current line | +| cw | Change word forward | +| cb | Change word backward | ** Insert mode In insert mode the cursor is a line and text can be edited. To return to normal mode use the ESC key. diff --git a/abacus-ui/src/data/editor_data.rs b/abacus-ui/src/data/editor_data.rs index eb69629..24c590a 100644 --- a/abacus-ui/src/data/editor_data.rs +++ b/abacus-ui/src/data/editor_data.rs @@ -104,37 +104,63 @@ impl EditorData { self.deselect(); } - pub fn word_scan_forward(&mut self) { - while self.cursor_pos < self.content.len_chars() { - self.cursor_pos += 1; - if !self - .current_char() - .map(|c| c.is_alphanumeric()) - .unwrap_or(false) + pub fn get_word_scan_forward_position(&mut self, skip_first: bool) -> usize { + if self.cursor_pos < self.content.len_chars() { + for (idx, ch) in self + .content + .chars_at(self.cursor_pos) + .skip(if skip_first { 1 } else { 0 }) + .enumerate() { - break; + if !ch.is_alphanumeric() { + return (idx + self.cursor_pos + 2).min(self.content.len_chars()); + } } - self.deselect(); + self.content.len_chars() + } else { + self.content.len_chars() } } - pub fn word_scan_backward(&mut self) { - if self.cursor_pos > 0 { - self.cursor_pos -= 1; + pub fn get_word_scan_backward_position(&mut self, skip_first: bool) -> usize { + let mut pos = self.cursor_pos; + if pos > 0 && skip_first { + pos -= 1; } - while self.cursor_pos > 0 { + while pos > 0 { if !self .content - .get_char(self.cursor_pos - 1) + .get_char(pos - 1) .map(|c| c.is_alphanumeric()) .unwrap_or(false) { break; } else { - self.cursor_pos -= 1; + pos -= 1; } - self.deselect(); } + pos + } + + pub fn delete_word_forward(&mut self) { + let end = self.get_word_scan_forward_position(false); + self.content.remove(self.cursor_pos..end); + } + + pub fn delete_word_backward(&mut self) { + let start = self.get_word_scan_backward_position(true); + self.content.remove(start..self.cursor_pos); + self.cursor_pos = start; + } + + pub fn word_scan_forward(&mut self) { + self.cursor_pos = self.get_word_scan_forward_position(true); + self.deselect(); + } + + pub fn word_scan_backward(&mut self) { + self.cursor_pos = self.get_word_scan_backward_position(true); + self.deselect(); } pub fn delete_char_forward(&mut self) { @@ -265,6 +291,11 @@ impl EditorData { self.deselect(); } + pub fn insert_mode(&mut self) { + self.mode = EditMode::Insert; + self.deselect(); + } + pub fn select_all(&mut self) { self.selection_pos = Some(0); self.cursor_pos = self.content.len_chars(); diff --git a/abacus-ui/src/editor.rs b/abacus-ui/src/editor.rs index 182c381..2fb6a80 100644 --- a/abacus-ui/src/editor.rs +++ b/abacus-ui/src/editor.rs @@ -36,6 +36,8 @@ mod keymap { pub const WORD_FORWARD: &str = "w"; pub const WORD_BACK: &str = "b"; pub const SELECT_MODE: &str = "v"; + pub const CHANGE_WORD_FORWARD: &str = "cw"; + pub const CHANGE_WORD_BACKWARD: &str = "cb"; #[derive(Debug, Default)] pub struct KeyList { @@ -63,6 +65,8 @@ mod keymap { WORD_FORWARD, WORD_BACK, SELECT_MODE, + CHANGE_WORD_FORWARD, + CHANGE_WORD_BACKWARD, ], } } @@ -360,6 +364,14 @@ impl Widget for AbacusEditor { Some(keymap::SELECT_MODE) => { data.selection_pos = Some(data.cursor_pos); } + Some(keymap::CHANGE_WORD_FORWARD) => { + data.delete_word_forward(); + data.insert_mode(); + } + Some(keymap::CHANGE_WORD_BACKWARD) => { + data.delete_word_backward(); + data.insert_mode(); + } _ => {} } } From 594387656902ba0f61f6447b698791f7a045d03b Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Mon, 17 Oct 2022 15:11:29 -0400 Subject: [PATCH 3/6] Added yank editor commands Yank, yank line, and paste were added to normal mode keybinds --- abacus-ui/README.org | 4 +++ abacus-ui/src/editor.rs | 56 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/abacus-ui/README.org b/abacus-ui/README.org index e1db715..67f7569 100644 --- a/abacus-ui/README.org +++ b/abacus-ui/README.org @@ -35,6 +35,10 @@ In normal mode the cursor is a block and functional keybinds can be used for mov | dd | Delete the current line | | cw | Change word forward | | cb | Change word backward | +| y | Yank selection | +| Y | Yank line | +| p | Paste line at cursor | +| P | Paste line below | ** Insert mode In insert mode the cursor is a line and text can be edited. To return to normal mode use the ESC key. diff --git a/abacus-ui/src/editor.rs b/abacus-ui/src/editor.rs index 2fb6a80..9a35092 100644 --- a/abacus-ui/src/editor.rs +++ b/abacus-ui/src/editor.rs @@ -38,6 +38,10 @@ mod keymap { pub const SELECT_MODE: &str = "v"; pub const CHANGE_WORD_FORWARD: &str = "cw"; pub const CHANGE_WORD_BACKWARD: &str = "cb"; + pub const YANK: &str = "y"; + pub const YANK_LINE: &str = "Y"; + pub const PASTE: &str = "p"; + pub const PASTE_LINE: &str = "P"; #[derive(Debug, Default)] pub struct KeyList { @@ -67,6 +71,10 @@ mod keymap { SELECT_MODE, CHANGE_WORD_FORWARD, CHANGE_WORD_BACKWARD, + YANK, + YANK_LINE, + PASTE, + PASTE_LINE, ], } } @@ -153,7 +161,7 @@ impl AbacusEditor { } if data.mode == EditMode::Normal { - let char_rect = if data.content.len_chars() == 0 { + let char_rect = if data.cursor_pos == 0 { let rects = layout.rects_for_range(0..1); rects.first().cloned() } else if data.current_char() == Some('\n') @@ -362,7 +370,9 @@ impl Widget for AbacusEditor { data.word_scan_backward(); } Some(keymap::SELECT_MODE) => { - data.selection_pos = Some(data.cursor_pos); + data.cursor_to_end_of_line(); + data.start_selection(); + data.cursor_to_start_of_line(); } Some(keymap::CHANGE_WORD_FORWARD) => { data.delete_word_forward(); @@ -372,6 +382,48 @@ impl Widget for AbacusEditor { data.delete_word_backward(); data.insert_mode(); } + Some(keymap::YANK) => { + if data.selection_pos.is_some() { + let mut cb: clipboard::ClipboardContext = + clipboard::ClipboardProvider::new().unwrap(); + if let Some(slice) = data.content.get_slice(data.select_range()) + { + let _ = cb.set_contents(slice.to_string()); + } + } + data.deselect(); + } + Some(keymap::YANK_LINE) => { + data.cursor_to_start_of_line(); + data.start_selection(); + data.cursor_to_end_of_line(); + if data.selection_pos.is_some() { + let mut cb: clipboard::ClipboardContext = + clipboard::ClipboardProvider::new().unwrap(); + if let Some(slice) = data.content.get_slice(data.select_range()) + { + let _ = cb.set_contents(slice.to_string()); + } + } + data.deselect(); + } + Some(keymap::PASTE) => { + let mut cb: clipboard::ClipboardContext = + clipboard::ClipboardProvider::new().unwrap(); + data.push_str(&cb.get_contents().unwrap_or_default()); + ctx.request_layout(); + } + Some(keymap::PASTE_LINE) => { + if data.selection_pos.is_some() { + data.delete_char_back(); + } + let mut cb: clipboard::ClipboardContext = + clipboard::ClipboardProvider::new().unwrap(); + data.cursor_to_end_of_line(); + data.push_str("\n"); + data.push_str(&cb.get_contents().unwrap_or_default()); + ctx.request_layout(); + } _ => {} } } From 3e4b69ee4944cdb7813125ae31f36ea40c67e885 Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Wed, 19 Oct 2022 20:24:17 -0400 Subject: [PATCH 4/6] Added Simple Series Aggregation Functions Added Series.max, Series.min, Series.head, Series.tail, Series.mean Added Range collection (start..end).collect() now produces an array --- abacus-core/src/dataframe.rs | 74 +++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/abacus-core/src/dataframe.rs b/abacus-core/src/dataframe.rs index d8edf37..c4d1675 100644 --- a/abacus-core/src/dataframe.rs +++ b/abacus-core/src/dataframe.rs @@ -25,8 +25,14 @@ pub fn setup_engine(engine: &mut rhai::Engine) { engine.register_fn("to_series", script_functions::to_series_unnamed); engine.register_fn("series", script_functions::series_unnamed); engine.register_fn("head", Series::s_head); + engine.register_fn("head", Series::s_head_len); + engine.register_fn("tail", Series::s_tail); + engine.register_fn("tail", Series::s_tail_len); engine.register_fn("sort", Series::s_sort); engine.register_fn("sum", Series::s_sum); + engine.register_fn("mean", Series::s_mean); + engine.register_fn("min", Series::s_min); + engine.register_fn("max", Series::s_max); engine.register_fn("add", Series::s_op_add); engine.register_fn("+", Series::add); @@ -67,6 +73,7 @@ pub fn setup_engine(engine: &mut rhai::Engine) { engine.register_fn("min", script_functions::min); engine.register_fn("max", script_functions::max); engine.register_fn("first", script_functions::first); + engine.register_fn("collect", script_functions::range_to_array); let _ = engine.register_custom_operator("gt", 200); let _ = engine.register_custom_operator("gte", 200); let _ = engine.register_custom_operator("<<", 200); @@ -217,9 +224,22 @@ impl Add for Series { } impl Series { - pub fn s_head(&mut self, n: i64) -> Series { + pub fn s_head(&mut self) -> Series { + Series(self.0.head(Some(1))) + } + + pub fn s_head_len(&mut self, n: i64) -> Series { Series(self.0.head(Some(n as usize))) } + + pub fn s_tail(&mut self) -> Series { + Series(self.0.tail(Some(self.0.len() - 1))) + } + + pub fn s_tail_len(&mut self, size: i64) -> Series { + Series(self.0.tail(Some(size as usize))) + } + pub fn s_sort(&mut self, reverse: bool) -> Series { Series(self.0.sort(reverse)) } @@ -228,6 +248,18 @@ impl Series { self.0.sum().unwrap_or_default() } + pub fn s_mean(&mut self) -> f64 { + self.0.mean().unwrap_or_default() + } + + pub fn s_min(&mut self) -> f64 { + self.0.min().unwrap_or_default() + } + + pub fn s_max(&mut self) -> f64 { + self.0.max().unwrap_or_default() + } + pub fn s_get(&mut self, n: i64) -> ScriptResult { let value = self.get(n as usize); match value { @@ -436,6 +468,10 @@ mod script_functions { pub fn gte_op(a: &str, b: rhai::Dynamic) -> DataFrameExpression { DataFrameExpression(polars::prelude::col(a).gt_eq(DataFrameExpression::from(b))) } + + pub fn range_to_array(range: std::ops::Range) -> rhai::Array { + range.map(Dynamic::from).collect() + } } fn implementation_df_select( @@ -690,4 +726,40 @@ from data ["age"] : ["age" gt 18]; .collect::>(); assert_eq!(s, vec![Some(22), Some(32)]); } + + #[test] + pub fn test_range_to_array() { + let res = process(r#"series((1..4).collect())"#); + let s = res + .into_series() + .i64() + .unwrap() + .into_iter() + .collect::>(); + assert_eq!(s, vec![Some(1), Some(2), Some(3)]); + } + + #[test] + pub fn test_series_min() { + let res = process(r#"series((2..4).collect()).min()"#); + dbg!(&res); + let s = res.into_scalar().as_float().unwrap(); + assert_eq!(s, 2.0); + } + + #[test] + pub fn test_series_max() { + let res = process(r#"series((2..4).collect()).max()"#); + dbg!(&res); + let s = res.into_scalar().as_float().unwrap(); + assert_eq!(s, 3.0); + } + + #[test] + pub fn test_series_mean() { + let res = process(r#"series((1..51).collect()).mean()"#); + dbg!(&res); + let s = res.into_scalar().as_float().unwrap(); + assert_eq!(s, 25.5); + } } From 6d67b476c44a6c32c9db391132cd31d201c7fc5a Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Wed, 19 Oct 2022 20:35:36 -0400 Subject: [PATCH 5/6] Initial Editor Focus Initial editor is now focused after launch --- abacus-ui/src/editor.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/abacus-ui/src/editor.rs b/abacus-ui/src/editor.rs index 9a35092..d462705 100644 --- a/abacus-ui/src/editor.rs +++ b/abacus-ui/src/editor.rs @@ -545,6 +545,9 @@ impl Widget for AbacusEditor { ctx.request_paint(); } } + Event::WindowConnected => { + ctx.request_focus(); + } _ => {} } } From 5e9f0db4bdb6fe55080b685a163e7e86066ae2d4 Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Wed, 19 Oct 2022 21:41:47 -0400 Subject: [PATCH 6/6] Linting fixes --- abacus-core/src/dataframe.rs | 12 ++++++------ abacus-core/src/engine.rs | 2 -- abacus-ui/src/data/app_data.rs | 20 -------------------- 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/abacus-core/src/dataframe.rs b/abacus-core/src/dataframe.rs index 7e402ba..12b4ef5 100644 --- a/abacus-core/src/dataframe.rs +++ b/abacus-core/src/dataframe.rs @@ -743,23 +743,23 @@ from data ["age"] : ["age" gt 18]; pub fn test_series_min() { let res = process(r#"series((2..4).collect()).min()"#); dbg!(&res); - let s = res.into_scalar().as_float().unwrap(); - assert_eq!(s, 2.0); + let s = res.into_scalar(); + assert_eq!(s, "2.0"); } #[test] pub fn test_series_max() { let res = process(r#"series((2..4).collect()).max()"#); dbg!(&res); - let s = res.into_scalar().as_float().unwrap(); - assert_eq!(s, 3.0); + let s = res.into_scalar(); + assert_eq!(s, "3.0"); } #[test] pub fn test_series_mean() { let res = process(r#"series((1..51).collect()).mean()"#); dbg!(&res); - let s = res.into_scalar().as_float().unwrap(); - assert_eq!(s, 25.5); + let s = res.into_scalar(); + assert_eq!(s, "25.5"); } } diff --git a/abacus-core/src/engine.rs b/abacus-core/src/engine.rs index 3e89a38..f864b9b 100644 --- a/abacus-core/src/engine.rs +++ b/abacus-core/src/engine.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use crate::dataframe; #[derive(Debug)] diff --git a/abacus-ui/src/data/app_data.rs b/abacus-ui/src/data/app_data.rs index d2d0b5a..097b65d 100644 --- a/abacus-ui/src/data/app_data.rs +++ b/abacus-ui/src/data/app_data.rs @@ -46,26 +46,6 @@ impl Default for AppData { } } -impl AppData { - pub fn process(&mut self) { - let mut engine = abacus_core::Engine::default(); - - for mut block in self.blocks.iter_mut() { - let start = std::time::Instant::now(); - block.output = engine.process_script(&block.editor_data.content.to_string()); - println!("block executed in {}", start.elapsed().as_millis()); - } - } - pub fn process_block(&mut self, index: usize) { - let mut engine = abacus_core::Engine::default(); - if let Some(block) = self.blocks.get_mut(index) { - let start = std::time::Instant::now(); - block.output = engine.process_script(&block.editor_data.content.to_string()); - println!("block executed in {}", start.elapsed().as_millis()); - } - } -} - #[derive(Clone, Data, Lens, Default, PartialEq, Debug)] pub struct Block { pub name: String,