From 6d59885200b732f58eca2ac323538a69884b45b4 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 20 Oct 2023 09:45:25 -0600 Subject: [PATCH 01/46] ViEditor: draw syntax background color --- src/edit/vi.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 4d5fd89..69e0403 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -242,6 +242,9 @@ impl<'a> Edit for ViEditor<'a> { ) where F: FnMut(i32, i32, u32, u32, Color), { + let size = self.buffer().size(); + f(0, 0, size.0 as u32, size.1 as u32, self.background_color()); + let font_size = self.buffer().metrics().font_size; let line_height = self.buffer().metrics().line_height; From c6e4f9d04c14054cf2ea2c8728f2bd1d16353ef0 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 20 Oct 2023 09:46:21 -0600 Subject: [PATCH 02/46] ViEditor: add passthrough mode (disables vi features) --- src/edit/vi.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 69e0403..e479d69 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -9,6 +9,7 @@ use crate::{ #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Mode { + Passthrough, Normal, Insert, Command, @@ -50,6 +51,15 @@ impl<'a> ViEditor<'a> { pub fn foreground_color(&self) -> Color { self.editor.foreground_color() } + + /// Set passthrough mode (true will turn off vi features) + pub fn set_passthrough(&mut self, passthrough: bool) { + if passthrough { + self.mode = Mode::Passthrough; + } else { + self.mode = Mode::Normal; + } + } } impl<'a> Edit for ViEditor<'a> { @@ -97,6 +107,7 @@ impl<'a> Edit for ViEditor<'a> { let old_mode = self.mode; match self.mode { + Mode::Passthrough => self.editor.action(font_system, action), Mode::Normal => match action { Action::Insert(c) => match c { // Enter insert mode after cursor @@ -369,6 +380,7 @@ impl<'a> Edit for ViEditor<'a> { cursor_glyph_opt(&self.cursor()) { let block_cursor = match self.mode { + Mode::Passthrough => false, Mode::Normal => true, Mode::Insert => false, _ => true, /*TODO: determine block cursor in other modes*/ From 4adcbf6784745774fb52919c97e67716e92670aa Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 20 Oct 2023 10:25:46 -0600 Subject: [PATCH 03/46] Editor: add SoftHome action to skip blank space --- src/edit/editor.rs | 11 +++++++++++ src/edit/mod.rs | 2 ++ 2 files changed, 13 insertions(+) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 274e05b..1e8d325 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -426,6 +426,17 @@ impl Edit for Editor { self.set_layout_cursor(font_system, cursor); self.cursor_x_opt = None; } + Action::SoftHome => { + let line: &mut BufferLine = &mut self.buffer.lines[self.cursor.line]; + self.cursor.index = line + .text() + .unicode_word_indices() + .map(|(i, _)| i) + .next() + .unwrap_or(0); + self.buffer.set_redraw(true); + self.cursor_x_opt = None; + } Action::End => { let mut cursor = self.buffer.layout_cursor(&self.cursor); cursor.glyph = usize::max_value(); diff --git a/src/edit/mod.rs b/src/edit/mod.rs index 1c16746..a3173a7 100644 --- a/src/edit/mod.rs +++ b/src/edit/mod.rs @@ -35,6 +35,8 @@ pub enum Action { Down, /// Move cursor to start of line Home, + /// Move cursor to start of line, skipping whitespace + SoftHome, /// Move cursor to end of line End, /// Move cursor to start of paragraph From a29eefca5a6f67a500a5ba6e7d034b6f6dc3fda7 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 20 Oct 2023 10:26:17 -0600 Subject: [PATCH 04/46] ViEditor: implement I and ^ using SoftHome --- src/edit/vi.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index e479d69..d018238 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -143,8 +143,7 @@ impl<'a> Edit for ViEditor<'a> { } // Enter insert mode at start of line 'I' => { - //TODO: soft home, skip whitespace - self.editor.action(font_system, Action::Home); + self.editor.action(font_system, Action::SoftHome); self.mode = Mode::Insert; } // Create line after and enter insert mode @@ -203,8 +202,7 @@ impl<'a> Edit for ViEditor<'a> { // Go to end of line '$' => self.editor.action(font_system, Action::End), // Go to start of line after whitespace - //TODO: implement this - '^' => self.editor.action(font_system, Action::Home), + '^' => self.editor.action(font_system, Action::SoftHome), // Enter command mode ':' => { self.mode = Mode::Command; From 37789ccdf7759bf5deb3b3ae2a8794668db6856c Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 20 Oct 2023 12:34:03 -0600 Subject: [PATCH 05/46] ViEditor: expose current mode, add word stubs --- src/edit/vi.rs | 122 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 93 insertions(+), 29 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index d018238..4c6a87a 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -8,26 +8,30 @@ use crate::{ }; #[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum Mode { +pub enum ViMode { + /// Passthrough mode, disables vi features Passthrough, + /// Normal mode Normal, + /// Insert mode Insert, + /// Command mode Command, - Search, - SearchBackwards, + /// Search mode + Search { forwards: bool }, } #[derive(Debug)] pub struct ViEditor<'a> { editor: SyntaxEditor<'a>, - mode: Mode, + mode: ViMode, } impl<'a> ViEditor<'a> { pub fn new(editor: SyntaxEditor<'a>) -> Self { Self { editor, - mode: Mode::Normal, + mode: ViMode::Normal, } } @@ -55,11 +59,16 @@ impl<'a> ViEditor<'a> { /// Set passthrough mode (true will turn off vi features) pub fn set_passthrough(&mut self, passthrough: bool) { if passthrough { - self.mode = Mode::Passthrough; + self.mode = ViMode::Passthrough; } else { - self.mode = Mode::Normal; + self.mode = ViMode::Normal; } } + + /// Get current vi editing mode + pub fn mode(&self) -> ViMode { + self.mode + } } impl<'a> Edit for ViEditor<'a> { @@ -107,24 +116,34 @@ impl<'a> Edit for ViEditor<'a> { let old_mode = self.mode; match self.mode { - Mode::Passthrough => self.editor.action(font_system, action), - Mode::Normal => match action { + ViMode::Passthrough => self.editor.action(font_system, action), + ViMode::Normal => match action { Action::Insert(c) => match c { // Enter insert mode after cursor 'a' => { self.editor.action(font_system, Action::Right); - self.mode = Mode::Insert; + self.mode = ViMode::Insert; } // Enter insert mode at end of line 'A' => { self.editor.action(font_system, Action::End); - self.mode = Mode::Insert; + self.mode = ViMode::Insert; + } + // Previous word + 'b' => { + //TODO: WORD vs word, iterate by vi word rules + self.editor.action(font_system, Action::PreviousWord); + } + // Previous WORD + 'B' => { + //TODO: WORD vs word, iterate by vi word rules + self.editor.action(font_system, Action::PreviousWord); } // Change mode 'c' => { if self.editor.select_opt().is_some() { self.editor.action(font_system, Action::Delete); - self.mode = Mode::Insert; + self.mode = ViMode::Insert; } else { //TODO: change to next cursor movement } @@ -137,20 +156,30 @@ impl<'a> Edit for ViEditor<'a> { //TODO: delete to next cursor movement } } + // End of word + 'e' => { + //TODO: WORD vs word, iterate by vi word rules + self.editor.action(font_system, Action::NextWord); + } + // End of WORD + 'E' => { + //TODO: WORD vs word, iterate by vi word rules + self.editor.action(font_system, Action::NextWord); + } // Enter insert mode at cursor 'i' => { - self.mode = Mode::Insert; + self.mode = ViMode::Insert; } // Enter insert mode at start of line 'I' => { self.editor.action(font_system, Action::SoftHome); - self.mode = Mode::Insert; + self.mode = ViMode::Insert; } // Create line after and enter insert mode 'o' => { self.editor.action(font_system, Action::End); self.editor.action(font_system, Action::Enter); - self.mode = Mode::Insert; + self.mode = ViMode::Insert; } // Create line before and enter insert mode 'O' => { @@ -158,7 +187,7 @@ impl<'a> Edit for ViEditor<'a> { self.editor.action(font_system, Action::Enter); self.editor.shape_as_needed(font_system); // TODO: do not require this? self.editor.action(font_system, Action::Up); - self.mode = Mode::Insert; + self.mode = ViMode::Insert; } // Left 'h' => self.editor.action(font_system, Action::Left), @@ -169,7 +198,7 @@ impl<'a> Edit for ViEditor<'a> { // Up 'k' => self.editor.action(font_system, Action::Up), // Right - 'l' => self.editor.action(font_system, Action::Right), + 'l' | ' ' => self.editor.action(font_system, Action::Right), // Bottom of screen //TODO: 'L' => self.editor.action(Action::ScreenLow), // Middle of screen @@ -193,6 +222,16 @@ impl<'a> Edit for ViEditor<'a> { self.editor.action(font_system, Action::End); } } + // Next word + 'w' => { + //TODO: WORD vs word, iterate by vi word rules + self.editor.action(font_system, Action::NextWord); + } + // Next WORD + 'W' => { + //TODO: WORD vs word, iterate by vi word rules + self.editor.action(font_system, Action::NextWord); + } // Remove character at cursor 'x' => self.editor.action(font_system, Action::Delete), // Remove character before cursor @@ -205,35 +244,60 @@ impl<'a> Edit for ViEditor<'a> { '^' => self.editor.action(font_system, Action::SoftHome), // Enter command mode ':' => { - self.mode = Mode::Command; + self.mode = ViMode::Command; } // Enter search mode '/' => { - self.mode = Mode::Search; + self.mode = ViMode::Search { forwards: true }; } // Enter search backwards mode '?' => { - self.mode = Mode::SearchBackwards; + self.mode = ViMode::Search { forwards: false }; } _ => (), }, + // Go to start of next line + Action::Enter => { + self.editor.action(font_system, Action::Down); + self.editor.action(font_system, Action::SoftHome); + } + // Left + Action::Backspace => { + self.editor.action(font_system, Action::Left); + } _ => self.editor.action(font_system, action), }, - Mode::Insert => match action { + ViMode::Insert => match action { Action::Escape => { let cursor = self.cursor(); let layout_cursor = self.buffer().layout_cursor(&cursor); if layout_cursor.glyph > 0 { self.editor.action(font_system, Action::Left); } - self.mode = Mode::Normal; + self.mode = ViMode::Normal; } _ => self.editor.action(font_system, action), }, - _ => { - //TODO: other modes - self.mode = Mode::Normal; - } + ViMode::Command => match action { + Action::Escape => { + self.mode = ViMode::Normal; + } + Action::Enter => {} + Action::Insert(c) => match c { + _ => {} + }, + _ => self.editor.action(font_system, action), + }, + ViMode::Search { forwards } => match action { + Action::Escape => { + self.mode = ViMode::Normal; + } + Action::Enter => {} + Action::Insert(c) => match c { + _ => {} + }, + _ => self.editor.action(font_system, action), + }, } if self.mode != old_mode { @@ -378,9 +442,9 @@ impl<'a> Edit for ViEditor<'a> { cursor_glyph_opt(&self.cursor()) { let block_cursor = match self.mode { - Mode::Passthrough => false, - Mode::Normal => true, - Mode::Insert => false, + ViMode::Passthrough => false, + ViMode::Normal => true, + ViMode::Insert => false, _ => true, /*TODO: determine block cursor in other modes*/ }; From 7526fa972640c66909576125c310afad67023940 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 20 Oct 2023 13:54:54 -0600 Subject: [PATCH 06/46] Editor: Request redraw/scroll on set_cursor --- src/edit/editor.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 1e8d325..44d0ed0 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -86,7 +86,11 @@ impl Edit for Editor { } fn set_cursor(&mut self, cursor: Cursor) { - self.cursor = cursor; + if self.cursor != cursor { + self.cursor = cursor; + self.cursor_moved = true; + self.buffer.set_redraw(true); + } } fn select_opt(&self) -> Option { From c1e40363ab576c90edb7b78f1f257b3845558b1a Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 20 Oct 2023 13:55:18 -0600 Subject: [PATCH 07/46] ViEditor: implement search, capture commands --- src/edit/vi.rs | 129 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 112 insertions(+), 17 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 4c6a87a..cd8fcc3 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -7,7 +7,7 @@ use crate::{ SyntaxEditor, }; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum ViMode { /// Passthrough mode, disables vi features Passthrough, @@ -16,15 +16,16 @@ pub enum ViMode { /// Insert mode Insert, /// Command mode - Command, + Command { value: String }, /// Search mode - Search { forwards: bool }, + Search { value: String, forwards: bool }, } #[derive(Debug)] pub struct ViEditor<'a> { editor: SyntaxEditor<'a>, mode: ViMode, + search_opt: Option<(String, bool)>, } impl<'a> ViEditor<'a> { @@ -32,6 +33,7 @@ impl<'a> ViEditor<'a> { Self { editor, mode: ViMode::Normal, + search_opt: None, } } @@ -66,8 +68,66 @@ impl<'a> ViEditor<'a> { } /// Get current vi editing mode - pub fn mode(&self) -> ViMode { - self.mode + pub fn mode(&self) -> &ViMode { + &self.mode + } + + fn search(&mut self, inverted: bool) { + let (search, mut forwards) = match &self.search_opt { + Some(some) => some, + None => return, + }; + + if inverted { + forwards = !forwards; + } + + let mut cursor = self.cursor(); + let start_line = cursor.line; + if forwards { + while cursor.line < self.buffer().lines.len() { + if let Some(index) = self.buffer().lines[cursor.line] + .text() + .match_indices(search.as_str()) + .filter_map(|(i, _)| { + if cursor.line != start_line || i > cursor.index { + Some(i) + } else { + None + } + }) + .next() + { + cursor.index = index; + self.set_cursor(cursor); + return; + } + + cursor.line += 1; + } + } else { + cursor.line += 1; + while cursor.line > 0 { + cursor.line -= 1; + + if let Some(index) = self.buffer().lines[cursor.line] + .text() + .rmatch_indices(search.as_str()) + .filter_map(|(i, _)| { + if cursor.line != start_line || i < cursor.index { + Some(i) + } else { + None + } + }) + .next() + { + cursor.index = index; + self.set_cursor(cursor); + return; + } + } + } } } @@ -113,7 +173,7 @@ impl<'a> Edit for ViEditor<'a> { } fn action(&mut self, font_system: &mut FontSystem, action: Action) { - let old_mode = self.mode; + let old_mode = self.mode.clone(); match self.mode { ViMode::Passthrough => self.editor.action(font_system, action), @@ -203,6 +263,10 @@ impl<'a> Edit for ViEditor<'a> { //TODO: 'L' => self.editor.action(Action::ScreenLow), // Middle of screen //TODO: 'M' => self.editor.action(Action::ScreenMiddle), + // Next search item + 'n' => self.search(false), + // Previous search item + 'N' => self.search(true), // Enter visual mode 'v' => { if self.editor.select_opt().is_some() { @@ -244,15 +308,23 @@ impl<'a> Edit for ViEditor<'a> { '^' => self.editor.action(font_system, Action::SoftHome), // Enter command mode ':' => { - self.mode = ViMode::Command; + self.mode = ViMode::Command { + value: String::new(), + }; } // Enter search mode '/' => { - self.mode = ViMode::Search { forwards: true }; + self.mode = ViMode::Search { + value: String::new(), + forwards: true, + }; } // Enter search backwards mode '?' => { - self.mode = ViMode::Search { forwards: false }; + self.mode = ViMode::Search { + value: String::new(), + forwards: false, + }; } _ => (), }, @@ -278,24 +350,47 @@ impl<'a> Edit for ViEditor<'a> { } _ => self.editor.action(font_system, action), }, - ViMode::Command => match action { + ViMode::Command { ref mut value } => match action { Action::Escape => { self.mode = ViMode::Normal; } - Action::Enter => {} Action::Insert(c) => match c { - _ => {} + _ => { + value.push(c); + } }, + Action::Enter => { + //TODO: run command + self.mode = ViMode::Normal; + } + Action::Backspace => { + if value.pop().is_none() { + self.mode = ViMode::Normal; + } + } _ => self.editor.action(font_system, action), }, - ViMode::Search { forwards } => match action { + ViMode::Search { + ref mut value, + forwards, + } => match action { Action::Escape => { self.mode = ViMode::Normal; } - Action::Enter => {} - Action::Insert(c) => match c { - _ => {} - }, + Action::Insert(c) => { + value.push(c); + } + Action::Enter => { + //TODO: do not require clone? + self.search_opt = Some((value.clone(), forwards)); + self.mode = ViMode::Normal; + self.search(false); + } + Action::Backspace => { + if value.pop().is_none() { + self.mode = ViMode::Normal; + } + } _ => self.editor.action(font_system, action), }, } From ad10e7373b5b36aaa5ecbc570f45fd8a20a2da6c Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 27 Oct 2023 13:08:27 -0600 Subject: [PATCH 08/46] Require default Attrs to be specified in set_rich_text --- examples/rich-text/src/main.rs | 2 +- src/buffer.rs | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index 66092db..a3d8c41 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -119,7 +119,7 @@ fn main() { editor .buffer_mut() - .set_rich_text(spans.iter().copied(), Shaping::Advanced); + .set_rich_text(spans.iter().copied(), attrs, Shaping::Advanced); let mut swash_cache = SwashCache::new(); diff --git a/src/buffer.rs b/src/buffer.rs index a04675d..3a32373 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -618,7 +618,7 @@ impl Buffer { attrs: Attrs, shaping: Shaping, ) { - self.set_rich_text(font_system, [(text, attrs)], shaping); + self.set_rich_text(font_system, [(text, attrs)], attrs, shaping); } /// Set text of buffer, using an iterator of styled spans (pairs of text and attributes) @@ -634,6 +634,7 @@ impl Buffer { /// ("hello, ", attrs), /// ("cosmic\ntext", attrs.family(Family::Monospace)), /// ], + /// attrs, /// Shaping::Advanced, /// ); /// ``` @@ -641,13 +642,14 @@ impl Buffer { &mut self, font_system: &mut FontSystem, spans: I, + default_attrs: Attrs, shaping: Shaping, ) where I: IntoIterator)>, { self.lines.clear(); - let mut attrs_list = AttrsList::new(Attrs::new()); + let mut attrs_list = AttrsList::new(default_attrs); let mut line_string = String::new(); let mut end = 0; let (string, spans_data): (String, Vec<_>) = spans @@ -676,7 +678,7 @@ impl Buffer { // this is reached only if this text is empty self.lines.push(BufferLine::new( String::new(), - AttrsList::new(Attrs::new()), + AttrsList::new(default_attrs), shaping, )); break; @@ -705,7 +707,7 @@ impl Buffer { if maybe_line.is_some() { // finalize this line and start a new line let prev_attrs_list = - core::mem::replace(&mut attrs_list, AttrsList::new(Attrs::new())); + core::mem::replace(&mut attrs_list, AttrsList::new(default_attrs)); let prev_line_string = core::mem::take(&mut line_string); let buffer_line = BufferLine::new(prev_line_string, prev_attrs_list, shaping); self.lines.push(buffer_line); @@ -941,14 +943,16 @@ impl<'a> BorrowedWithFontSystem<'a, Buffer> { /// ("hello, ", attrs), /// ("cosmic\ntext", attrs.family(Family::Monospace)), /// ], + /// attrs, /// Shaping::Advanced, /// ); /// ``` - pub fn set_rich_text<'r, 's, I>(&mut self, spans: I, shaping: Shaping) + pub fn set_rich_text<'r, 's, I>(&mut self, spans: I, default_attrs: Attrs, shaping: Shaping) where I: IntoIterator)>, { - self.inner.set_rich_text(self.font_system, spans, shaping); + self.inner + .set_rich_text(self.font_system, spans, default_attrs, shaping); } /// Draw the buffer From 423fc2243930645036ff053e384a3ce64e70255e Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 27 Oct 2023 13:17:56 -0600 Subject: [PATCH 09/46] ViEditor: fix cursor and select positions --- src/edit/vi.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index cd8fcc3..483a88c 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -419,6 +419,7 @@ impl<'a> Edit for ViEditor<'a> { for run in self.buffer().layout_runs() { let line_i = run.line_i; let line_y = run.line_y; + let line_top = run.line_top; let cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32, f32)> { //TODO: better calculation of width @@ -497,7 +498,7 @@ impl<'a> Edit for ViEditor<'a> { } else if let Some((min, max)) = range_opt.take() { f( min, - (line_y - font_size) as i32, + line_top as i32, cmp::max(0, max - min) as u32, line_height as u32, Color::rgba(color.r(), color.g(), color.b(), 0x33), @@ -523,7 +524,7 @@ impl<'a> Edit for ViEditor<'a> { } f( min, - (line_y - font_size) as i32, + line_top as i32, cmp::max(0, max - min) as u32, line_height as u32, Color::rgba(color.r(), color.g(), color.b(), 0x33), @@ -583,7 +584,7 @@ impl<'a> Edit for ViEditor<'a> { let right_x = cmp::max(start_x, end_x); f( left_x, - (line_y - font_size) as i32, + line_top as i32, (right_x - left_x) as u32, line_height as u32, Color::rgba(color.r(), color.g(), color.b(), 0x33), @@ -591,7 +592,7 @@ impl<'a> Edit for ViEditor<'a> { } else { f( start_x, - (line_y - font_size) as i32, + line_top as i32, 1, line_height as u32, color, From d53932bd7c7e835cb93e9969c5de302f6fa04cf0 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 31 Oct 2023 20:40:46 -0600 Subject: [PATCH 10/46] Add function to set metrics and size simultaneously --- src/buffer.rs | 36 +++++++++++++++++++++++++++++------- src/edit/vi.rs | 8 +------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 3a32373..2f60395 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -552,12 +552,7 @@ impl Buffer { /// /// Will panic if `metrics.font_size` is zero. pub fn set_metrics(&mut self, font_system: &mut FontSystem, metrics: Metrics) { - if metrics != self.metrics { - assert_ne!(metrics.font_size, 0.0, "font size cannot be 0"); - self.metrics = metrics; - self.relayout(font_system); - self.shape_until_scroll(font_system); - } + self.set_metrics_and_size(font_system, metrics, self.width, self.height); } /// Get the current [`Wrap`] @@ -581,10 +576,27 @@ impl Buffer { /// Set the current buffer dimensions pub fn set_size(&mut self, font_system: &mut FontSystem, width: f32, height: f32) { + self.set_metrics_and_size(font_system, self.metrics, width, height); + } + + /// Set the current [`Metrics`] and buffer dimensions at the same time + /// + /// # Panics + /// + /// Will panic if `metrics.font_size` is zero. + pub fn set_metrics_and_size( + &mut self, + font_system: &mut FontSystem, + metrics: Metrics, + width: f32, + height: f32, + ) { let clamped_width = width.max(0.0); let clamped_height = height.max(0.0); - if clamped_width != self.width || clamped_height != self.height { + if metrics != self.metrics || clamped_width != self.width || clamped_height != self.height { + assert_ne!(metrics.font_size, 0.0, "font size cannot be 0"); + self.metrics = metrics; self.width = clamped_width; self.height = clamped_height; self.relayout(font_system); @@ -925,6 +937,16 @@ impl<'a> BorrowedWithFontSystem<'a, Buffer> { self.inner.set_size(self.font_system, width, height); } + /// Set the current [`Metrics`] and buffer dimensions at the same time + /// + /// # Panics + /// + /// Will panic if `metrics.font_size` is zero. + pub fn set_metrics_and_size(&mut self, metrics: Metrics, width: f32, height: f32) { + self.inner + .set_metrics_and_size(self.font_system, metrics, width, height); + } + /// Set text of buffer, using provided attributes for each line by default pub fn set_text(&mut self, text: &str, attrs: Attrs, shaping: Shaping) { self.inner.set_text(self.font_system, text, attrs, shaping); diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 483a88c..f11ec1c 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -590,13 +590,7 @@ impl<'a> Edit for ViEditor<'a> { Color::rgba(color.r(), color.g(), color.b(), 0x33), ); } else { - f( - start_x, - line_top as i32, - 1, - line_height as u32, - color, - ); + f(start_x, line_top as i32, 1, line_height as u32, color); } } From 7855dce09d457ea6d516f621fdaafaa7ce78d9d4 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 1 Nov 2023 13:31:53 -0600 Subject: [PATCH 11/46] Add indent action and tab width --- src/edit/editor.rs | 173 ++++++++++++++++++++++++++++++++++++++++++++ src/edit/mod.rs | 24 +++++- src/edit/syntect.rs | 8 ++ src/edit/vi.rs | 8 ++ 4 files changed, 210 insertions(+), 3 deletions(-) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 44d0ed0..f1c6074 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -23,6 +23,7 @@ pub struct Editor { cursor_x_opt: Option, select_opt: Option, cursor_moved: bool, + tab_width: usize, } impl Editor { @@ -34,6 +35,7 @@ impl Editor { cursor_x_opt: None, select_opt: None, cursor_moved: false, + tab_width: 4, } } @@ -104,6 +106,21 @@ impl Edit for Editor { } } + fn tab_width(&self) -> usize { + self.tab_width + } + + fn set_tab_width(&mut self, tab_width: usize) { + // A tab width of 0 is not allowed + if tab_width == 0 { + return; + } + if self.tab_width != tab_width { + self.tab_width = tab_width; + self.buffer.set_redraw(true); + } + } + fn shape_as_needed(&mut self, font_system: &mut FontSystem) { if self.cursor_moved { self.buffer.shape_until_cursor(font_system, self.cursor); @@ -576,6 +593,162 @@ impl Edit for Editor { self.buffer.lines[self.cursor.line].append(old_line); } } + Action::Indent => { + // Get start and end of selection + let (start, end) = match self.select_opt { + Some(select) => match select.line.cmp(&self.cursor.line) { + cmp::Ordering::Greater => (self.cursor, select), + cmp::Ordering::Less => (select, self.cursor), + cmp::Ordering::Equal => { + /* select.line == self.cursor.line */ + if select.index < self.cursor.index { + (select, self.cursor) + } else { + /* select.index >= self.cursor.index */ + (self.cursor, select) + } + } + }, + None => (self.cursor, self.cursor), + }; + + // For every line in selection + for line_i in start.line..=end.line { + let line = &mut self.buffer.lines[line_i]; + + // Determine indexes of last indent and first character after whitespace + let mut after_whitespace = 0; + let mut required_indent = 0; + { + let text = line.text(); + for (count, (index, c)) in text.char_indices().enumerate() { + if !c.is_whitespace() { + after_whitespace = index; + required_indent = self.tab_width - (count % self.tab_width); + break; + } + } + } + + // No indent required (not possible?) + if required_indent == 0 { + continue; + } + + // Save line after last whitespace + let after = line.split_off(after_whitespace); + + // Add required indent + line.append(BufferLine::new( + " ".repeat(required_indent), + AttrsList::new(line.attrs_list().defaults()), + Shaping::Advanced, + )); + + // Re-add line after last whitespace + line.append(after); + + // Adjust cursor + if self.cursor.line == line_i { + if self.cursor.index >= after_whitespace { + self.cursor.index += required_indent; + self.cursor_moved = true; + } + } + + // Adjust selection + match self.select_opt { + Some(ref mut select) => { + if select.line == line_i { + if select.index >= after_whitespace { + select.index += required_indent; + } + } + } + None => {} + } + + // Request redraw + self.buffer.set_redraw(true); + } + } + Action::Unindent => { + // Get start and end of selection + let (start, end) = match self.select_opt { + Some(select) => match select.line.cmp(&self.cursor.line) { + cmp::Ordering::Greater => (self.cursor, select), + cmp::Ordering::Less => (select, self.cursor), + cmp::Ordering::Equal => { + /* select.line == self.cursor.line */ + if select.index < self.cursor.index { + (select, self.cursor) + } else { + /* select.index >= self.cursor.index */ + (self.cursor, select) + } + } + }, + None => (self.cursor, self.cursor), + }; + + // For every line in selection + for line_i in start.line..=end.line { + let line = &mut self.buffer.lines[line_i]; + + // Determine indexes of last indent and first character after whitespace + let mut last_indent = 0; + let mut after_whitespace = 0; + { + let text = line.text(); + for (count, (index, c)) in text.char_indices().enumerate() { + if !c.is_whitespace() { + after_whitespace = index; + break; + } + if count % self.tab_width == 0 { + last_indent = index; + } + } + } + + // No de-indent required + if last_indent == after_whitespace { + continue; + } + + // Save line after last whitespace + let after = line.split_off(after_whitespace); + + // Drop part of line after last indent + line.split_off(last_indent); + + // Re-add line after last whitespace + line.append(after); + + // Adjust cursor + if self.cursor.line == line_i { + if self.cursor.index > last_indent { + self.cursor.index -= after_whitespace - last_indent; + self.cursor_moved = true; + } + } + + // Adjust selection + match self.select_opt { + Some(ref mut select) => { + if select.line == line_i { + if select.index > last_indent { + select.index -= after_whitespace - last_indent; + } + } + } + None => {} + } + + // Request redraw + self.buffer.set_redraw(true); + } + } Action::Click { x, y } => { self.select_opt = None; diff --git a/src/edit/mod.rs b/src/edit/mod.rs index a3173a7..3a8041b 100644 --- a/src/edit/mod.rs +++ b/src/edit/mod.rs @@ -59,12 +59,24 @@ pub enum Action { Backspace, /// Delete text in front of cursor Delete, + // Indent text (typically Tab) + Indent, + // Unindent text (typically Shift+Tab) + Unindent, /// Mouse click at specified position - Click { x: i32, y: i32 }, + Click { + x: i32, + y: i32, + }, /// Mouse drag to specified position - Drag { x: i32, y: i32 }, + Drag { + x: i32, + y: i32, + }, /// Scroll specified number of lines - Scroll { lines: i32 }, + Scroll { + lines: i32, + }, /// Move cursor to previous word boundary PreviousWord, /// Move cursor to next word boundary @@ -113,6 +125,12 @@ pub trait Edit { /// Set the current selection position fn set_select_opt(&mut self, select_opt: Option); + /// Get the current tab width + fn tab_width(&self) -> usize; + + /// Set the current tab width. A tab_width of 0 is not allowed, and will be ignored + fn set_tab_width(&mut self, tab_width: usize); + /// Shape lines until scroll, after adjusting scroll if the cursor moved fn shape_as_needed(&mut self, font_system: &mut FontSystem); diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index 8eda391..684af71 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -157,6 +157,14 @@ impl<'a> Edit for SyntaxEditor<'a> { self.editor.set_select_opt(select_opt); } + fn tab_width(&self) -> usize { + self.editor.tab_width() + } + + fn set_tab_width(&mut self, tab_width: usize) { + self.editor.set_tab_width(tab_width); + } + fn shape_as_needed(&mut self, font_system: &mut FontSystem) { #[cfg(feature = "std")] let now = std::time::Instant::now(); diff --git a/src/edit/vi.rs b/src/edit/vi.rs index f11ec1c..8733869 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -156,6 +156,14 @@ impl<'a> Edit for ViEditor<'a> { self.editor.set_select_opt(select_opt); } + fn tab_width(&self) -> usize { + self.editor.tab_width() + } + + fn set_tab_width(&mut self, tab_width: usize) { + self.editor.set_tab_width(tab_width); + } + fn shape_as_needed(&mut self, font_system: &mut FontSystem) { self.editor.shape_as_needed(font_system); } From ca35e1f429af2ac3121f8afb0991963820dbd1bf Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 1 Nov 2023 14:17:37 -0600 Subject: [PATCH 12/46] ViEditor: redraw when passthrough mode changed --- src/edit/vi.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 8733869..05e1e43 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -60,10 +60,13 @@ impl<'a> ViEditor<'a> { /// Set passthrough mode (true will turn off vi features) pub fn set_passthrough(&mut self, passthrough: bool) { - if passthrough { - self.mode = ViMode::Passthrough; - } else { - self.mode = ViMode::Normal; + if passthrough != (self.mode == ViMode::Passthrough) { + if passthrough { + self.mode = ViMode::Passthrough; + } else { + self.mode = ViMode::Normal; + } + self.buffer_mut().set_redraw(true); } } From 6196d72266a99730ecf4d77d5e8d44dda86cfe1e Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 2 Nov 2023 09:57:24 -0600 Subject: [PATCH 13/46] Syntax highlight on demand --- src/edit/syntect.rs | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index 684af71..5d995ac 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -169,12 +169,30 @@ impl<'a> Edit for SyntaxEditor<'a> { #[cfg(feature = "std")] let now = std::time::Instant::now(); + let cursor = self.cursor(); let buffer = self.editor.buffer_mut(); - + let lines = buffer.visible_lines(); + let scroll_end = buffer.scroll() + lines; + let mut total_layout = 0; let mut highlighted = 0; for line_i in 0..buffer.lines.len() { + // Break out if we have reached the end of scroll and are past the cursor + if total_layout >= scroll_end && line_i > cursor.line { + break; + } + let line = &mut buffer.lines[line_i]; if !line.is_reset() && line_i < self.syntax_cache.len() { + //TODO: duplicated code! + // Perform shaping and layout of this line in order to count if we have reached scroll + match buffer.line_layout(font_system, line_i) { + Some(layout_lines) => { + total_layout += layout_lines.len() as i32; + }, + None => { + //TODO: should this be possible? + } + } continue; } highlighted += 1; @@ -229,8 +247,15 @@ impl<'a> Edit for SyntaxEditor<'a> { line.set_attrs_list(attrs_list); line.set_wrap(Wrap::Word); - //TODO: efficiently do syntax highlighting without having to shape whole buffer - buffer.line_shape(font_system, line_i); + // Perform shaping and layout of this line in order to count if we have reached scroll + match buffer.line_layout(font_system, line_i) { + Some(layout_lines) => { + total_layout += layout_lines.len() as i32; + }, + None => { + //TODO: should this be possible? + } + } let cache_item = (parse_state.clone(), highlight_state.clone()); if line_i < self.syntax_cache.len() { From 241c4ca357b91334c07cb41e2679857841b33e19 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 2 Nov 2023 10:24:21 -0600 Subject: [PATCH 14/46] Buffer::set_rich_text: Only add attrs if they don't match the defaults --- src/buffer.rs | 5 ++++- src/edit/syntect.rs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 2f60395..9d23f63 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -704,7 +704,10 @@ impl Buffer { let text_start = line_string.len(); line_string.push_str(text); let text_end = line_string.len(); - attrs_list.add_span(text_start..text_end, *attrs); + // Only add attrs if they don't match the defaults + if *attrs != attrs_list.defaults() { + attrs_list.add_span(text_start..text_end, *attrs); + } } // we know that at the end of a line, diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index 5d995ac..be5e5b2 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -188,7 +188,7 @@ impl<'a> Edit for SyntaxEditor<'a> { match buffer.line_layout(font_system, line_i) { Some(layout_lines) => { total_layout += layout_lines.len() as i32; - }, + } None => { //TODO: should this be possible? } @@ -251,7 +251,7 @@ impl<'a> Edit for SyntaxEditor<'a> { match buffer.line_layout(font_system, line_i) { Some(layout_lines) => { total_layout += layout_lines.len() as i32; - }, + } None => { //TODO: should this be possible? } From ac389d9eebe1a6d8fe21bae315a853a5e0205b73 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 2 Nov 2023 12:55:45 -0600 Subject: [PATCH 15/46] SyntaxEditor: Allow retrieving syntax theme, optimize updates to theme --- src/edit/syntect.rs | 19 ++++++++++++++----- src/edit/vi.rs | 12 +++++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index be5e5b2..ec55671 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -3,7 +3,7 @@ use alloc::{string::String, vec::Vec}; #[cfg(feature = "std")] use std::{fs, io, path::Path}; use syntect::highlighting::{ - FontStyle, HighlightState, Highlighter, RangedHighlightIterator, Theme, ThemeSet, + FontStyle, HighlightState, Highlighter, RangedHighlightIterator, ThemeSet, }; use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet}; @@ -12,6 +12,8 @@ use crate::{ Shaping, Style, Weight, Wrap, }; +pub use syntect::highlighting::Theme as SyntaxTheme; + #[derive(Debug)] pub struct SyntaxSystem { pub syntax_set: SyntaxSet, @@ -35,7 +37,7 @@ pub struct SyntaxEditor<'a> { editor: Editor, syntax_system: &'a SyntaxSystem, syntax: &'a SyntaxReference, - theme: &'a Theme, + theme: &'a SyntaxTheme, highlighter: Highlighter<'a>, syntax_cache: Vec<(ParseState, HighlightState)>, } @@ -65,9 +67,11 @@ impl<'a> SyntaxEditor<'a> { /// Modifies the theme of the [`SyntaxEditor`], returning false if the theme is missing pub fn update_theme(&mut self, theme_name: &str) -> bool { if let Some(theme) = self.syntax_system.theme_set.themes.get(theme_name) { - self.theme = theme; - self.highlighter = Highlighter::new(theme); - self.syntax_cache.clear(); + if self.theme != theme { + self.theme = theme; + self.highlighter = Highlighter::new(theme); + self.syntax_cache.clear(); + } true } else { @@ -130,6 +134,11 @@ impl<'a> SyntaxEditor<'a> { Color::rgb(0xFF, 0xFF, 0xFF) } } + + /// Get the current syntect theme + pub fn theme(&self) -> &SyntaxTheme { + self.theme + } } impl<'a> Edit for SyntaxEditor<'a> { diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 05e1e43..f3cf7b3 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -4,7 +4,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::{ Action, AttrsList, BorrowedWithFontSystem, Buffer, Color, Cursor, Edit, FontSystem, - SyntaxEditor, + SyntaxEditor, SyntaxTheme, }; #[derive(Clone, Debug, Eq, PartialEq)] @@ -37,6 +37,11 @@ impl<'a> ViEditor<'a> { } } + /// Modifies the theme of the [`SyntaxEditor`], returning false if the theme is missing + pub fn update_theme(&mut self, theme_name: &str) -> bool { + self.editor.update_theme(theme_name) + } + /// Load text from a file, and also set syntax to the best option #[cfg(feature = "std")] pub fn load_text>( @@ -58,6 +63,11 @@ impl<'a> ViEditor<'a> { self.editor.foreground_color() } + /// Get the current syntect theme + pub fn theme(&self) -> &SyntaxTheme { + self.editor.theme() + } + /// Set passthrough mode (true will turn off vi features) pub fn set_passthrough(&mut self, passthrough: bool) { if passthrough != (self.mode == ViMode::Passthrough) { From e62fea5efddb20fd1bc518e0d733a86f6858fa73 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 2 Nov 2023 13:38:25 -0600 Subject: [PATCH 16/46] SyntaxEditor: Support using two-face syntax definitions --- Cargo.toml | 9 +++++---- src/edit/syntect.rs | 12 ++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b04f5fa..a662f71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,19 +11,20 @@ rust-version = "1.65" [dependencies] fontdb = { version = "0.15.0", default-features = false } +hashbrown = { version = "0.14.1", optional = true, default-features = false } libm = "0.2.8" log = "0.4.20" +rangemap = "1.4.0" +rustc-hash = { version = "1.1.0", default-features = false } rustybuzz = { version = "0.11.0", default-features = false, features = ["libm"] } +self_cell = "1.0.1" swash = { version = "0.1.8", optional = true } syntect = { version = "5.1.0", optional = true } sys-locale = { version = "0.3.1", optional = true } +two-face = { version = "0.3.0", optional = true } unicode-linebreak = "0.1.5" unicode-script = "0.5.5" unicode-segmentation = "1.10.1" -rangemap = "1.4.0" -hashbrown = { version = "0.14.1", optional = true, default-features = false } -rustc-hash = { version = "1.1.0", default-features = false } -self_cell = "1.0.1" [dependencies.unicode-bidi] version = "0.3.13" diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index ec55671..9ba06dd 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -22,6 +22,7 @@ pub struct SyntaxSystem { impl SyntaxSystem { /// Create a new [`SyntaxSystem`] + #[cfg(not(feature = "two-face"))] pub fn new() -> Self { Self { //TODO: store newlines in buffer @@ -29,6 +30,17 @@ impl SyntaxSystem { theme_set: ThemeSet::load_defaults(), } } + + /// Create a new [`SyntaxSystem`] using `[two-face]` definitions + #[cfg(feature = "two-face")] + pub fn new() -> Self { + Self { + //TODO: store newlines in buffer + syntax_set: two_face::syntax::extra_no_newlines(), + //TODO: use two-face themes + theme_set: ThemeSet::load_defaults(), + } + } } /// A wrapper of [`Editor`] with syntax highlighting provided by [`SyntaxSystem`] From db0883b5253ce2e770c6e478c2bea68d36f1a0d2 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 7 Nov 2023 15:56:31 -0700 Subject: [PATCH 17/46] Editor: add GotoLine action --- src/edit/editor.rs | 6 ++++++ src/edit/mod.rs | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index f1c6074..3876855 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -91,6 +91,7 @@ impl Edit for Editor { if self.cursor != cursor { self.cursor = cursor; self.cursor_moved = true; + self.cursor_x_opt = None; self.buffer.set_redraw(true); } } @@ -854,6 +855,11 @@ impl Edit for Editor { self.cursor.index = self.buffer.lines[self.cursor.line].text().len(); self.cursor_x_opt = None; } + Action::GotoLine(line) => { + let mut cursor = self.buffer.layout_cursor(&self.cursor); + cursor.line = line; + self.set_layout_cursor(font_system, cursor); + } } if old_cursor != self.cursor { diff --git a/src/edit/mod.rs b/src/edit/mod.rs index 3a8041b..9c43a96 100644 --- a/src/edit/mod.rs +++ b/src/edit/mod.rs @@ -89,6 +89,8 @@ pub enum Action { BufferStart, /// Move cursor to the end of the document BufferEnd, + /// Move cursor to specific line + GotoLine(usize), } /// A trait to allow easy replacements of [`Editor`], like `SyntaxEditor` From 659001dad8d88e4a429db6bbfe2a51d559070dab Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 7 Nov 2023 15:56:43 -0700 Subject: [PATCH 18/46] editor-orbclient: fix scaling --- examples/editor-orbclient/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/editor-orbclient/src/main.rs b/examples/editor-orbclient/src/main.rs index ca82313..14caf46 100644 --- a/examples/editor-orbclient/src/main.rs +++ b/examples/editor-orbclient/src/main.rs @@ -22,7 +22,7 @@ fn main() { let display_scale = match orbclient::get_display_size() { Ok((w, h)) => { log::info!("Display size: {}, {}", w, h); - (h as f32 / 1600.0) + 1.0 + (h / 1600) as f32 + 1.0 } Err(err) => { log::warn!("Failed to get display size: {}", err); From 74c92e04194e1fc02ce2fd6cbde2d8f90115fe19 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 7 Nov 2023 15:57:00 -0700 Subject: [PATCH 19/46] ViEditor: switch to using modit --- Cargo.toml | 8 +- src/edit/vi.rs | 448 ++++++++++++++++++++++--------------------------- 2 files changed, 205 insertions(+), 251 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a662f71..53a39bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,12 @@ unicode-linebreak = "0.1.5" unicode-script = "0.5.5" unicode-segmentation = "1.10.1" +#TODO: crates release +[dependencies.modit] +git = "https://github.com/pop-os/modit.git" +optional = true +#path = "../modit" + [dependencies.unicode-bidi] version = "0.3.13" default-features = false @@ -41,7 +47,7 @@ std = [ "sys-locale", "unicode-bidi/std", ] -vi = ["syntect"] +vi = ["modit", "syntect"] wasm-web = ["sys-locale?/js"] warn_on_missing_glyphs = [] fontconfig = ["fontdb/fontconfig", "std"] diff --git a/src/edit/vi.rs b/src/edit/vi.rs index f3cf7b3..13027bd 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -1,5 +1,6 @@ use alloc::string::String; use core::cmp; +use modit::{Event, Motion, Operator, Parser, WordIter}; use unicode_segmentation::UnicodeSegmentation; use crate::{ @@ -7,24 +8,13 @@ use crate::{ SyntaxEditor, SyntaxTheme, }; -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum ViMode { - /// Passthrough mode, disables vi features - Passthrough, - /// Normal mode - Normal, - /// Insert mode - Insert, - /// Command mode - Command { value: String }, - /// Search mode - Search { value: String, forwards: bool }, -} +pub use modit::{ViMode, ViParser}; #[derive(Debug)] pub struct ViEditor<'a> { editor: SyntaxEditor<'a>, - mode: ViMode, + parser: ViParser, + passthrough: bool, search_opt: Option<(String, bool)>, } @@ -32,7 +22,8 @@ impl<'a> ViEditor<'a> { pub fn new(editor: SyntaxEditor<'a>) -> Self { Self { editor, - mode: ViMode::Normal, + parser: ViParser::new(), + passthrough: false, search_opt: None, } } @@ -70,19 +61,15 @@ impl<'a> ViEditor<'a> { /// Set passthrough mode (true will turn off vi features) pub fn set_passthrough(&mut self, passthrough: bool) { - if passthrough != (self.mode == ViMode::Passthrough) { - if passthrough { - self.mode = ViMode::Passthrough; - } else { - self.mode = ViMode::Normal; - } + if passthrough != self.passthrough { + self.passthrough = passthrough; self.buffer_mut().set_redraw(true); } } - /// Get current vi editing mode - pub fn mode(&self) -> &ViMode { - &self.mode + /// Get current vi parser + pub fn parser(&self) -> &ViParser { + &self.parser } fn search(&mut self, inverted: bool) { @@ -194,231 +181,189 @@ impl<'a> Edit for ViEditor<'a> { } fn action(&mut self, font_system: &mut FontSystem, action: Action) { - let old_mode = self.mode.clone(); + let editor = &mut self.editor; + let c = match action { + Action::Escape => modit::ESCAPE, + Action::Insert(c) => c, + Action::Enter => modit::ENTER, + Action::Backspace => modit::BACKSPACE, + Action::Delete => modit::DELETE, + _ => return editor.action(font_system, action), + }; + //TODO: redraw on parser mode change + self.parser.parse(c, false, |event| { + log::info!("{:?}", event); + let action = match event { + Event::Backspace => Action::Backspace, + Event::Delete => Action::Delete, + Event::Down => Action::Down, + Event::End => Action::End, + Event::Escape => Action::Escape, + Event::Home => Action::Home, + Event::Insert(c) => Action::Insert(c), + Event::Left => Action::Left, + Event::NewLine => Action::Enter, + Event::Operator(count, operator, motion, text_object_opt) => { + let start = editor.cursor(); - match self.mode { - ViMode::Passthrough => self.editor.action(font_system, action), - ViMode::Normal => match action { - Action::Insert(c) => match c { - // Enter insert mode after cursor - 'a' => { - self.editor.action(font_system, Action::Right); - self.mode = ViMode::Insert; - } - // Enter insert mode at end of line - 'A' => { - self.editor.action(font_system, Action::End); - self.mode = ViMode::Insert; - } - // Previous word - 'b' => { - //TODO: WORD vs word, iterate by vi word rules - self.editor.action(font_system, Action::PreviousWord); - } - // Previous WORD - 'B' => { - //TODO: WORD vs word, iterate by vi word rules - self.editor.action(font_system, Action::PreviousWord); - } - // Change mode - 'c' => { - if self.editor.select_opt().is_some() { - self.editor.action(font_system, Action::Delete); - self.mode = ViMode::Insert; - } else { - //TODO: change to next cursor movement - } - } - // Delete mode - 'd' => { - if self.editor.select_opt().is_some() { - self.editor.action(font_system, Action::Delete); - } else { - //TODO: delete to next cursor movement - } - } - // End of word - 'e' => { - //TODO: WORD vs word, iterate by vi word rules - self.editor.action(font_system, Action::NextWord); - } - // End of WORD - 'E' => { - //TODO: WORD vs word, iterate by vi word rules - self.editor.action(font_system, Action::NextWord); - } - // Enter insert mode at cursor - 'i' => { - self.mode = ViMode::Insert; - } - // Enter insert mode at start of line - 'I' => { - self.editor.action(font_system, Action::SoftHome); - self.mode = ViMode::Insert; - } - // Create line after and enter insert mode - 'o' => { - self.editor.action(font_system, Action::End); - self.editor.action(font_system, Action::Enter); - self.mode = ViMode::Insert; - } - // Create line before and enter insert mode - 'O' => { - self.editor.action(font_system, Action::Home); - self.editor.action(font_system, Action::Enter); - self.editor.shape_as_needed(font_system); // TODO: do not require this? - self.editor.action(font_system, Action::Up); - self.mode = ViMode::Insert; - } - // Left - 'h' => self.editor.action(font_system, Action::Left), - // Top of screen - //TODO: 'H' => self.editor.action(Action::ScreenHigh), - // Down - 'j' => self.editor.action(font_system, Action::Down), - // Up - 'k' => self.editor.action(font_system, Action::Up), - // Right - 'l' | ' ' => self.editor.action(font_system, Action::Right), - // Bottom of screen - //TODO: 'L' => self.editor.action(Action::ScreenLow), - // Middle of screen - //TODO: 'M' => self.editor.action(Action::ScreenMiddle), - // Next search item - 'n' => self.search(false), - // Previous search item - 'N' => self.search(true), - // Enter visual mode - 'v' => { - if self.editor.select_opt().is_some() { - self.editor.set_select_opt(None); - } else { - self.editor.set_select_opt(Some(self.editor.cursor())); - } - } - // Enter line visual mode - 'V' => { - if self.editor.select_opt().is_some() { - self.editor.set_select_opt(None); - } else { - self.editor.action(font_system, Action::Home); - self.editor.set_select_opt(Some(self.editor.cursor())); - //TODO: set cursor_x_opt to max - self.editor.action(font_system, Action::End); - } - } - // Next word - 'w' => { - //TODO: WORD vs word, iterate by vi word rules - self.editor.action(font_system, Action::NextWord); - } - // Next WORD - 'W' => { - //TODO: WORD vs word, iterate by vi word rules - self.editor.action(font_system, Action::NextWord); - } - // Remove character at cursor - 'x' => self.editor.action(font_system, Action::Delete), - // Remove character before cursor - 'X' => self.editor.action(font_system, Action::Backspace), - // Go to start of line - '0' => self.editor.action(font_system, Action::Home), - // Go to end of line - '$' => self.editor.action(font_system, Action::End), - // Go to start of line after whitespace - '^' => self.editor.action(font_system, Action::SoftHome), - // Enter command mode - ':' => { - self.mode = ViMode::Command { - value: String::new(), + for _ in 0..count { + let action = match motion { + Motion::Down => Action::Down, + Motion::End => Action::End, + Motion::GotoLine(line) => Action::GotoLine(line.saturating_sub(1)), + Motion::GotoEof => { + Action::GotoLine(editor.buffer().lines.len().saturating_sub(1)) + } + Motion::Home => Action::Home, + Motion::Left => Action::Left, + Motion::NextWordEnd(word) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer_mut(); + loop { + let text = buffer.lines[cursor.line].text(); + if cursor.index < text.len() { + cursor.index = WordIter::new(text, word) + .map(|(i, w)| { + i + w + .char_indices() + .last() + .map(|(i, _)| i) + .unwrap_or(0) + }) + .find(|&i| i > cursor.index) + .unwrap_or(text.len()); + if cursor.index == text.len() { + // Try again, searching next line + continue; + } + } else if cursor.line + 1 < buffer.lines.len() { + // Go to next line and rerun loop + cursor.line += 1; + cursor.index = 0; + continue; + } + break; + } + editor.set_cursor(cursor); + continue; + } + Motion::NextWordStart(word) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer_mut(); + loop { + let text = buffer.lines[cursor.line].text(); + if cursor.index < text.len() { + cursor.index = WordIter::new(text, word) + .map(|(i, _)| i) + .find(|&i| i > cursor.index) + .unwrap_or(text.len()); + if cursor.index == text.len() { + // Try again, searching next line + continue; + } + } else if cursor.line + 1 < buffer.lines.len() { + // Go to next line and rerun loop + cursor.line += 1; + cursor.index = 0; + continue; + } + break; + } + editor.set_cursor(cursor); + continue; + } + Motion::PreviousWordEnd(word) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer_mut(); + loop { + let text = buffer.lines[cursor.line].text(); + if cursor.index > 0 { + cursor.index = WordIter::new(text, word) + .map(|(i, w)| { + i + w + .char_indices() + .last() + .map(|(i, _)| i) + .unwrap_or(0) + }) + .filter(|&i| i < cursor.index) + .last() + .unwrap_or(0); + if cursor.index == 0 { + // Try again, searching previous line + continue; + } + } else if cursor.line > 0 { + // Go to previous line and rerun loop + cursor.line -= 1; + cursor.index = buffer.lines[cursor.line].text().len(); + continue; + } + break; + } + editor.set_cursor(cursor); + continue; + } + Motion::PreviousWordStart(word) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer_mut(); + loop { + let text = buffer.lines[cursor.line].text(); + if cursor.index > 0 { + cursor.index = WordIter::new(text, word) + .map(|(i, _)| i) + .filter(|&i| i < cursor.index) + .last() + .unwrap_or(0); + if cursor.index == 0 { + // Try again, searching previous line + continue; + } + } else if cursor.line > 0 { + // Go to previous line and rerun loop + cursor.line -= 1; + cursor.index = buffer.lines[cursor.line].text().len(); + continue; + } + break; + } + editor.set_cursor(cursor); + continue; + } + Motion::Right => Action::Right, + Motion::SoftHome => Action::SoftHome, + Motion::Up => Action::Up, + _ => { + log::info!("TODO"); + break; + } }; + editor.action(font_system, action); } - // Enter search mode - '/' => { - self.mode = ViMode::Search { - value: String::new(), - forwards: true, - }; - } - // Enter search backwards mode - '?' => { - self.mode = ViMode::Search { - value: String::new(), - forwards: false, - }; - } - _ => (), - }, - // Go to start of next line - Action::Enter => { - self.editor.action(font_system, Action::Down); - self.editor.action(font_system, Action::SoftHome); - } - // Left - Action::Backspace => { - self.editor.action(font_system, Action::Left); - } - _ => self.editor.action(font_system, action), - }, - ViMode::Insert => match action { - Action::Escape => { - let cursor = self.cursor(); - let layout_cursor = self.buffer().layout_cursor(&cursor); - if layout_cursor.glyph > 0 { - self.editor.action(font_system, Action::Left); - } - self.mode = ViMode::Normal; - } - _ => self.editor.action(font_system, action), - }, - ViMode::Command { ref mut value } => match action { - Action::Escape => { - self.mode = ViMode::Normal; - } - Action::Insert(c) => match c { - _ => { - value.push(c); - } - }, - Action::Enter => { - //TODO: run command - self.mode = ViMode::Normal; - } - Action::Backspace => { - if value.pop().is_none() { - self.mode = ViMode::Normal; - } - } - _ => self.editor.action(font_system, action), - }, - ViMode::Search { - ref mut value, - forwards, - } => match action { - Action::Escape => { - self.mode = ViMode::Normal; - } - Action::Insert(c) => { - value.push(c); - } - Action::Enter => { - //TODO: do not require clone? - self.search_opt = Some((value.clone(), forwards)); - self.mode = ViMode::Normal; - self.search(false); - } - Action::Backspace => { - if value.pop().is_none() { - self.mode = ViMode::Normal; - } - } - _ => self.editor.action(font_system, action), - }, - } - if self.mode != old_mode { - self.buffer_mut().set_redraw(true); - } + let end = editor.cursor(); + + println!("start {:?}, end {:?}", start, end); + return; + } + Event::Paste => { + log::info!("TODO"); + return; + } + Event::Replace(char) => { + log::info!("TODO"); + return; + } + Event::Right => Action::Right, + Event::SoftHome => Action::SoftHome, + Event::Undo => { + log::info!("TODO"); + return; + } + Event::Up => Action::Up, + }; + editor.action(font_system, action); + }); } #[cfg(feature = "swash")] @@ -558,11 +503,14 @@ impl<'a> Edit for ViEditor<'a> { if let Some((cursor_glyph, cursor_glyph_offset, cursor_glyph_width)) = cursor_glyph_opt(&self.cursor()) { - let block_cursor = match self.mode { - ViMode::Passthrough => false, - ViMode::Normal => true, - ViMode::Insert => false, - _ => true, /*TODO: determine block cursor in other modes*/ + let block_cursor = if self.passthrough { + false + } else { + match self.parser.mode { + ViMode::Normal(_) => true, + ViMode::Insert => false, + _ => true, /*TODO: determine block cursor in other modes*/ + } }; let (start_x, end_x) = match run.glyphs.get(cursor_glyph) { From fa83b2efe9cda4ff360eb0cb156b91451f17d65b Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 8 Nov 2023 11:03:53 -0700 Subject: [PATCH 20/46] Support NextChar and PreviousChar modit motions --- src/edit/vi.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 13027bd..bb6f3ac 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -216,6 +216,43 @@ impl<'a> Edit for ViEditor<'a> { } Motion::Home => Action::Home, Motion::Left => Action::Left, + Motion::NextChar(find_c) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer_mut(); + let text = buffer.lines[cursor.line].text(); + if cursor.index < text.len() { + match text[cursor.index..] + .char_indices() + .filter(|&(i, c)| i > 0 && c == find_c) + .next() + { + Some((i, _)) => { + cursor.index += i; + editor.set_cursor(cursor); + } + None => {} + } + } + continue; + } + Motion::NextCharTill(find_c) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer_mut(); + let text = buffer.lines[cursor.line].text(); + if cursor.index < text.len() { + let mut last_i = 0; + for (i, c) in text[cursor.index..].char_indices() { + if last_i > 0 && c == find_c { + cursor.index += last_i; + editor.set_cursor(cursor); + break; + } else { + last_i = i; + } + } + } + continue; + } Motion::NextWordEnd(word) => { let mut cursor = editor.cursor(); let buffer = editor.buffer_mut(); @@ -272,6 +309,52 @@ impl<'a> Edit for ViEditor<'a> { editor.set_cursor(cursor); continue; } + Motion::PreviousChar(find_c) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer_mut(); + let text = buffer.lines[cursor.line].text(); + if cursor.index > 0 { + match text[..cursor.index] + .char_indices() + .filter(|&(_, c)| c == find_c) + .last() + { + Some((i, _)) => { + cursor.index = i; + editor.set_cursor(cursor); + } + None => {} + } + } + continue; + } + Motion::PreviousCharTill(find_c) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer_mut(); + let text = buffer.lines[cursor.line].text(); + if cursor.index > 0 { + match text[..cursor.index] + .char_indices() + .filter_map(|(i, c)| { + if c == find_c { + let end = i + c.len_utf8(); + if end < cursor.index { + return Some(end); + } + } + None + }) + .last() + { + Some(i) => { + cursor.index = i; + editor.set_cursor(cursor); + } + None => {} + } + } + continue; + } Motion::PreviousWordEnd(word) => { let mut cursor = editor.cursor(); let buffer = editor.buffer_mut(); From 7a4cf29d4d7e7c836ff0a21ae91124df5b3dd89b Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 8 Nov 2023 11:56:16 -0700 Subject: [PATCH 21/46] Editor: shaped and layout lines inserted by Action::Enter --- src/edit/editor.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 3876855..187f84b 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -524,6 +524,9 @@ impl Edit for Editor { self.cursor.index = 0; self.buffer.lines.insert(self.cursor.line, new_line); + + // Ensure line is properly shaped and laid out (for potential immediate commands) + self.buffer.line_layout(font_system, self.cursor.line); } Action::Backspace => { if self.delete_selection() { From aece6486b96899c4ba8bff188f827ebf9b7aa4b0 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 8 Nov 2023 11:57:02 -0700 Subject: [PATCH 22/46] Adapt to newer modit --- src/edit/vi.rs | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index bb6f3ac..22fd5a7 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -182,6 +182,7 @@ impl<'a> Edit for ViEditor<'a> { fn action(&mut self, font_system: &mut FontSystem, action: Action) { let editor = &mut self.editor; + log::info!("Action {:?}", action); let c = match action { Action::Escape => modit::ESCAPE, Action::Insert(c) => c, @@ -192,18 +193,26 @@ impl<'a> Edit for ViEditor<'a> { }; //TODO: redraw on parser mode change self.parser.parse(c, false, |event| { - log::info!("{:?}", event); + log::info!(" Event {:?}", event); let action = match event { + Event::Redraw => { + editor.buffer_mut().set_redraw(true); + return; + } Event::Backspace => Action::Backspace, Event::Delete => Action::Delete, - Event::Down => Action::Down, - Event::End => Action::End, Event::Escape => Action::Escape, - Event::Home => Action::Home, Event::Insert(c) => Action::Insert(c), - Event::Left => Action::Left, Event::NewLine => Action::Enter, - Event::Operator(count, operator, motion, text_object_opt) => { + Event::Paste => { + log::info!("TODO"); + return; + } + Event::Undo => { + log::info!("TODO"); + return; + } + Event::Cmd(count, operator, motion, text_object_opt) => { let start = editor.cursor(); for _ in 0..count { @@ -429,21 +438,6 @@ impl<'a> Edit for ViEditor<'a> { println!("start {:?}, end {:?}", start, end); return; } - Event::Paste => { - log::info!("TODO"); - return; - } - Event::Replace(char) => { - log::info!("TODO"); - return; - } - Event::Right => Action::Right, - Event::SoftHome => Action::SoftHome, - Event::Undo => { - log::info!("TODO"); - return; - } - Event::Up => Action::Up, }; editor.action(font_system, action); }); @@ -590,8 +584,7 @@ impl<'a> Edit for ViEditor<'a> { false } else { match self.parser.mode { - ViMode::Normal(_) => true, - ViMode::Insert => false, + ViMode::Insert | ViMode::Replace => false, _ => true, /*TODO: determine block cursor in other modes*/ } }; From d7e066c105f29314473688af637abca71d846a8b Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 8 Nov 2023 14:23:13 -0700 Subject: [PATCH 23/46] Support more modit events --- src/edit/vi.rs | 154 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 135 insertions(+), 19 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 22fd5a7..94eb594 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -1,6 +1,6 @@ use alloc::string::String; use core::cmp; -use modit::{Event, Motion, Operator, Parser, WordIter}; +use modit::{Event, Motion, Operator, Parser, TextObject, WordIter}; use unicode_segmentation::UnicodeSegmentation; use crate::{ @@ -195,11 +195,15 @@ impl<'a> Edit for ViEditor<'a> { self.parser.parse(c, false, |event| { log::info!(" Event {:?}", event); let action = match event { - Event::Redraw => { - editor.buffer_mut().set_redraw(true); + Event::AutoIndent => { + log::info!("TODO"); return; } Event::Backspace => Action::Backspace, + Event::Copy => { + log::info!("TODO"); + return; + } Event::Delete => Action::Delete, Event::Escape => Action::Escape, Event::Insert(c) => Action::Insert(c), @@ -208,13 +212,129 @@ impl<'a> Edit for ViEditor<'a> { log::info!("TODO"); return; } + Event::Redraw => { + editor.buffer_mut().set_redraw(true); + return; + } + Event::SelectClear => { + editor.set_select_opt(None); + return; + } + Event::SelectStart => { + let cursor = editor.cursor(); + editor.set_select_opt(Some(cursor)); + return; + } + Event::SelectTextObject(text_object, include) => { + fn select_in( + editor: &mut SyntaxEditor, + start_c: char, + end_c: char, + include: bool, + ) { + // Find the largest encompasing object, or if there is none, find the next one. + let cursor = editor.cursor(); + let buffer = editor.buffer(); + + // Search forwards for isolated end character, counting start and end characters found + let mut end = cursor; + let mut starts = 0; + let mut ends = 0; + 'find_end: loop { + let line = &buffer.lines[end.line]; + let text = line.text(); + for (i, c) in text[end.index..].char_indices() { + if c == end_c { + ends += 1; + } else if c == start_c { + starts += 1; + } + if ends > starts { + end.index += if include { i + c.len_utf8() } else { i }; + break 'find_end; + } + } + if end.line + 1 < buffer.lines.len() { + end.line += 1; + end.index = 0; + } else { + break 'find_end; + } + } + + // Search backwards to resolve starts and ends + let mut start = cursor; + 'find_start: loop { + let line = &buffer.lines[start.line]; + let text = line.text(); + for (i, c) in text[..start.index].char_indices().rev() { + if c == start_c { + starts += 1; + } else if c == end_c { + ends += 1; + } + if starts >= ends { + start.index = if include { i } else { i + c.len_utf8() }; + break 'find_start; + } + } + if start.line > 0 { + start.line -= 1; + start.index = buffer.lines[start.line].text().len(); + } else { + break 'find_start; + } + } + + editor.set_select_opt(Some(start)); + editor.set_cursor(end); + } + + match text_object { + TextObject::AngleBrackets => select_in(editor, '<', '>', include), + TextObject::CurlyBrackets => select_in(editor, '{', '}', include), + TextObject::DoubleQuotes => select_in(editor, '"', '"', include), + TextObject::Parentheses => select_in(editor, '(', ')', include), + TextObject::SingleQuotes => select_in(editor, '\'', '\'', include), + TextObject::SquareBrackets => select_in(editor, '[', ']', include), + TextObject::Ticks => select_in(editor, '`', '`', include), + TextObject::Word(word) => { + let mut cursor = editor.cursor(); + let mut select_opt = editor.select_opt(); + let buffer = editor.buffer(); + let text = buffer.lines[cursor.line].text(); + match WordIter::new(text, word) + .find(|&(i, w)| i <= cursor.index && i + w.len() > cursor.index) + { + Some((i, w)) => { + cursor.index = i; + select_opt = Some(cursor); + cursor.index += w.len(); + } + None => { + //TODO + } + } + editor.set_select_opt(select_opt); + editor.set_cursor(cursor); + } + _ => { + log::info!("TODO: {:?}", text_object); + } + } + return; + } + Event::ShiftLeft => Action::Unindent, + Event::ShiftRight => Action::Indent, + Event::SwapCase => { + log::info!("TODO"); + return; + } Event::Undo => { log::info!("TODO"); return; } - Event::Cmd(count, operator, motion, text_object_opt) => { - let start = editor.cursor(); - + Event::Motion(motion, count) => { for _ in 0..count { let action = match motion { Motion::Down => Action::Down, @@ -227,7 +347,7 @@ impl<'a> Edit for ViEditor<'a> { Motion::Left => Action::Left, Motion::NextChar(find_c) => { let mut cursor = editor.cursor(); - let buffer = editor.buffer_mut(); + let buffer = editor.buffer(); let text = buffer.lines[cursor.line].text(); if cursor.index < text.len() { match text[cursor.index..] @@ -246,7 +366,7 @@ impl<'a> Edit for ViEditor<'a> { } Motion::NextCharTill(find_c) => { let mut cursor = editor.cursor(); - let buffer = editor.buffer_mut(); + let buffer = editor.buffer(); let text = buffer.lines[cursor.line].text(); if cursor.index < text.len() { let mut last_i = 0; @@ -264,7 +384,7 @@ impl<'a> Edit for ViEditor<'a> { } Motion::NextWordEnd(word) => { let mut cursor = editor.cursor(); - let buffer = editor.buffer_mut(); + let buffer = editor.buffer(); loop { let text = buffer.lines[cursor.line].text(); if cursor.index < text.len() { @@ -295,7 +415,7 @@ impl<'a> Edit for ViEditor<'a> { } Motion::NextWordStart(word) => { let mut cursor = editor.cursor(); - let buffer = editor.buffer_mut(); + let buffer = editor.buffer(); loop { let text = buffer.lines[cursor.line].text(); if cursor.index < text.len() { @@ -320,7 +440,7 @@ impl<'a> Edit for ViEditor<'a> { } Motion::PreviousChar(find_c) => { let mut cursor = editor.cursor(); - let buffer = editor.buffer_mut(); + let buffer = editor.buffer(); let text = buffer.lines[cursor.line].text(); if cursor.index > 0 { match text[..cursor.index] @@ -339,7 +459,7 @@ impl<'a> Edit for ViEditor<'a> { } Motion::PreviousCharTill(find_c) => { let mut cursor = editor.cursor(); - let buffer = editor.buffer_mut(); + let buffer = editor.buffer(); let text = buffer.lines[cursor.line].text(); if cursor.index > 0 { match text[..cursor.index] @@ -366,7 +486,7 @@ impl<'a> Edit for ViEditor<'a> { } Motion::PreviousWordEnd(word) => { let mut cursor = editor.cursor(); - let buffer = editor.buffer_mut(); + let buffer = editor.buffer(); loop { let text = buffer.lines[cursor.line].text(); if cursor.index > 0 { @@ -398,7 +518,7 @@ impl<'a> Edit for ViEditor<'a> { } Motion::PreviousWordStart(word) => { let mut cursor = editor.cursor(); - let buffer = editor.buffer_mut(); + let buffer = editor.buffer(); loop { let text = buffer.lines[cursor.line].text(); if cursor.index > 0 { @@ -426,16 +546,12 @@ impl<'a> Edit for ViEditor<'a> { Motion::SoftHome => Action::SoftHome, Motion::Up => Action::Up, _ => { - log::info!("TODO"); + log::info!("TODO: {:?}", motion); break; } }; editor.action(font_system, action); } - - let end = editor.cursor(); - - println!("start {:?}, end {:?}", start, end); return; } }; From c79c1326944c0a688e5c35015b834c18b53aa5c0 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 8 Nov 2023 15:32:04 -0700 Subject: [PATCH 24/46] Editor: Fix SoftHome --- src/edit/editor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 187f84b..742370d 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -452,8 +452,8 @@ impl Edit for Editor { let line: &mut BufferLine = &mut self.buffer.lines[self.cursor.line]; self.cursor.index = line .text() - .unicode_word_indices() - .map(|(i, _)| i) + .char_indices() + .filter_map(|(i, c)| if c.is_whitespace() { None } else { Some(i) }) .next() .unwrap_or(0); self.buffer.set_redraw(true); From 9efcc41a5aca6df16a66da53aae75b1191ba0576 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 9 Nov 2023 09:35:04 -0700 Subject: [PATCH 25/46] Remove unused import and implemented todo --- src/edit/vi.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 94eb594..75cd0f2 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -1,6 +1,6 @@ use alloc::string::String; use core::cmp; -use modit::{Event, Motion, Operator, Parser, TextObject, WordIter}; +use modit::{Event, Motion, Parser, TextObject, WordIter}; use unicode_segmentation::UnicodeSegmentation; use crate::{ @@ -191,7 +191,6 @@ impl<'a> Edit for ViEditor<'a> { Action::Delete => modit::DELETE, _ => return editor.action(font_system, action), }; - //TODO: redraw on parser mode change self.parser.parse(c, false, |event| { log::info!(" Event {:?}", event); let action = match event { From e8dd8ec7d1a75219980a653998552a78eb9ec0c0 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 10 Nov 2023 09:47:45 -0700 Subject: [PATCH 26/46] Support modit::Key enum --- src/edit/vi.rs | 422 ++++++++++++++++++++++++------------------------- 1 file changed, 205 insertions(+), 217 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 75cd0f2..2fbd000 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -1,6 +1,6 @@ use alloc::string::String; use core::cmp; -use modit::{Event, Motion, Parser, TextObject, WordIter}; +use modit::{Event, Key, Motion, Parser, TextObject, WordIter}; use unicode_segmentation::UnicodeSegmentation; use crate::{ @@ -183,15 +183,15 @@ impl<'a> Edit for ViEditor<'a> { fn action(&mut self, font_system: &mut FontSystem, action: Action) { let editor = &mut self.editor; log::info!("Action {:?}", action); - let c = match action { - Action::Escape => modit::ESCAPE, - Action::Insert(c) => c, - Action::Enter => modit::ENTER, - Action::Backspace => modit::BACKSPACE, - Action::Delete => modit::DELETE, + let key = match action { + Action::Backspace => Key::Backspace, + Action::Delete => Key::Delete, + Action::Enter => Key::Enter, + Action::Escape => Key::Escape, + Action::Insert(c) => Key::Char(c), _ => return editor.action(font_system, action), }; - self.parser.parse(c, false, |event| { + self.parser.parse(key, false, |event| { log::info!(" Event {:?}", event); let action = match event { Event::AutoIndent => { @@ -333,225 +333,213 @@ impl<'a> Edit for ViEditor<'a> { log::info!("TODO"); return; } - Event::Motion(motion, count) => { - for _ in 0..count { - let action = match motion { - Motion::Down => Action::Down, - Motion::End => Action::End, - Motion::GotoLine(line) => Action::GotoLine(line.saturating_sub(1)), - Motion::GotoEof => { - Action::GotoLine(editor.buffer().lines.len().saturating_sub(1)) + Event::Motion(motion) => { + match motion { + Motion::Down => Action::Down, + Motion::End => Action::End, + Motion::GotoLine(line) => Action::GotoLine(line.saturating_sub(1)), + Motion::GotoEof => { + Action::GotoLine(editor.buffer().lines.len().saturating_sub(1)) + } + Motion::Home => Action::Home, + Motion::Left => Action::Left, + Motion::NextChar(find_c) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer(); + let text = buffer.lines[cursor.line].text(); + if cursor.index < text.len() { + match text[cursor.index..] + .char_indices() + .filter(|&(i, c)| i > 0 && c == find_c) + .next() + { + Some((i, _)) => { + cursor.index += i; + editor.set_cursor(cursor); + } + None => {} + } } - Motion::Home => Action::Home, - Motion::Left => Action::Left, - Motion::NextChar(find_c) => { - let mut cursor = editor.cursor(); - let buffer = editor.buffer(); + return; + } + Motion::NextCharTill(find_c) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer(); + let text = buffer.lines[cursor.line].text(); + if cursor.index < text.len() { + let mut last_i = 0; + for (i, c) in text[cursor.index..].char_indices() { + if last_i > 0 && c == find_c { + cursor.index += last_i; + editor.set_cursor(cursor); + break; + } else { + last_i = i; + } + } + } + return; + } + Motion::NextWordEnd(word) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer(); + loop { let text = buffer.lines[cursor.line].text(); if cursor.index < text.len() { - match text[cursor.index..] - .char_indices() - .filter(|&(i, c)| i > 0 && c == find_c) - .next() - { - Some((i, _)) => { - cursor.index += i; - editor.set_cursor(cursor); - } - None => {} - } - } - continue; - } - Motion::NextCharTill(find_c) => { - let mut cursor = editor.cursor(); - let buffer = editor.buffer(); - let text = buffer.lines[cursor.line].text(); - if cursor.index < text.len() { - let mut last_i = 0; - for (i, c) in text[cursor.index..].char_indices() { - if last_i > 0 && c == find_c { - cursor.index += last_i; - editor.set_cursor(cursor); - break; - } else { - last_i = i; - } - } - } - continue; - } - Motion::NextWordEnd(word) => { - let mut cursor = editor.cursor(); - let buffer = editor.buffer(); - loop { - let text = buffer.lines[cursor.line].text(); - if cursor.index < text.len() { - cursor.index = WordIter::new(text, word) - .map(|(i, w)| { - i + w - .char_indices() - .last() - .map(|(i, _)| i) - .unwrap_or(0) - }) - .find(|&i| i > cursor.index) - .unwrap_or(text.len()); - if cursor.index == text.len() { - // Try again, searching next line - continue; - } - } else if cursor.line + 1 < buffer.lines.len() { - // Go to next line and rerun loop - cursor.line += 1; - cursor.index = 0; - continue; - } - break; - } - editor.set_cursor(cursor); - continue; - } - Motion::NextWordStart(word) => { - let mut cursor = editor.cursor(); - let buffer = editor.buffer(); - loop { - let text = buffer.lines[cursor.line].text(); - if cursor.index < text.len() { - cursor.index = WordIter::new(text, word) - .map(|(i, _)| i) - .find(|&i| i > cursor.index) - .unwrap_or(text.len()); - if cursor.index == text.len() { - // Try again, searching next line - continue; - } - } else if cursor.line + 1 < buffer.lines.len() { - // Go to next line and rerun loop - cursor.line += 1; - cursor.index = 0; - continue; - } - break; - } - editor.set_cursor(cursor); - continue; - } - Motion::PreviousChar(find_c) => { - let mut cursor = editor.cursor(); - let buffer = editor.buffer(); - let text = buffer.lines[cursor.line].text(); - if cursor.index > 0 { - match text[..cursor.index] - .char_indices() - .filter(|&(_, c)| c == find_c) - .last() - { - Some((i, _)) => { - cursor.index = i; - editor.set_cursor(cursor); - } - None => {} - } - } - continue; - } - Motion::PreviousCharTill(find_c) => { - let mut cursor = editor.cursor(); - let buffer = editor.buffer(); - let text = buffer.lines[cursor.line].text(); - if cursor.index > 0 { - match text[..cursor.index] - .char_indices() - .filter_map(|(i, c)| { - if c == find_c { - let end = i + c.len_utf8(); - if end < cursor.index { - return Some(end); - } - } - None + cursor.index = WordIter::new(text, word) + .map(|(i, w)| { + i + w.char_indices().last().map(|(i, _)| i).unwrap_or(0) }) - .last() - { - Some(i) => { - cursor.index = i; - editor.set_cursor(cursor); - } - None => {} - } - } - continue; - } - Motion::PreviousWordEnd(word) => { - let mut cursor = editor.cursor(); - let buffer = editor.buffer(); - loop { - let text = buffer.lines[cursor.line].text(); - if cursor.index > 0 { - cursor.index = WordIter::new(text, word) - .map(|(i, w)| { - i + w - .char_indices() - .last() - .map(|(i, _)| i) - .unwrap_or(0) - }) - .filter(|&i| i < cursor.index) - .last() - .unwrap_or(0); - if cursor.index == 0 { - // Try again, searching previous line - continue; - } - } else if cursor.line > 0 { - // Go to previous line and rerun loop - cursor.line -= 1; - cursor.index = buffer.lines[cursor.line].text().len(); + .find(|&i| i > cursor.index) + .unwrap_or(text.len()); + if cursor.index == text.len() { + // Try again, searching next line continue; } - break; + } else if cursor.line + 1 < buffer.lines.len() { + // Go to next line and rerun loop + cursor.line += 1; + cursor.index = 0; + continue; } - editor.set_cursor(cursor); - continue; - } - Motion::PreviousWordStart(word) => { - let mut cursor = editor.cursor(); - let buffer = editor.buffer(); - loop { - let text = buffer.lines[cursor.line].text(); - if cursor.index > 0 { - cursor.index = WordIter::new(text, word) - .map(|(i, _)| i) - .filter(|&i| i < cursor.index) - .last() - .unwrap_or(0); - if cursor.index == 0 { - // Try again, searching previous line - continue; - } - } else if cursor.line > 0 { - // Go to previous line and rerun loop - cursor.line -= 1; - cursor.index = buffer.lines[cursor.line].text().len(); - continue; - } - break; - } - editor.set_cursor(cursor); - continue; - } - Motion::Right => Action::Right, - Motion::SoftHome => Action::SoftHome, - Motion::Up => Action::Up, - _ => { - log::info!("TODO: {:?}", motion); break; } - }; - editor.action(font_system, action); + editor.set_cursor(cursor); + return; + } + Motion::NextWordStart(word) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer(); + loop { + let text = buffer.lines[cursor.line].text(); + if cursor.index < text.len() { + cursor.index = WordIter::new(text, word) + .map(|(i, _)| i) + .find(|&i| i > cursor.index) + .unwrap_or(text.len()); + if cursor.index == text.len() { + // Try again, searching next line + continue; + } + } else if cursor.line + 1 < buffer.lines.len() { + // Go to next line and rerun loop + cursor.line += 1; + cursor.index = 0; + continue; + } + break; + } + editor.set_cursor(cursor); + return; + } + Motion::PreviousChar(find_c) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer(); + let text = buffer.lines[cursor.line].text(); + if cursor.index > 0 { + match text[..cursor.index] + .char_indices() + .filter(|&(_, c)| c == find_c) + .last() + { + Some((i, _)) => { + cursor.index = i; + editor.set_cursor(cursor); + } + None => {} + } + } + return; + } + Motion::PreviousCharTill(find_c) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer(); + let text = buffer.lines[cursor.line].text(); + if cursor.index > 0 { + match text[..cursor.index] + .char_indices() + .filter_map(|(i, c)| { + if c == find_c { + let end = i + c.len_utf8(); + if end < cursor.index { + return Some(end); + } + } + None + }) + .last() + { + Some(i) => { + cursor.index = i; + editor.set_cursor(cursor); + } + None => {} + } + } + return; + } + Motion::PreviousWordEnd(word) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer(); + loop { + let text = buffer.lines[cursor.line].text(); + if cursor.index > 0 { + cursor.index = WordIter::new(text, word) + .map(|(i, w)| { + i + w.char_indices().last().map(|(i, _)| i).unwrap_or(0) + }) + .filter(|&i| i < cursor.index) + .last() + .unwrap_or(0); + if cursor.index == 0 { + // Try again, searching previous line + continue; + } + } else if cursor.line > 0 { + // Go to previous line and rerun loop + cursor.line -= 1; + cursor.index = buffer.lines[cursor.line].text().len(); + continue; + } + break; + } + editor.set_cursor(cursor); + return; + } + Motion::PreviousWordStart(word) => { + let mut cursor = editor.cursor(); + let buffer = editor.buffer(); + loop { + let text = buffer.lines[cursor.line].text(); + if cursor.index > 0 { + cursor.index = WordIter::new(text, word) + .map(|(i, _)| i) + .filter(|&i| i < cursor.index) + .last() + .unwrap_or(0); + if cursor.index == 0 { + // Try again, searching previous line + continue; + } + } else if cursor.line > 0 { + // Go to previous line and rerun loop + cursor.line -= 1; + cursor.index = buffer.lines[cursor.line].text().len(); + continue; + } + break; + } + editor.set_cursor(cursor); + return; + } + Motion::Right => Action::Right, + Motion::SoftHome => Action::SoftHome, + Motion::Up => Action::Up, + _ => { + log::info!("TODO: {:?}", motion); + return; + } } - return; } }; editor.action(font_system, action); From ddcd3c8795b05f502582fbae30c96b439033725a Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 10 Nov 2023 12:23:00 -0700 Subject: [PATCH 27/46] Support search --- src/edit/vi.rs | 248 +++++++++++++++++++++++++------------------------ 1 file changed, 126 insertions(+), 122 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 2fbd000..82e9d79 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -10,6 +10,114 @@ use crate::{ pub use modit::{ViMode, ViParser}; +fn search(editor: &mut E, search: &str, forwards: bool) { + let mut cursor = editor.cursor(); + let start_line = cursor.line; + if forwards { + while cursor.line < editor.buffer().lines.len() { + if let Some(index) = editor.buffer().lines[cursor.line] + .text() + .match_indices(search) + .filter_map(|(i, _)| { + if cursor.line != start_line || i > cursor.index { + Some(i) + } else { + None + } + }) + .next() + { + cursor.index = index; + editor.set_cursor(cursor); + return; + } + + cursor.line += 1; + } + } else { + cursor.line += 1; + while cursor.line > 0 { + cursor.line -= 1; + + if let Some(index) = editor.buffer().lines[cursor.line] + .text() + .rmatch_indices(search) + .filter_map(|(i, _)| { + if cursor.line != start_line || i < cursor.index { + Some(i) + } else { + None + } + }) + .next() + { + cursor.index = index; + editor.set_cursor(cursor); + return; + } + } + } +} + +fn select_in(editor: &mut E, start_c: char, end_c: char, include: bool) { + // Find the largest encompasing object, or if there is none, find the next one. + let cursor = editor.cursor(); + let buffer = editor.buffer(); + + // Search forwards for isolated end character, counting start and end characters found + let mut end = cursor; + let mut starts = 0; + let mut ends = 0; + 'find_end: loop { + let line = &buffer.lines[end.line]; + let text = line.text(); + for (i, c) in text[end.index..].char_indices() { + if c == end_c { + ends += 1; + } else if c == start_c { + starts += 1; + } + if ends > starts { + end.index += if include { i + c.len_utf8() } else { i }; + break 'find_end; + } + } + if end.line + 1 < buffer.lines.len() { + end.line += 1; + end.index = 0; + } else { + break 'find_end; + } + } + + // Search backwards to resolve starts and ends + let mut start = cursor; + 'find_start: loop { + let line = &buffer.lines[start.line]; + let text = line.text(); + for (i, c) in text[..start.index].char_indices().rev() { + if c == start_c { + starts += 1; + } else if c == end_c { + ends += 1; + } + if starts >= ends { + start.index = if include { i } else { i + c.len_utf8() }; + break 'find_start; + } + } + if start.line > 0 { + start.line -= 1; + start.index = buffer.lines[start.line].text().len(); + } else { + break 'find_start; + } + } + + editor.set_select_opt(Some(start)); + editor.set_cursor(end); +} + #[derive(Debug)] pub struct ViEditor<'a> { editor: SyntaxEditor<'a>, @@ -71,64 +179,6 @@ impl<'a> ViEditor<'a> { pub fn parser(&self) -> &ViParser { &self.parser } - - fn search(&mut self, inverted: bool) { - let (search, mut forwards) = match &self.search_opt { - Some(some) => some, - None => return, - }; - - if inverted { - forwards = !forwards; - } - - let mut cursor = self.cursor(); - let start_line = cursor.line; - if forwards { - while cursor.line < self.buffer().lines.len() { - if let Some(index) = self.buffer().lines[cursor.line] - .text() - .match_indices(search.as_str()) - .filter_map(|(i, _)| { - if cursor.line != start_line || i > cursor.index { - Some(i) - } else { - None - } - }) - .next() - { - cursor.index = index; - self.set_cursor(cursor); - return; - } - - cursor.line += 1; - } - } else { - cursor.line += 1; - while cursor.line > 0 { - cursor.line -= 1; - - if let Some(index) = self.buffer().lines[cursor.line] - .text() - .rmatch_indices(search.as_str()) - .filter_map(|(i, _)| { - if cursor.line != start_line || i < cursor.index { - Some(i) - } else { - None - } - }) - .next() - { - cursor.index = index; - self.set_cursor(cursor); - return; - } - } - } - } } impl<'a> Edit for ViEditor<'a> { @@ -225,70 +275,6 @@ impl<'a> Edit for ViEditor<'a> { return; } Event::SelectTextObject(text_object, include) => { - fn select_in( - editor: &mut SyntaxEditor, - start_c: char, - end_c: char, - include: bool, - ) { - // Find the largest encompasing object, or if there is none, find the next one. - let cursor = editor.cursor(); - let buffer = editor.buffer(); - - // Search forwards for isolated end character, counting start and end characters found - let mut end = cursor; - let mut starts = 0; - let mut ends = 0; - 'find_end: loop { - let line = &buffer.lines[end.line]; - let text = line.text(); - for (i, c) in text[end.index..].char_indices() { - if c == end_c { - ends += 1; - } else if c == start_c { - starts += 1; - } - if ends > starts { - end.index += if include { i + c.len_utf8() } else { i }; - break 'find_end; - } - } - if end.line + 1 < buffer.lines.len() { - end.line += 1; - end.index = 0; - } else { - break 'find_end; - } - } - - // Search backwards to resolve starts and ends - let mut start = cursor; - 'find_start: loop { - let line = &buffer.lines[start.line]; - let text = line.text(); - for (i, c) in text[..start.index].char_indices().rev() { - if c == start_c { - starts += 1; - } else if c == end_c { - ends += 1; - } - if starts >= ends { - start.index = if include { i } else { i + c.len_utf8() }; - break 'find_start; - } - } - if start.line > 0 { - start.line -= 1; - start.index = buffer.lines[start.line].text().len(); - } else { - break 'find_start; - } - } - - editor.set_select_opt(Some(start)); - editor.set_cursor(end); - } - match text_object { TextObject::AngleBrackets => select_in(editor, '<', '>', include), TextObject::CurlyBrackets => select_in(editor, '{', '}', include), @@ -323,6 +309,10 @@ impl<'a> Edit for ViEditor<'a> { } return; } + Event::SetSearch(value, forwards) => { + self.search_opt = Some((value, forwards)); + return; + } Event::ShiftLeft => Action::Unindent, Event::ShiftRight => Action::Indent, Event::SwapCase => { @@ -380,6 +370,13 @@ impl<'a> Edit for ViEditor<'a> { } return; } + Motion::NextSearch => match &self.search_opt { + Some((value, forwards)) => { + search(editor, value, *forwards); + return; + } + None => return, + }, Motion::NextWordEnd(word) => { let mut cursor = editor.cursor(); let buffer = editor.buffer(); @@ -478,6 +475,13 @@ impl<'a> Edit for ViEditor<'a> { } return; } + Motion::PreviousSearch => match &self.search_opt { + Some((value, forwards)) => { + search(editor, value, !*forwards); + return; + } + None => return, + }, Motion::PreviousWordEnd(word) => { let mut cursor = editor.cursor(); let buffer = editor.buffer(); From fbc33c183a5fd469c2a344045449f5f8760d83b7 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 10 Nov 2023 15:53:19 -0700 Subject: [PATCH 28/46] Convert more actions to modit keys, fix passthrough --- src/edit/vi.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 82e9d79..70b3959 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -233,13 +233,32 @@ impl<'a> Edit for ViEditor<'a> { fn action(&mut self, font_system: &mut FontSystem, action: Action) { let editor = &mut self.editor; log::info!("Action {:?}", action); + + if self.passthrough { + return editor.action(font_system, action); + } + let key = match action { + //TODO: this leaves lots of room for issues in translation, should we directly accept Key? Action::Backspace => Key::Backspace, Action::Delete => Key::Delete, + Action::Down => Key::Down, + Action::End => Key::End, Action::Enter => Key::Enter, Action::Escape => Key::Escape, + Action::Home => Key::Home, + Action::Indent => Key::Tab, Action::Insert(c) => Key::Char(c), - _ => return editor.action(font_system, action), + Action::Left => Key::Left, + Action::PageDown => Key::PageDown, + Action::PageUp => Key::PageUp, + Action::Right => Key::Right, + Action::Unindent => Key::Backtab, + Action::Up => Key::Up, + _ => { + log::info!("pass through action {:?}", action); + return editor.action(font_system, action); + } }; self.parser.parse(key, false, |event| { log::info!(" Event {:?}", event); From d001e5c09eef98ee0c3b1f549132924d32aa63ec Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Sun, 12 Nov 2023 19:01:20 -0700 Subject: [PATCH 29/46] Implement all modit motions required --- src/edit/vi.rs | 51 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 70b3959..0c0bed3 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -344,6 +344,10 @@ impl<'a> Edit for ViEditor<'a> { } Event::Motion(motion) => { match motion { + Motion::Around => { + //TODO: what to do for this psuedo-motion? + return; + } Motion::Down => Action::Down, Motion::End => Action::End, Motion::GotoLine(line) => Action::GotoLine(line.saturating_sub(1)), @@ -351,7 +355,15 @@ impl<'a> Edit for ViEditor<'a> { Action::GotoLine(editor.buffer().lines.len().saturating_sub(1)) } Motion::Home => Action::Home, + Motion::Inside => { + //TODO: what to do for this psuedo-motion? + return; + } Motion::Left => Action::Left, + Motion::Line => { + //TODO: what to do for this psuedo-motion? + return; + } Motion::NextChar(find_c) => { let mut cursor = editor.cursor(); let buffer = editor.buffer(); @@ -448,6 +460,8 @@ impl<'a> Edit for ViEditor<'a> { editor.set_cursor(cursor); return; } + Motion::PageDown => Action::PageDown, + Motion::PageUp => Action::PageUp, Motion::PreviousChar(find_c) => { let mut cursor = editor.cursor(); let buffer = editor.buffer(); @@ -556,12 +570,41 @@ impl<'a> Edit for ViEditor<'a> { return; } Motion::Right => Action::Right, - Motion::SoftHome => Action::SoftHome, - Motion::Up => Action::Up, - _ => { - log::info!("TODO: {:?}", motion); + Motion::ScreenHigh => { + //TODO: is this efficient? + if let Some(first) = editor.buffer().layout_runs().next() { + Action::GotoLine(first.line_i) + } else { + return; + } + } + Motion::ScreenLow => { + //TODO: is this efficient? + if let Some(last) = editor.buffer().layout_runs().last() { + Action::GotoLine(last.line_i) + } else { + return; + } + } + Motion::ScreenMiddle => { + //TODO: is this efficient? + let mut layout_runs = editor.buffer().layout_runs(); + if let Some(first) = layout_runs.next() { + if let Some(last) = layout_runs.last() { + Action::GotoLine((last.line_i + first.line_i) / 2) + } else { + return; + } + } else { + return; + } + } + Motion::Selection => { + //TODO: what to do for this psuedo-motion? return; } + Motion::SoftHome => Action::SoftHome, + Motion::Up => Action::Up, } } }; From b3c5f14e47540fa38f228a9b5a2841f8b616b974 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 13 Nov 2023 10:42:03 -0700 Subject: [PATCH 30/46] Remove two-face (it can be added by user of library) --- Cargo.toml | 1 - src/edit/syntect.rs | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 53a39bb..6b1012c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ self_cell = "1.0.1" swash = { version = "0.1.8", optional = true } syntect = { version = "5.1.0", optional = true } sys-locale = { version = "0.3.1", optional = true } -two-face = { version = "0.3.0", optional = true } unicode-linebreak = "0.1.5" unicode-script = "0.5.5" unicode-segmentation = "1.10.1" diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index 9ba06dd..ec55671 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -22,7 +22,6 @@ pub struct SyntaxSystem { impl SyntaxSystem { /// Create a new [`SyntaxSystem`] - #[cfg(not(feature = "two-face"))] pub fn new() -> Self { Self { //TODO: store newlines in buffer @@ -30,17 +29,6 @@ impl SyntaxSystem { theme_set: ThemeSet::load_defaults(), } } - - /// Create a new [`SyntaxSystem`] using `[two-face]` definitions - #[cfg(feature = "two-face")] - pub fn new() -> Self { - Self { - //TODO: store newlines in buffer - syntax_set: two_face::syntax::extra_no_newlines(), - //TODO: use two-face themes - theme_set: ThemeSet::load_defaults(), - } - } } /// A wrapper of [`Editor`] with syntax highlighting provided by [`SyntaxSystem`] From e942e649eddd0b13edb7dabe29c290972c142273 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 13 Nov 2023 11:10:05 -0700 Subject: [PATCH 31/46] Support LeftInLine and RightInLine motions --- src/edit/vi.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 0c0bed3..4ceddd1 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -360,6 +360,14 @@ impl<'a> Edit for ViEditor<'a> { return; } Motion::Left => Action::Left, + Motion::LeftInLine => { + let cursor = editor.cursor(); + if cursor.index > 0 { + Action::Left + } else { + return; + } + } Motion::Line => { //TODO: what to do for this psuedo-motion? return; @@ -570,6 +578,15 @@ impl<'a> Edit for ViEditor<'a> { return; } Motion::Right => Action::Right, + Motion::RightInLine => { + let cursor = editor.cursor(); + let buffer = editor.buffer(); + if cursor.index < buffer.lines[cursor.line].text().len() { + Action::Right + } else { + return; + } + } Motion::ScreenHigh => { //TODO: is this efficient? if let Some(first) = editor.buffer().layout_runs().next() { From 7830f4107c7202c09ac1e7d4093532518501ecfd Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 13 Nov 2023 12:37:07 -0700 Subject: [PATCH 32/46] Enable external change tracking --- src/edit/editor.rs | 421 +++++++++++++++++++++++--------------------- src/edit/mod.rs | 19 +- src/edit/syntect.rs | 12 +- src/edit/vi.rs | 10 +- 4 files changed, 253 insertions(+), 209 deletions(-) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 742370d..a9f8faf 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -11,8 +11,8 @@ use unicode_segmentation::UnicodeSegmentation; #[cfg(feature = "swash")] use crate::Color; use crate::{ - Action, Affinity, AttrsList, Buffer, BufferLine, Cursor, Edit, FontSystem, LayoutCursor, - Shaping, + Action, Affinity, AttrsList, Buffer, BufferLine, Change, ChangeItem, Cursor, Edit, FontSystem, + LayoutCursor, Shaping, }; /// A wrapper of [`Buffer`] for easy editing @@ -24,6 +24,7 @@ pub struct Editor { select_opt: Option, cursor_moved: bool, tab_width: usize, + change: Option, } impl Editor { @@ -36,6 +37,7 @@ impl Editor { select_opt: None, cursor_moved: false, tab_width: 4, + change: None, } } @@ -72,6 +74,142 @@ impl Editor { self.buffer.set_redraw(true); } } + + fn delete_range(&mut self, start: Cursor, end: Cursor) { + // Collect removed data for change tracking + let mut change_text = String::new(); + + // Delete the selection from the last line + let end_line_opt = if end.line > start.line { + // Get part of line after selection + let after = self.buffer.lines[end.line].split_off(end.index); + + // Remove end line + let removed = self.buffer.lines.remove(end.line); + change_text.insert_str(0, removed.text()); + + Some(after) + } else { + None + }; + + // Delete interior lines (in reverse for safety) + for line_i in (start.line + 1..end.line).rev() { + let removed = self.buffer.lines.remove(line_i); + change_text.insert_str(0, removed.text()); + } + + // Delete the selection from the first line + { + // Get part after selection if start line is also end line + let after_opt = if start.line == end.line { + Some(self.buffer.lines[start.line].split_off(end.index)) + } else { + None + }; + + // Delete selected part of line + let removed = self.buffer.lines[start.line].split_off(start.index); + change_text.insert_str(0, removed.text()); + + // Re-add part of line after selection + if let Some(after) = after_opt { + self.buffer.lines[start.line].append(after); + } + + // Re-add valid parts of end line + if let Some(end_line) = end_line_opt { + self.buffer.lines[start.line].append(end_line); + } + } + + if let Some(ref mut change) = self.change { + let item = ChangeItem::Delete(start, change_text); + change.items.push(item); + } + } + + fn insert_at( + &mut self, + mut cursor: Cursor, + data: &str, + attrs_list: Option, + ) -> Cursor { + let mut remaining_split_len = data.len(); + if remaining_split_len == 0 { + return cursor; + } + + if let Some(ref mut change) = self.change { + let item = ChangeItem::Insert(cursor, data.to_string()); + change.items.push(item); + } + + let line: &mut BufferLine = &mut self.buffer.lines[cursor.line]; + let insert_line = cursor.line + 1; + + // Collect text after insertion as a line + let after: BufferLine = line.split_off(cursor.index); + let after_len = after.text().len(); + + // Collect attributes + let mut final_attrs = attrs_list.unwrap_or_else(|| { + AttrsList::new(line.attrs_list().get_span(cursor.index.saturating_sub(1))) + }); + + // Append the inserted text, line by line + // we want to see a blank entry if the string ends with a newline + let addendum = once("").filter(|_| data.ends_with('\n')); + let mut lines_iter = data.split_inclusive('\n').chain(addendum); + if let Some(data_line) = lines_iter.next() { + let mut these_attrs = final_attrs.split_off(data_line.len()); + remaining_split_len -= data_line.len(); + core::mem::swap(&mut these_attrs, &mut final_attrs); + line.append(BufferLine::new( + data_line + .strip_suffix(char::is_control) + .unwrap_or(data_line), + these_attrs, + Shaping::Advanced, + )); + } else { + panic!("str::lines() did not yield any elements"); + } + if let Some(data_line) = lines_iter.next_back() { + remaining_split_len -= data_line.len(); + let mut tmp = BufferLine::new( + data_line + .strip_suffix(char::is_control) + .unwrap_or(data_line), + final_attrs.split_off(remaining_split_len), + Shaping::Advanced, + ); + tmp.append(after); + self.buffer.lines.insert(insert_line, tmp); + cursor.line += 1; + } else { + line.append(after); + } + for data_line in lines_iter.rev() { + remaining_split_len -= data_line.len(); + let tmp = BufferLine::new( + data_line + .strip_suffix(char::is_control) + .unwrap_or(data_line), + final_attrs.split_off(remaining_split_len), + Shaping::Advanced, + ); + self.buffer.lines.insert(insert_line, tmp); + cursor.line += 1; + } + + assert_eq!(remaining_split_len, 0); + + // Append the text after insertion + cursor.index = self.buffer.lines[cursor.line].text().len() - after_len; + + cursor + } } impl Edit for Editor { @@ -198,123 +336,25 @@ impl Edit for Editor { // Reset cursor to start of selection self.cursor = start; - // Delete the selection from the last line - let end_line_opt = if end.line > start.line { - // Get part of line after selection - let after = self.buffer.lines[end.line].split_off(end.index); - - // Remove end line - self.buffer.lines.remove(end.line); - - Some(after) - } else { - None - }; - - // Delete interior lines (in reverse for safety) - for line_i in (start.line + 1..end.line).rev() { - self.buffer.lines.remove(line_i); - } - - // Delete the selection from the first line - { - // Get part after selection if start line is also end line - let after_opt = if start.line == end.line { - Some(self.buffer.lines[start.line].split_off(end.index)) - } else { - None - }; - - // Delete selected part of line - self.buffer.lines[start.line].split_off(start.index); - - // Re-add part of line after selection - if let Some(after) = after_opt { - self.buffer.lines[start.line].append(after); - } - - // Re-add valid parts of end line - if let Some(end_line) = end_line_opt { - self.buffer.lines[start.line].append(end_line); - } - } + // Delete from start to end of selection + self.delete_range(start, end); true } fn insert_string(&mut self, data: &str, attrs_list: Option) { self.delete_selection(); - let mut remaining_split_len = data.len(); - if remaining_split_len == 0 { - return; - } + let new_cursor = self.insert_at(self.cursor, data, attrs_list); + self.set_cursor(new_cursor); + } - let line: &mut BufferLine = &mut self.buffer.lines[self.cursor.line]; - let insert_line = self.cursor.line + 1; + fn start_change(&mut self) { + //TODO: what to do if overwriting change? + self.change = Some(Change::default()); + } - // Collect text after insertion as a line - let after: BufferLine = line.split_off(self.cursor.index); - let after_len = after.text().len(); - - // Collect attributes - let mut final_attrs = attrs_list.unwrap_or_else(|| { - AttrsList::new( - line.attrs_list() - .get_span(self.cursor.index.saturating_sub(1)), - ) - }); - - // Append the inserted text, line by line - // we want to see a blank entry if the string ends with a newline - let addendum = once("").filter(|_| data.ends_with('\n')); - let mut lines_iter = data.split_inclusive('\n').chain(addendum); - if let Some(data_line) = lines_iter.next() { - let mut these_attrs = final_attrs.split_off(data_line.len()); - remaining_split_len -= data_line.len(); - core::mem::swap(&mut these_attrs, &mut final_attrs); - line.append(BufferLine::new( - data_line - .strip_suffix(char::is_control) - .unwrap_or(data_line), - these_attrs, - Shaping::Advanced, - )); - } else { - panic!("str::lines() did not yield any elements"); - } - if let Some(data_line) = lines_iter.next_back() { - remaining_split_len -= data_line.len(); - let mut tmp = BufferLine::new( - data_line - .strip_suffix(char::is_control) - .unwrap_or(data_line), - final_attrs.split_off(remaining_split_len), - Shaping::Advanced, - ); - tmp.append(after); - self.buffer.lines.insert(insert_line, tmp); - self.cursor.line += 1; - } else { - line.append(after); - } - for data_line in lines_iter.rev() { - remaining_split_len -= data_line.len(); - let tmp = BufferLine::new( - data_line - .strip_suffix(char::is_control) - .unwrap_or(data_line), - final_attrs.split_off(remaining_split_len), - Shaping::Advanced, - ); - self.buffer.lines.insert(insert_line, tmp); - self.cursor.line += 1; - } - - assert_eq!(remaining_split_len, 0); - - // Append the text after insertion - self.cursor.index = self.buffer.lines[self.cursor.line].text().len() - after_len; - self.cursor_moved = true; + fn finish_change(&mut self) -> Option { + self.change.take() } fn action(&mut self, font_system: &mut FontSystem, action: Action) { @@ -322,7 +362,7 @@ impl Edit for Editor { match action { Action::Previous => { - let line = &mut self.buffer.lines[self.cursor.line]; + let line = &self.buffer.lines[self.cursor.line]; if self.cursor.index > 0 { // Find previous character index let mut prev_index = 0; @@ -346,7 +386,7 @@ impl Edit for Editor { self.cursor_x_opt = None; } Action::Next => { - let line = &mut self.buffer.lines[self.cursor.line]; + let line = &self.buffer.lines[self.cursor.line]; if self.cursor.index < line.text().len() { for (i, c) in line.text().grapheme_indices(true) { if i == self.cursor.index { @@ -449,7 +489,7 @@ impl Edit for Editor { self.cursor_x_opt = None; } Action::SoftHome => { - let line: &mut BufferLine = &mut self.buffer.lines[self.cursor.line]; + let line = &self.buffer.lines[self.cursor.line]; self.cursor.index = line .text() .char_indices() @@ -516,14 +556,7 @@ impl Edit for Editor { } } Action::Enter => { - self.delete_selection(); - - let new_line = self.buffer.lines[self.cursor.line].split_off(self.cursor.index); - - self.cursor.line += 1; - self.cursor.index = 0; - - self.buffer.lines.insert(self.cursor.line, new_line); + self.insert_string("\n", None); // Ensure line is properly shaped and laid out (for potential immediate commands) self.buffer.line_layout(font_system, self.cursor.line); @@ -531,70 +564,60 @@ impl Edit for Editor { Action::Backspace => { if self.delete_selection() { // Deleted selection - } else if self.cursor.index > 0 { - let line = &mut self.buffer.lines[self.cursor.line]; + } else { + // Save current cursor as end + let end = self.cursor; - // Get text line after cursor - let after = line.split_off(self.cursor.index); - - // Find previous character index - let mut prev_index = 0; - for (i, _) in line.text().char_indices() { - if i < self.cursor.index { - prev_index = i; - } else { - break; - } + if self.cursor.index > 0 { + // Move cursor to previous character index + let line = &self.buffer.lines[self.cursor.line]; + self.cursor.index = line.text()[..self.cursor.index] + .char_indices() + .rev() + .next() + .map_or(0, |(i, _)| i); + } else if self.cursor.line > 0 { + // Move cursor to previous line + self.cursor.line -= 1; + let line = &self.buffer.lines[self.cursor.line]; + self.cursor.index = line.text().len(); } - self.cursor.index = prev_index; - - // Remove character - line.split_off(self.cursor.index); - - // Add text after cursor - line.append(after); - } else if self.cursor.line > 0 { - let mut line_index = self.cursor.line; - let old_line = self.buffer.lines.remove(line_index); - line_index -= 1; - - let line = &mut self.buffer.lines[line_index]; - - self.cursor.line = line_index; - self.cursor.index = line.text().len(); - - line.append(old_line); + if self.cursor != end { + // Delete range + self.delete_range(self.cursor, end); + } } } Action::Delete => { if self.delete_selection() { // Deleted selection - } else if self.cursor.index < self.buffer.lines[self.cursor.line].text().len() { - let line = &mut self.buffer.lines[self.cursor.line]; + } else { + // Save current cursor as end + let mut end = self.cursor; - let range_opt = line - .text() - .grapheme_indices(true) - .take_while(|(i, _)| *i <= self.cursor.index) - .last() - .map(|(i, c)| i..(i + c.len())); + if self.cursor.index < self.buffer.lines[self.cursor.line].text().len() { + let line = &self.buffer.lines[self.cursor.line]; - if let Some(range) = range_opt { - self.cursor.index = range.start; + let range_opt = line + .text() + .grapheme_indices(true) + .take_while(|(i, _)| *i <= self.cursor.index) + .last() + .map(|(i, c)| i..(i + c.len())); - // Get text after deleted EGC - let after = line.split_off(range.end); - - // Delete EGC - line.split_off(range.start); - - // Add text after deleted EGC - line.append(after); + if let Some(range) = range_opt { + self.cursor.index = range.start; + end.index = range.end; + } + } else if self.cursor.line + 1 < self.buffer.lines.len() { + end.line += 1; + end.index = 0; + } + + if self.cursor != end { + self.delete_range(self.cursor, end); } - } else if self.cursor.line + 1 < self.buffer.lines.len() { - let old_line = self.buffer.lines.remove(self.cursor.line + 1); - self.buffer.lines[self.cursor.line].append(old_line); } } Action::Indent => { @@ -618,12 +641,11 @@ impl Edit for Editor { // For every line in selection for line_i in start.line..=end.line { - let line = &mut self.buffer.lines[line_i]; - // Determine indexes of last indent and first character after whitespace let mut after_whitespace = 0; let mut required_indent = 0; { + let line = &self.buffer.lines[line_i]; let text = line.text(); for (count, (index, c)) in text.char_indices().enumerate() { if !c.is_whitespace() { @@ -636,28 +658,22 @@ impl Edit for Editor { // No indent required (not possible?) if required_indent == 0 { - continue; + required_indent = self.tab_width; } - // Save line after last whitespace - let after = line.split_off(after_whitespace); - - // Add required indent - line.append(BufferLine::new( - " ".repeat(required_indent), - AttrsList::new(line.attrs_list().defaults()), - Shaping::Advanced, - )); - - // Re-add line after last whitespace - line.append(after); + self.insert_at( + Cursor::new(line_i, after_whitespace), + &" ".repeat(required_indent), + None, + ); // Adjust cursor if self.cursor.line == line_i { - if self.cursor.index >= after_whitespace { - self.cursor.index += required_indent; - self.cursor_moved = true; + //TODO: should we be forcing cursor index to current indent location? + if self.cursor.index < after_whitespace { + self.cursor.index = after_whitespace; } + self.cursor.index += required_indent; } // Adjust selection @@ -697,12 +713,11 @@ impl Edit for Editor { // For every line in selection for line_i in start.line..=end.line { - let line = &mut self.buffer.lines[line_i]; - // Determine indexes of last indent and first character after whitespace let mut last_indent = 0; let mut after_whitespace = 0; { + let line = &self.buffer.lines[line_i]; let text = line.text(); for (count, (index, c)) in text.char_indices().enumerate() { if !c.is_whitespace() { @@ -720,20 +735,16 @@ impl Edit for Editor { continue; } - // Save line after last whitespace - let after = line.split_off(after_whitespace); - - // Drop part of line after last indent - line.split_off(last_indent); - - // Re-add line after last whitespace - line.append(after); + // Delete one indent + self.delete_range( + Cursor::new(line_i, last_indent), + Cursor::new(line_i, after_whitespace), + ); // Adjust cursor if self.cursor.line == line_i { if self.cursor.index > last_indent { self.cursor.index -= after_whitespace - last_indent; - self.cursor_moved = true; } } @@ -786,7 +797,7 @@ impl Edit for Editor { self.buffer.set_scroll(scroll); } Action::PreviousWord => { - let line: &mut BufferLine = &mut self.buffer.lines[self.cursor.line]; + let line = &self.buffer.lines[self.cursor.line]; if self.cursor.index > 0 { self.cursor.index = line .text() @@ -805,7 +816,7 @@ impl Edit for Editor { self.cursor_x_opt = None; } Action::NextWord => { - let line: &mut BufferLine = &mut self.buffer.lines[self.cursor.line]; + let line = &self.buffer.lines[self.cursor.line]; if self.cursor.index < line.text().len() { self.cursor.index = line .text() diff --git a/src/edit/mod.rs b/src/edit/mod.rs index 9c43a96..e5027a7 100644 --- a/src/edit/mod.rs +++ b/src/edit/mod.rs @@ -1,5 +1,5 @@ #[cfg(not(feature = "std"))] -use alloc::string::String; +use alloc::{string::String, vec::Vec}; #[cfg(feature = "swash")] use crate::Color; @@ -93,6 +93,17 @@ pub enum Action { GotoLine(usize), } +#[derive(Clone, Debug)] +pub enum ChangeItem { + Delete(Cursor, String), + Insert(Cursor, String), +} + +#[derive(Clone, Debug, Default)] +pub struct Change { + items: Vec, +} + /// A trait to allow easy replacements of [`Editor`], like `SyntaxEditor` pub trait Edit { /// Mutably borrows `self` together with an [`FontSystem`] for more convenient methods @@ -147,6 +158,12 @@ pub trait Edit { /// attributes, or with the previous character's attributes if None is given. fn insert_string(&mut self, data: &str, attrs_list: Option); + /// Start collecting change + fn start_change(&mut self); + + /// Get completed change + fn finish_change(&mut self) -> Option; + /// Perform an [Action] on the editor fn action(&mut self, font_system: &mut FontSystem, action: Action); diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index ec55671..75b401a 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -8,8 +8,8 @@ use syntect::highlighting::{ use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet}; use crate::{ - Action, AttrsList, BorrowedWithFontSystem, Buffer, Color, Cursor, Edit, Editor, FontSystem, - Shaping, Style, Weight, Wrap, + Action, AttrsList, BorrowedWithFontSystem, Buffer, Change, Color, Cursor, Edit, Editor, + FontSystem, Shaping, Style, Weight, Wrap, }; pub use syntect::highlighting::Theme as SyntaxTheme; @@ -304,6 +304,14 @@ impl<'a> Edit for SyntaxEditor<'a> { self.editor.insert_string(data, attrs_list); } + fn start_change(&mut self) { + self.editor.start_change(); + } + + fn finish_change(&mut self) -> Option { + self.editor.finish_change() + } + fn action(&mut self, font_system: &mut FontSystem, action: Action) { self.editor.action(font_system, action); } diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 4ceddd1..f5e7c73 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -4,7 +4,7 @@ use modit::{Event, Key, Motion, Parser, TextObject, WordIter}; use unicode_segmentation::UnicodeSegmentation; use crate::{ - Action, AttrsList, BorrowedWithFontSystem, Buffer, Color, Cursor, Edit, FontSystem, + Action, AttrsList, BorrowedWithFontSystem, Buffer, Change, Color, Cursor, Edit, FontSystem, SyntaxEditor, SyntaxTheme, }; @@ -230,6 +230,14 @@ impl<'a> Edit for ViEditor<'a> { self.editor.insert_string(data, attrs_list); } + fn start_change(&mut self) { + self.editor.start_change(); + } + + fn finish_change(&mut self) -> Option { + self.editor.finish_change() + } + fn action(&mut self, font_system: &mut FontSystem, action: Action) { let editor = &mut self.editor; log::info!("Action {:?}", action); From 5352fdee940ef009ea978837afdfc44c468f7be8 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 13 Nov 2023 13:31:06 -0700 Subject: [PATCH 33/46] Undo/redo support in ViEditor --- Cargo.toml | 3 +- src/edit/editor.rs | 54 ++++++++++++-- src/edit/mod.rs | 37 ++++++++-- src/edit/syntect.rs | 4 ++ src/edit/vi.rs | 168 +++++++++++++++++++++++++++++--------------- 5 files changed, 198 insertions(+), 68 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6b1012c..5c73a8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ self_cell = "1.0.1" swash = { version = "0.1.8", optional = true } syntect = { version = "5.1.0", optional = true } sys-locale = { version = "0.3.1", optional = true } +undo_2 = { version = "0.2.0", optional = true } unicode-linebreak = "0.1.5" unicode-script = "0.5.5" unicode-segmentation = "1.10.1" @@ -46,7 +47,7 @@ std = [ "sys-locale", "unicode-bidi/std", ] -vi = ["modit", "syntect"] +vi = ["modit", "syntect", "undo_2"] wasm-web = ["sys-locale?/js"] warn_on_missing_glyphs = [] fontconfig = ["fontdb/fontconfig", "std"] diff --git a/src/edit/editor.rs b/src/edit/editor.rs index a9f8faf..1ab8639 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -124,7 +124,12 @@ impl Editor { } if let Some(ref mut change) = self.change { - let item = ChangeItem::Delete(start, change_text); + let item = ChangeItem { + start, + end, + text: change_text, + insert: false, + }; change.items.push(item); } } @@ -140,10 +145,8 @@ impl Editor { return cursor; } - if let Some(ref mut change) = self.change { - let item = ChangeItem::Insert(cursor, data.to_string()); - change.items.push(item); - } + // Save cursor for change tracking + let start = cursor; let line: &mut BufferLine = &mut self.buffer.lines[cursor.line]; let insert_line = cursor.line + 1; @@ -208,6 +211,16 @@ impl Editor { // Append the text after insertion cursor.index = self.buffer.lines[cursor.line].text().len() - after_len; + if let Some(ref mut change) = self.change { + let item = ChangeItem { + start, + end: cursor, + text: data.to_string(), + insert: true, + }; + change.items.push(item); + } + cursor } } @@ -348,9 +361,36 @@ impl Edit for Editor { self.set_cursor(new_cursor); } + fn apply_change(&mut self, change: &Change) -> bool { + // Cannot apply changes if there is a pending change + match self.change.take() { + Some(pending) => { + if !pending.items.is_empty() { + //TODO: is this a good idea? + log::warn!("pending change caused apply_change to be ignored!"); + self.change = Some(pending); + return false; + } + } + None => {} + } + + for item in change.items.iter() { + //TODO: edit cursor if needed? + if item.insert { + self.cursor = self.insert_at(item.start, &item.text, None); + } else { + self.cursor = item.start; + self.delete_range(item.start, item.end); + } + } + true + } + fn start_change(&mut self) { - //TODO: what to do if overwriting change? - self.change = Some(Change::default()); + if self.change.is_none() { + self.change = Some(Change::default()); + } } fn finish_change(&mut self) -> Option { diff --git a/src/edit/mod.rs b/src/edit/mod.rs index e5027a7..fbe66c2 100644 --- a/src/edit/mod.rs +++ b/src/edit/mod.rs @@ -93,15 +93,41 @@ pub enum Action { GotoLine(usize), } +/// A unique change to an editor #[derive(Clone, Debug)] -pub enum ChangeItem { - Delete(Cursor, String), - Insert(Cursor, String), +pub struct ChangeItem { + /// Cursor indicating start of change + pub start: Cursor, + /// Cursor indicating end of change + pub end: Cursor, + /// Text to be inserted or deleted + pub text: String, + /// Insert if true, delete if false + pub insert: bool, } +impl ChangeItem { + // Reverse change item (in place) + pub fn reverse(&mut self) { + self.insert = !self.insert; + } +} + +/// A set of change items grouped into one logical change #[derive(Clone, Debug, Default)] pub struct Change { - items: Vec, + /// Change items grouped into one change + pub items: Vec, +} + +impl Change { + // Reverse change (in place) + pub fn reverse(&mut self) { + self.items.reverse(); + for item in self.items.iter_mut() { + item.reverse(); + } + } } /// A trait to allow easy replacements of [`Editor`], like `SyntaxEditor` @@ -158,6 +184,9 @@ pub trait Edit { /// attributes, or with the previous character's attributes if None is given. fn insert_string(&mut self, data: &str, attrs_list: Option); + /// Apply a change + fn apply_change(&mut self, change: &Change) -> bool; + /// Start collecting change fn start_change(&mut self); diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index 75b401a..ff6ccd5 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -304,6 +304,10 @@ impl<'a> Edit for SyntaxEditor<'a> { self.editor.insert_string(data, attrs_list); } + fn apply_change(&mut self, change: &Change) -> bool { + self.editor.apply_change(change) + } + fn start_change(&mut self) { self.editor.start_change(); } diff --git a/src/edit/vi.rs b/src/edit/vi.rs index f5e7c73..331ec50 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -1,6 +1,7 @@ use alloc::string::String; use core::cmp; use modit::{Event, Key, Motion, Parser, TextObject, WordIter}; +use undo_2::Commands; use unicode_segmentation::UnicodeSegmentation; use crate::{ @@ -10,6 +11,20 @@ use crate::{ pub use modit::{ViMode, ViParser}; +fn undo_2_action(editor: &mut E, action: undo_2::Action<&Change>) { + match action { + undo_2::Action::Do(change) => { + editor.apply_change(change); + } + undo_2::Action::Undo(change) => { + //TODO: make this more efficient + let mut reversed = change.clone(); + reversed.reverse(); + editor.apply_change(&reversed); + } + } +} + fn search(editor: &mut E, search: &str, forwards: bool) { let mut cursor = editor.cursor(); let start_line = cursor.line; @@ -124,6 +139,7 @@ pub struct ViEditor<'a> { parser: ViParser, passthrough: bool, search_opt: Option<(String, bool)>, + commands: Commands, } impl<'a> ViEditor<'a> { @@ -133,6 +149,7 @@ impl<'a> ViEditor<'a> { parser: ViParser::new(), passthrough: false, search_opt: None, + commands: Commands::new(), } } @@ -179,66 +196,24 @@ impl<'a> ViEditor<'a> { pub fn parser(&self) -> &ViParser { &self.parser } -} -impl<'a> Edit for ViEditor<'a> { - fn buffer(&self) -> &Buffer { - self.editor.buffer() + /// Redo a change + pub fn redo(&mut self) { + log::info!("Redo"); + for action in self.commands.redo() { + undo_2_action(&mut self.editor, action); + } } - fn buffer_mut(&mut self) -> &mut Buffer { - self.editor.buffer_mut() + /// Undo a change + pub fn undo(&mut self) { + log::info!("Undo"); + for action in self.commands.undo() { + undo_2_action(&mut self.editor, action); + } } - fn cursor(&self) -> Cursor { - self.editor.cursor() - } - - fn set_cursor(&mut self, cursor: Cursor) { - self.editor.set_cursor(cursor); - } - - fn select_opt(&self) -> Option { - self.editor.select_opt() - } - - fn set_select_opt(&mut self, select_opt: Option) { - self.editor.set_select_opt(select_opt); - } - - fn tab_width(&self) -> usize { - self.editor.tab_width() - } - - fn set_tab_width(&mut self, tab_width: usize) { - self.editor.set_tab_width(tab_width); - } - - fn shape_as_needed(&mut self, font_system: &mut FontSystem) { - self.editor.shape_as_needed(font_system); - } - - fn copy_selection(&self) -> Option { - self.editor.copy_selection() - } - - fn delete_selection(&mut self) -> bool { - self.editor.delete_selection() - } - - fn insert_string(&mut self, data: &str, attrs_list: Option) { - self.editor.insert_string(data, attrs_list); - } - - fn start_change(&mut self) { - self.editor.start_change(); - } - - fn finish_change(&mut self) -> Option { - self.editor.finish_change() - } - - fn action(&mut self, font_system: &mut FontSystem, action: Action) { + fn action_inner(&mut self, font_system: &mut FontSystem, action: Action) { let editor = &mut self.editor; log::info!("Action {:?}", action); @@ -268,6 +243,7 @@ impl<'a> Edit for ViEditor<'a> { return editor.action(font_system, action); } }; + self.parser.parse(key, false, |event| { log::info!(" Event {:?}", event); let action = match event { @@ -347,7 +323,9 @@ impl<'a> Edit for ViEditor<'a> { return; } Event::Undo => { - log::info!("TODO"); + for action in self.commands.undo() { + undo_2_action(editor, action); + } return; } Event::Motion(motion) => { @@ -636,6 +614,84 @@ impl<'a> Edit for ViEditor<'a> { editor.action(font_system, action); }); } +} + +impl<'a> Edit for ViEditor<'a> { + fn buffer(&self) -> &Buffer { + self.editor.buffer() + } + + fn buffer_mut(&mut self) -> &mut Buffer { + self.editor.buffer_mut() + } + + fn cursor(&self) -> Cursor { + self.editor.cursor() + } + + fn set_cursor(&mut self, cursor: Cursor) { + self.editor.set_cursor(cursor); + } + + fn select_opt(&self) -> Option { + self.editor.select_opt() + } + + fn set_select_opt(&mut self, select_opt: Option) { + self.editor.set_select_opt(select_opt); + } + + fn tab_width(&self) -> usize { + self.editor.tab_width() + } + + fn set_tab_width(&mut self, tab_width: usize) { + self.editor.set_tab_width(tab_width); + } + + fn shape_as_needed(&mut self, font_system: &mut FontSystem) { + self.editor.shape_as_needed(font_system); + } + + fn copy_selection(&self) -> Option { + self.editor.copy_selection() + } + + fn delete_selection(&mut self) -> bool { + self.editor.delete_selection() + } + + fn insert_string(&mut self, data: &str, attrs_list: Option) { + self.editor.insert_string(data, attrs_list); + } + + fn apply_change(&mut self, change: &Change) -> bool { + self.editor.apply_change(change) + } + + fn start_change(&mut self) { + self.editor.start_change(); + } + + fn finish_change(&mut self) -> Option { + self.editor.finish_change() + } + + fn action(&mut self, font_system: &mut FontSystem, action: Action) { + self.start_change(); + + self.action_inner(font_system, action); + + match self.finish_change() { + Some(change) => { + if !change.items.is_empty() { + log::info!("{:?}", change); + self.commands.push(change); + } + } + None => {} + } + } #[cfg(feature = "swash")] fn draw( From 4c85a6be721b729350acddcc8a4396353786b901 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 13 Nov 2023 14:46:46 -0700 Subject: [PATCH 34/46] ViEditor: Track when changed --- src/edit/vi.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 331ec50..abe5473 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -140,6 +140,7 @@ pub struct ViEditor<'a> { passthrough: bool, search_opt: Option<(String, bool)>, commands: Commands, + changed: bool, } impl<'a> ViEditor<'a> { @@ -150,6 +151,7 @@ impl<'a> ViEditor<'a> { passthrough: false, search_opt: None, commands: Commands::new(), + changed: false, } } @@ -184,6 +186,16 @@ impl<'a> ViEditor<'a> { self.editor.theme() } + /// Get changed flag + pub fn changed(&self) -> bool { + self.changed + } + + /// Set changed flag + pub fn set_changed(&mut self, changed: bool) { + self.changed = changed; + } + /// Set passthrough mode (true will turn off vi features) pub fn set_passthrough(&mut self, passthrough: bool) { if passthrough != self.passthrough { @@ -202,6 +214,8 @@ impl<'a> ViEditor<'a> { log::info!("Redo"); for action in self.commands.redo() { undo_2_action(&mut self.editor, action); + //TODO: clear changed flag when back to last saved state? + self.changed = true; } } @@ -210,6 +224,8 @@ impl<'a> ViEditor<'a> { log::info!("Undo"); for action in self.commands.undo() { undo_2_action(&mut self.editor, action); + //TODO: clear changed flag when back to last saved state? + self.changed = true; } } @@ -682,11 +698,12 @@ impl<'a> Edit for ViEditor<'a> { self.action_inner(font_system, action); + //TODO: join changes together match self.finish_change() { Some(change) => { if !change.items.is_empty() { - log::info!("{:?}", change); self.commands.push(change); + self.changed = true; } } None => {} From 0eefb12608b96c62272b7ddd61c5216a8ca1afe1 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 14 Nov 2023 09:03:36 -0700 Subject: [PATCH 35/46] Editor: Fix indent/unindent empty lines --- src/edit/editor.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 1ab8639..151e6f7 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -682,11 +682,13 @@ impl Edit for Editor { // For every line in selection for line_i in start.line..=end.line { // Determine indexes of last indent and first character after whitespace - let mut after_whitespace = 0; + let mut after_whitespace; let mut required_indent = 0; { let line = &self.buffer.lines[line_i]; let text = line.text(); + // Default to end of line if no non-whitespace found + after_whitespace = text.len(); for (count, (index, c)) in text.char_indices().enumerate() { if !c.is_whitespace() { after_whitespace = index; @@ -755,10 +757,12 @@ impl Edit for Editor { for line_i in start.line..=end.line { // Determine indexes of last indent and first character after whitespace let mut last_indent = 0; - let mut after_whitespace = 0; + let mut after_whitespace; { let line = &self.buffer.lines[line_i]; let text = line.text(); + // Default to end of line if no non-whitespace found + after_whitespace = text.len(); for (count, (index, c)) in text.char_indices().enumerate() { if !c.is_whitespace() { after_whitespace = index; From bab94a782300f1d54c7b2b2715560f15b1623ab1 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 14 Nov 2023 12:28:56 -0700 Subject: [PATCH 36/46] Join together vim changes --- src/edit/vi.rs | 181 ++++++++++++++++++++++++++----------------------- 1 file changed, 98 insertions(+), 83 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index abe5473..f1d6f63 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -25,6 +25,23 @@ fn undo_2_action(editor: &mut E, action: undo_2::Action<&Change>) { } } +fn finish_change( + editor: &mut E, + commands: &mut undo_2::Commands, + changed: &mut bool, +) { + //TODO: join changes together + match editor.finish_change() { + Some(change) => { + if !change.items.is_empty() { + commands.push(change); + *changed = true; + } + } + None => {} + } +} + fn search(editor: &mut E, search: &str, forwards: bool) { let mut cursor = editor.cursor(); let start_line = cursor.line; @@ -228,13 +245,79 @@ impl<'a> ViEditor<'a> { self.changed = true; } } +} - fn action_inner(&mut self, font_system: &mut FontSystem, action: Action) { - let editor = &mut self.editor; +impl<'a> Edit for ViEditor<'a> { + fn buffer(&self) -> &Buffer { + self.editor.buffer() + } + + fn buffer_mut(&mut self) -> &mut Buffer { + self.editor.buffer_mut() + } + + fn cursor(&self) -> Cursor { + self.editor.cursor() + } + + fn set_cursor(&mut self, cursor: Cursor) { + self.editor.set_cursor(cursor); + } + + fn select_opt(&self) -> Option { + self.editor.select_opt() + } + + fn set_select_opt(&mut self, select_opt: Option) { + self.editor.set_select_opt(select_opt); + } + + fn tab_width(&self) -> usize { + self.editor.tab_width() + } + + fn set_tab_width(&mut self, tab_width: usize) { + self.editor.set_tab_width(tab_width); + } + + fn shape_as_needed(&mut self, font_system: &mut FontSystem) { + self.editor.shape_as_needed(font_system); + } + + fn copy_selection(&self) -> Option { + self.editor.copy_selection() + } + + fn delete_selection(&mut self) -> bool { + self.editor.delete_selection() + } + + fn insert_string(&mut self, data: &str, attrs_list: Option) { + self.editor.insert_string(data, attrs_list); + } + + fn apply_change(&mut self, change: &Change) -> bool { + self.editor.apply_change(change) + } + + fn start_change(&mut self) { + self.editor.start_change(); + } + + fn finish_change(&mut self) -> Option { + self.editor.finish_change() + } + + fn action(&mut self, font_system: &mut FontSystem, action: Action) { log::info!("Action {:?}", action); + let editor = &mut self.editor; + if self.passthrough { - return editor.action(font_system, action); + editor.start_change(); + editor.action(font_system, action); + finish_change(editor, &mut self.commands, &mut self.changed); + return; } let key = match action { @@ -256,7 +339,10 @@ impl<'a> ViEditor<'a> { Action::Up => Key::Up, _ => { log::info!("pass through action {:?}", action); - return editor.action(font_system, action); + editor.start_change(); + editor.action(font_system, action); + finish_change(editor, &mut self.commands, &mut self.changed); + return; } }; @@ -268,6 +354,14 @@ impl<'a> ViEditor<'a> { return; } Event::Backspace => Action::Backspace, + Event::ChangeStart => { + editor.start_change(); + return; + } + Event::ChangeFinish => { + finish_change(editor, &mut self.commands, &mut self.changed); + return; + } Event::Copy => { log::info!("TODO"); return; @@ -630,85 +724,6 @@ impl<'a> ViEditor<'a> { editor.action(font_system, action); }); } -} - -impl<'a> Edit for ViEditor<'a> { - fn buffer(&self) -> &Buffer { - self.editor.buffer() - } - - fn buffer_mut(&mut self) -> &mut Buffer { - self.editor.buffer_mut() - } - - fn cursor(&self) -> Cursor { - self.editor.cursor() - } - - fn set_cursor(&mut self, cursor: Cursor) { - self.editor.set_cursor(cursor); - } - - fn select_opt(&self) -> Option { - self.editor.select_opt() - } - - fn set_select_opt(&mut self, select_opt: Option) { - self.editor.set_select_opt(select_opt); - } - - fn tab_width(&self) -> usize { - self.editor.tab_width() - } - - fn set_tab_width(&mut self, tab_width: usize) { - self.editor.set_tab_width(tab_width); - } - - fn shape_as_needed(&mut self, font_system: &mut FontSystem) { - self.editor.shape_as_needed(font_system); - } - - fn copy_selection(&self) -> Option { - self.editor.copy_selection() - } - - fn delete_selection(&mut self) -> bool { - self.editor.delete_selection() - } - - fn insert_string(&mut self, data: &str, attrs_list: Option) { - self.editor.insert_string(data, attrs_list); - } - - fn apply_change(&mut self, change: &Change) -> bool { - self.editor.apply_change(change) - } - - fn start_change(&mut self) { - self.editor.start_change(); - } - - fn finish_change(&mut self) -> Option { - self.editor.finish_change() - } - - fn action(&mut self, font_system: &mut FontSystem, action: Action) { - self.start_change(); - - self.action_inner(font_system, action); - - //TODO: join changes together - match self.finish_change() { - Some(change) => { - if !change.items.is_empty() { - self.commands.push(change); - self.changed = true; - } - } - None => {} - } - } #[cfg(feature = "swash")] fn draw( From abf58279be4747399a240de7a0e1ada7ba5275e5 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 14 Nov 2023 13:23:00 -0700 Subject: [PATCH 37/46] Implement TextObject::Search --- src/edit/vi.rs | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index f1d6f63..10aa758 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -42,14 +42,14 @@ fn finish_change( } } -fn search(editor: &mut E, search: &str, forwards: bool) { +fn search(editor: &mut E, value: &str, forwards: bool) -> bool { let mut cursor = editor.cursor(); let start_line = cursor.line; if forwards { while cursor.line < editor.buffer().lines.len() { if let Some(index) = editor.buffer().lines[cursor.line] .text() - .match_indices(search) + .match_indices(value) .filter_map(|(i, _)| { if cursor.line != start_line || i > cursor.index { Some(i) @@ -61,7 +61,7 @@ fn search(editor: &mut E, search: &str, forwards: bool) { { cursor.index = index; editor.set_cursor(cursor); - return; + return true; } cursor.line += 1; @@ -73,7 +73,7 @@ fn search(editor: &mut E, search: &str, forwards: bool) { if let Some(index) = editor.buffer().lines[cursor.line] .text() - .rmatch_indices(search) + .rmatch_indices(value) .filter_map(|(i, _)| { if cursor.line != start_line || i < cursor.index { Some(i) @@ -85,10 +85,11 @@ fn search(editor: &mut E, search: &str, forwards: bool) { { cursor.index = index; editor.set_cursor(cursor); - return; + return true; } } } + false } fn select_in(editor: &mut E, start_c: char, end_c: char, include: bool) { @@ -313,9 +314,12 @@ impl<'a> Edit for ViEditor<'a> { let editor = &mut self.editor; + // Ensure a change is always started + editor.start_change(); + if self.passthrough { - editor.start_change(); editor.action(font_system, action); + // Always finish change when passing through (TODO: group changes) finish_change(editor, &mut self.commands, &mut self.changed); return; } @@ -339,8 +343,8 @@ impl<'a> Edit for ViEditor<'a> { Action::Up => Key::Up, _ => { log::info!("pass through action {:?}", action); - editor.start_change(); editor.action(font_system, action); + // Always finish change when passing through (TODO: group changes) finish_change(editor, &mut self.commands, &mut self.changed); return; } @@ -393,6 +397,20 @@ impl<'a> Edit for ViEditor<'a> { TextObject::CurlyBrackets => select_in(editor, '{', '}', include), TextObject::DoubleQuotes => select_in(editor, '"', '"', include), TextObject::Parentheses => select_in(editor, '(', ')', include), + TextObject::Search { forwards } => { + match &self.search_opt { + Some((value, _)) => { + if search(editor, value, forwards) { + let mut cursor = editor.cursor(); + editor.set_select_opt(Some(cursor)); + //TODO: traverse lines if necessary + cursor.index += value.len(); + editor.set_cursor(cursor); + } + } + None => {} + } + } TextObject::SingleQuotes => select_in(editor, '\'', '\'', include), TextObject::SquareBrackets => select_in(editor, '[', ']', include), TextObject::Ticks => select_in(editor, '`', '`', include), From 56f71ef9736f852fd47163c30ade198e289207cc Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 14 Nov 2023 13:43:33 -0700 Subject: [PATCH 38/46] Shape if needed to process left/right commands --- src/edit/editor.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 151e6f7..80fb525 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -445,9 +445,9 @@ impl Edit for Editor { self.cursor_x_opt = None; } Action::Left => { - let rtl_opt = self.buffer.lines[self.cursor.line] - .shape_opt() - .as_ref() + let rtl_opt = self + .buffer + .line_shape(font_system, self.cursor.line) .map(|shape| shape.rtl); if let Some(rtl) = rtl_opt { if rtl { @@ -458,9 +458,9 @@ impl Edit for Editor { } } Action::Right => { - let rtl_opt = self.buffer.lines[self.cursor.line] - .shape_opt() - .as_ref() + let rtl_opt = self + .buffer + .line_shape(font_system, self.cursor.line) .map(|shape| shape.rtl); if let Some(rtl) = rtl_opt { if rtl { @@ -878,9 +878,9 @@ impl Edit for Editor { self.cursor_x_opt = None; } Action::LeftWord => { - let rtl_opt = self.buffer.lines[self.cursor.line] - .shape_opt() - .as_ref() + let rtl_opt = self + .buffer + .line_shape(font_system, self.cursor.line) .map(|shape| shape.rtl); if let Some(rtl) = rtl_opt { if rtl { @@ -891,9 +891,9 @@ impl Edit for Editor { } } Action::RightWord => { - let rtl_opt = self.buffer.lines[self.cursor.line] - .shape_opt() - .as_ref() + let rtl_opt = self + .buffer + .line_shape(font_system, self.cursor.line) .map(|shape| shape.rtl); if let Some(rtl) = rtl_opt { if rtl { From 38bed64ef14ff4b702f505385ba53d7269f30026 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 15 Nov 2023 09:05:57 -0700 Subject: [PATCH 39/46] Use cosmic_undo_2 instead of undo_2 for improved compiler support --- Cargo.toml | 4 ++-- src/edit/vi.rs | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5c73a8f..f16684f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/pop-os/cosmic-text" rust-version = "1.65" [dependencies] +cosmic_undo_2 = { version = "0.2.0", optional = true } fontdb = { version = "0.15.0", default-features = false } hashbrown = { version = "0.14.1", optional = true, default-features = false } libm = "0.2.8" @@ -21,7 +22,6 @@ self_cell = "1.0.1" swash = { version = "0.1.8", optional = true } syntect = { version = "5.1.0", optional = true } sys-locale = { version = "0.3.1", optional = true } -undo_2 = { version = "0.2.0", optional = true } unicode-linebreak = "0.1.5" unicode-script = "0.5.5" unicode-segmentation = "1.10.1" @@ -47,7 +47,7 @@ std = [ "sys-locale", "unicode-bidi/std", ] -vi = ["modit", "syntect", "undo_2"] +vi = ["modit", "syntect", "cosmic_undo_2"] wasm-web = ["sys-locale?/js"] warn_on_missing_glyphs = [] fontconfig = ["fontdb/fontconfig", "std"] diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 10aa758..03bb840 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -1,7 +1,6 @@ use alloc::string::String; use core::cmp; use modit::{Event, Key, Motion, Parser, TextObject, WordIter}; -use undo_2::Commands; use unicode_segmentation::UnicodeSegmentation; use crate::{ @@ -11,12 +10,12 @@ use crate::{ pub use modit::{ViMode, ViParser}; -fn undo_2_action(editor: &mut E, action: undo_2::Action<&Change>) { +fn undo_2_action(editor: &mut E, action: cosmic_undo_2::Action<&Change>) { match action { - undo_2::Action::Do(change) => { + cosmic_undo_2::Action::Do(change) => { editor.apply_change(change); } - undo_2::Action::Undo(change) => { + cosmic_undo_2::Action::Undo(change) => { //TODO: make this more efficient let mut reversed = change.clone(); reversed.reverse(); @@ -27,7 +26,7 @@ fn undo_2_action(editor: &mut E, action: undo_2::Action<&Change>) { fn finish_change( editor: &mut E, - commands: &mut undo_2::Commands, + commands: &mut cosmic_undo_2::Commands, changed: &mut bool, ) { //TODO: join changes together @@ -157,7 +156,7 @@ pub struct ViEditor<'a> { parser: ViParser, passthrough: bool, search_opt: Option<(String, bool)>, - commands: Commands, + commands: cosmic_undo_2::Commands, changed: bool, } @@ -168,7 +167,7 @@ impl<'a> ViEditor<'a> { parser: ViParser::new(), passthrough: false, search_opt: None, - commands: Commands::new(), + commands: cosmic_undo_2::Commands::new(), changed: false, } } From 6536231dfc213be7bbb7f7e6a0ab14745a8831c5 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 15 Nov 2023 09:09:37 -0700 Subject: [PATCH 40/46] Fix no_std compilation --- src/edit/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 80fb525..a7693af 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 #[cfg(not(feature = "std"))] -use alloc::string::String; +use alloc::string::{String, ToString}; use core::{ cmp::{self, Ordering}, iter::once, From 19ae07bd3beadabcc3b9e4d9e3234ea4504cb0ed Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 15 Nov 2023 09:21:13 -0700 Subject: [PATCH 41/46] Fix some clippy lints --- src/buffer.rs | 14 +++----------- src/edit/editor.rs | 31 +++++++++---------------------- src/edit/mod.rs | 2 +- src/lib.rs | 4 ++-- src/shape.rs | 8 +++----- 5 files changed, 18 insertions(+), 41 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 9d23f63..c67547c 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,10 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 #[cfg(not(feature = "std"))] -use alloc::{ - string::{String, ToString}, - vec::Vec, -}; +use alloc::{string::String, vec::Vec}; use core::{cmp, fmt}; use unicode_segmentation::UnicodeSegmentation; @@ -54,8 +51,9 @@ impl Cursor { } /// Whether to associate cursors placed at a boundary between runs with the run before or after it. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] pub enum Affinity { + #[default] Before, After, } @@ -86,12 +84,6 @@ impl Affinity { } } -impl Default for Affinity { - fn default() -> Self { - Affinity::Before - } -} - /// The position of a cursor within a [`Buffer`]. #[derive(Debug)] pub struct LayoutCursor { diff --git a/src/edit/editor.rs b/src/edit/editor.rs index a7693af..357ff29 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -613,8 +613,7 @@ impl Edit for Editor { let line = &self.buffer.lines[self.cursor.line]; self.cursor.index = line.text()[..self.cursor.index] .char_indices() - .rev() - .next() + .next_back() .map_or(0, |(i, _)| i); } else if self.cursor.line > 0 { // Move cursor to previous line @@ -719,15 +718,10 @@ impl Edit for Editor { } // Adjust selection - match self.select_opt { - Some(ref mut select) => { - if select.line == line_i { - if select.index >= after_whitespace { - select.index += required_indent; - } - } + if let Some(ref mut select) = self.select_opt { + if select.line == line_i && select.index >= after_whitespace { + select.index += required_indent; } - None => {} } // Request redraw @@ -786,22 +780,15 @@ impl Edit for Editor { ); // Adjust cursor - if self.cursor.line == line_i { - if self.cursor.index > last_indent { - self.cursor.index -= after_whitespace - last_indent; - } + if self.cursor.line == line_i && self.cursor.index > last_indent { + self.cursor.index -= after_whitespace - last_indent; } // Adjust selection - match self.select_opt { - Some(ref mut select) => { - if select.line == line_i { - if select.index > last_indent { - select.index -= after_whitespace - last_indent; - } - } + if let Some(ref mut select) = self.select_opt { + if select.line == line_i && select.index > last_indent { + select.index -= after_whitespace - last_indent; } - None => {} } // Request redraw diff --git a/src/edit/mod.rs b/src/edit/mod.rs index fbe66c2..be93795 100644 --- a/src/edit/mod.rs +++ b/src/edit/mod.rs @@ -167,7 +167,7 @@ pub trait Edit { /// Get the current tab width fn tab_width(&self) -> usize; - /// Set the current tab width. A tab_width of 0 is not allowed, and will be ignored + /// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored fn set_tab_width(&mut self, tab_width: usize); /// Shape lines until scroll, after adjusting scroll if the cursor moved diff --git a/src/lib.rs b/src/lib.rs index ac684b9..52b48f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,10 +61,10 @@ #![allow(clippy::new_without_default)] // TODO: address occurrences and then deny // +// Overflows can produce unpredictable results and are only checked in debug builds +#![allow(clippy::arithmetic_side_effects)] // Indexing a slice can cause panics and that is something we always want to avoid #![allow(clippy::indexing_slicing)] -// Overflows can produce unpredictable results and are only checked in debug builds -#![allow(clippy::integer_arithmetic)] // Soundness issues // // Dereferencing unaligned pointers may be undefined behavior diff --git a/src/shape.rs b/src/shape.rs index 53cba57..e4e5f99 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -1251,12 +1251,10 @@ impl ShapeLine { layout_lines.push(LayoutLine { w: if align != Align::Justified { visual_line.w + } else if self.rtl { + start_x - x } else { - if self.rtl { - start_x - x - } else { - x - } + x }, max_ascent: max_ascent * font_size, max_descent: max_descent * font_size, From 27d447b6fcaf82a7e072f6df6fe3d0de00225b1d Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 15 Nov 2023 12:42:51 -0700 Subject: [PATCH 42/46] Use fontdb 0.16 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f16684f..31dc0e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ rust-version = "1.65" [dependencies] cosmic_undo_2 = { version = "0.2.0", optional = true } -fontdb = { version = "0.15.0", default-features = false } +fontdb = { version = "0.16.0", default-features = false } hashbrown = { version = "0.14.1", optional = true, default-features = false } libm = "0.2.8" log = "0.4.20" From 8024cbe504fbe3b73df4222b2a9d64e7d32d5c6d Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 15 Nov 2023 12:43:14 -0700 Subject: [PATCH 43/46] Fix redoxer script --- redoxer.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/redoxer.sh b/redoxer.sh index ba39b4c..bc7310c 100755 --- a/redoxer.sh +++ b/redoxer.sh @@ -10,12 +10,12 @@ redoxer install \ --path examples/editor-orbclient \ --root "target/redoxer" -cmd="env RUST_LOG=cosmic_text=debug,editor_orbclient=debug ./bin/editor-orbclient" +args=(env RUST_LOG=cosmic_text=debug,editor_orbclient=debug /root/bin/editor-orbclient) if [ -f "$1" ] then filename="$(basename "$1")" cp "$1" "target/redoxer/${filename}" - cmd="${cmd} '${filename}'" + args+=("${filename}") fi cd target/redoxer @@ -24,5 +24,4 @@ cd target/redoxer redoxer exec \ --gui \ --folder . \ - /bin/sh -c \ - "${cmd}" + "${args[@]}" From 1207fd6d804b76b52137a368a58b27150eb865b8 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 16 Nov 2023 08:38:48 -0700 Subject: [PATCH 44/46] Edit: use u16 for tab_width --- src/edit/editor.rs | 14 ++++++++------ src/edit/mod.rs | 4 ++-- src/edit/syntect.rs | 4 ++-- src/edit/vi.rs | 4 ++-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 357ff29..43360ef 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -23,7 +23,7 @@ pub struct Editor { cursor_x_opt: Option, select_opt: Option, cursor_moved: bool, - tab_width: usize, + tab_width: u16, change: Option, } @@ -258,11 +258,11 @@ impl Edit for Editor { } } - fn tab_width(&self) -> usize { + fn tab_width(&self) -> u16 { self.tab_width } - fn set_tab_width(&mut self, tab_width: usize) { + fn set_tab_width(&mut self, tab_width: u16) { // A tab width of 0 is not allowed if tab_width == 0 { return; @@ -679,6 +679,7 @@ impl Edit for Editor { }; // For every line in selection + let tab_width: usize = self.tab_width.into(); for line_i in start.line..=end.line { // Determine indexes of last indent and first character after whitespace let mut after_whitespace; @@ -691,7 +692,7 @@ impl Edit for Editor { for (count, (index, c)) in text.char_indices().enumerate() { if !c.is_whitespace() { after_whitespace = index; - required_indent = self.tab_width - (count % self.tab_width); + required_indent = tab_width - (count % tab_width); break; } } @@ -699,7 +700,7 @@ impl Edit for Editor { // No indent required (not possible?) if required_indent == 0 { - required_indent = self.tab_width; + required_indent = tab_width; } self.insert_at( @@ -748,6 +749,7 @@ impl Edit for Editor { }; // For every line in selection + let tab_width: usize = self.tab_width.into(); for line_i in start.line..=end.line { // Determine indexes of last indent and first character after whitespace let mut last_indent = 0; @@ -762,7 +764,7 @@ impl Edit for Editor { after_whitespace = index; break; } - if count % self.tab_width == 0 { + if count % tab_width == 0 { last_indent = index; } } diff --git a/src/edit/mod.rs b/src/edit/mod.rs index be93795..595c704 100644 --- a/src/edit/mod.rs +++ b/src/edit/mod.rs @@ -165,10 +165,10 @@ pub trait Edit { fn set_select_opt(&mut self, select_opt: Option); /// Get the current tab width - fn tab_width(&self) -> usize; + fn tab_width(&self) -> u16; /// Set the current tab width. A `tab_width` of 0 is not allowed, and will be ignored - fn set_tab_width(&mut self, tab_width: usize); + fn set_tab_width(&mut self, tab_width: u16); /// Shape lines until scroll, after adjusting scroll if the cursor moved fn shape_as_needed(&mut self, font_system: &mut FontSystem); diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index ff6ccd5..3125d12 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -166,11 +166,11 @@ impl<'a> Edit for SyntaxEditor<'a> { self.editor.set_select_opt(select_opt); } - fn tab_width(&self) -> usize { + fn tab_width(&self) -> u16 { self.editor.tab_width() } - fn set_tab_width(&mut self, tab_width: usize) { + fn set_tab_width(&mut self, tab_width: u16) { self.editor.set_tab_width(tab_width); } diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 03bb840..118f0b5 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -272,11 +272,11 @@ impl<'a> Edit for ViEditor<'a> { self.editor.set_select_opt(select_opt); } - fn tab_width(&self) -> usize { + fn tab_width(&self) -> u16 { self.editor.tab_width() } - fn set_tab_width(&mut self, tab_width: usize) { + fn set_tab_width(&mut self, tab_width: u16) { self.editor.set_tab_width(tab_width); } From 7d21045b2f93a382f7ddea209b8f28204888d418 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 16 Nov 2023 08:59:43 -0700 Subject: [PATCH 45/46] Add primitive auto indent --- src/edit/editor.rs | 29 ++++++++++++++++++++++++++++- src/edit/mod.rs | 6 ++++++ src/edit/syntect.rs | 8 ++++++++ src/edit/vi.rs | 8 ++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 43360ef..fe026ed 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -23,6 +23,7 @@ pub struct Editor { cursor_x_opt: Option, select_opt: Option, cursor_moved: bool, + auto_indent: bool, tab_width: u16, change: Option, } @@ -36,6 +37,7 @@ impl Editor { cursor_x_opt: None, select_opt: None, cursor_moved: false, + auto_indent: false, tab_width: 4, change: None, } @@ -258,6 +260,14 @@ impl Edit for Editor { } } + fn auto_indent(&self) -> bool { + self.auto_indent + } + + fn set_auto_indent(&mut self, auto_indent: bool) { + self.auto_indent = auto_indent; + } + fn tab_width(&self) -> u16 { self.tab_width } @@ -596,7 +606,24 @@ impl Edit for Editor { } } Action::Enter => { - self.insert_string("\n", None); + //TODO: what about indenting more after opening brackets or parentheses? + if self.auto_indent { + let mut string = String::from("\n"); + { + let line = &self.buffer.lines[self.cursor.line]; + let text = line.text(); + for c in text.chars() { + if c.is_whitespace() { + string.push(c); + } else { + break; + } + } + } + self.insert_string(&string, None); + } else { + self.insert_string("\n", None); + } // Ensure line is properly shaped and laid out (for potential immediate commands) self.buffer.line_layout(font_system, self.cursor.line); diff --git a/src/edit/mod.rs b/src/edit/mod.rs index 595c704..7976537 100644 --- a/src/edit/mod.rs +++ b/src/edit/mod.rs @@ -164,6 +164,12 @@ pub trait Edit { /// Set the current selection position fn set_select_opt(&mut self, select_opt: Option); + /// Get the current automatic indentation setting + fn auto_indent(&self) -> bool; + + /// Enable or disable automatic indentation + fn set_auto_indent(&mut self, auto_indent: bool); + /// Get the current tab width fn tab_width(&self) -> u16; diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index 3125d12..ae91963 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -166,6 +166,14 @@ impl<'a> Edit for SyntaxEditor<'a> { self.editor.set_select_opt(select_opt); } + fn auto_indent(&self) -> bool { + self.editor.auto_indent() + } + + fn set_auto_indent(&mut self, auto_indent: bool) { + self.editor.set_auto_indent(auto_indent); + } + fn tab_width(&self) -> u16 { self.editor.tab_width() } diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 118f0b5..db88372 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -272,6 +272,14 @@ impl<'a> Edit for ViEditor<'a> { self.editor.set_select_opt(select_opt); } + fn auto_indent(&self) -> bool { + self.editor.auto_indent() + } + + fn set_auto_indent(&mut self, auto_indent: bool) { + self.editor.set_auto_indent(auto_indent); + } + fn tab_width(&self) -> u16 { self.editor.tab_width() } From 1201d0c8b556859f5f9ef1e62a5e2f2369792e7a Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 17 Nov 2023 07:53:24 -0700 Subject: [PATCH 46/46] Use crates.io modit --- Cargo.toml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 31dc0e2..ddb78a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ fontdb = { version = "0.16.0", default-features = false } hashbrown = { version = "0.14.1", optional = true, default-features = false } libm = "0.2.8" log = "0.4.20" +modit = { version = "0.1.0", optional = true } rangemap = "1.4.0" rustc-hash = { version = "1.1.0", default-features = false } rustybuzz = { version = "0.11.0", default-features = false, features = ["libm"] } @@ -26,12 +27,6 @@ unicode-linebreak = "0.1.5" unicode-script = "0.5.5" unicode-segmentation = "1.10.1" -#TODO: crates release -[dependencies.modit] -git = "https://github.com/pop-os/modit.git" -optional = true -#path = "../modit" - [dependencies.unicode-bidi] version = "0.3.13" default-features = false