diff --git a/crates/ferrite-core/src/buffer.rs b/crates/ferrite-core/src/buffer.rs index 3949a86..24efe32 100644 --- a/crates/ferrite-core/src/buffer.rs +++ b/crates/ferrite-core/src/buffer.rs @@ -778,7 +778,7 @@ impl Buffer { if text.is_empty() { return; } - // TODO collect multiple words/whitespace chars into single undo step + self.history.begin(self.cursor, self.dirty); fn get_pair_char(s: &str) -> Option<&str> { @@ -796,7 +796,7 @@ impl Buffer { let lines = Rope::from_str(text).len_lines(); - let inserted_bytes = if self.cursor.has_selection() { + let (inserted_bytes, finish) = if self.cursor.has_selection() { let start_byte_idx = self.cursor.position.min(self.cursor.anchor); let end_byte_idx = self.cursor.position.max(self.cursor.anchor); if let Some(pair) = get_pair_char(text) { @@ -810,7 +810,7 @@ impl Buffer { self.cursor.position = self.cursor.position.min(self.cursor.anchor); self.cursor.anchor = self.cursor.position; } - text.len() + (text.len(), false) } else if auto_indent && lines > 1 { let indent = self.guess_indent(self.cursor.position); let min_indent_width = Rope::from_str(&indent).width(0); @@ -861,7 +861,7 @@ impl Buffer { self.history .insert(&mut self.rope, self.cursor.position + text.len(), pair); }*/ - input.len() + (input.len(), true) } else { self.history .insert(&mut self.rope, self.cursor.position, text); @@ -869,7 +869,7 @@ impl Buffer { self.history .insert(&mut self.rope, self.cursor.position + text.len(), pair); }*/ - text.len() + (text.len(), false) }; self.cursor.position += inserted_bytes; @@ -881,7 +881,10 @@ impl Buffer { if self.clamp_cursor { self.center_on_cursor(); } - self.history.finish(); + + if finish { + self.history.finish(); + } } pub fn backspace(&mut self) { @@ -939,7 +942,6 @@ impl Buffer { if self.clamp_cursor { self.center_on_cursor(); } - self.history.finish(); } pub fn backspace_word(&mut self) { @@ -992,7 +994,6 @@ impl Buffer { if self.clamp_cursor { self.center_on_cursor(); } - self.history.finish(); } pub fn delete_word(&mut self) { @@ -1116,6 +1117,7 @@ impl Buffer { if self.clamp_cursor { self.center_on_cursor(); } + self.history.finish(); } pub fn tab(&mut self, back: bool) { @@ -1325,11 +1327,13 @@ impl Buffer { pub fn paste(&mut self) { self.insert_text(&clipboard::get_contents(), true); + self.history.finish(); } pub fn paste_primary(&mut self, col: usize, line: usize) { self.set_cursor_pos(col, line); self.insert_text(&clipboard::get_primary(), true); + self.history.finish(); } // TODO make this not use eof @@ -1372,6 +1376,7 @@ impl Buffer { self.history.save(); self.queue_syntax_update(); + self.history.finish(); Ok(()) } diff --git a/crates/ferrite-core/src/buffer/format.rs b/crates/ferrite-core/src/buffer/format.rs index 01b8feb..3369d8c 100644 --- a/crates/ferrite-core/src/buffer/format.rs +++ b/crates/ferrite-core/src/buffer/format.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use ferrite_utility::graphemes::ensure_grapheme_boundary_next_byte; +use ferrite_utility::graphemes::RopeGraphemeExt; use ropey::Rope; use subprocess::{Exec, PopenError, Redirection}; @@ -98,10 +98,10 @@ impl Buffer { let len = self.rope.len_bytes(); self.history.replace(&mut self.rope, 0..len, &new_rope); - let pos = ensure_grapheme_boundary_next_byte( - self.rope.slice(..), - self.cursor.position.min(self.rope.len_bytes()), - ); + // TODO position curser better then using byte offset + let pos = self + .rope + .ensure_grapheme_boundary_next_byte(self.cursor.position.min(self.rope.len_bytes())); self.cursor.position = pos; self.cursor.anchor = pos; @@ -128,10 +128,10 @@ impl Buffer { let len = self.rope.len_bytes(); self.history.replace(&mut self.rope, 0..len, &new_rope); - let pos = ensure_grapheme_boundary_next_byte( - self.rope.slice(..), - self.cursor.position.min(self.rope.len_bytes()), - ); + // TODO position curser better then using byte offset + let pos = self + .rope + .ensure_grapheme_boundary_next_byte(self.cursor.position.min(self.rope.len_bytes())); self.cursor.position = pos; self.cursor.anchor = pos; diff --git a/crates/ferrite-core/src/buffer/history.rs b/crates/ferrite-core/src/buffer/history.rs index aa500cc..f5c70ee 100644 --- a/crates/ferrite-core/src/buffer/history.rs +++ b/crates/ferrite-core/src/buffer/history.rs @@ -1,18 +1,58 @@ use std::{mem, ops::Range}; -use ferrite_utility::graphemes::ensure_grapheme_boundary_next_byte; +use ferrite_utility::graphemes::RopeGraphemeExt; use ropey::Rope; use super::Cursor; +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +enum EditClass { + Word, + WhiteSpace, + Other, + Remove, +} + +impl EditClass { + fn mergeable(first: &EditClass, second: &EditClass) -> bool { + match (first, second) { + (EditClass::WhiteSpace, EditClass::WhiteSpace) => true, + (EditClass::Word, EditClass::Word) => true, + (EditClass::Remove, EditClass::Remove) => true, + (EditClass::WhiteSpace, EditClass::Word) => true, + _ => false, + } + } +} + +impl From<&str> for EditClass { + fn from(value: &str) -> Self { + if Rope::from_str(value).is_word_char() { + return EditClass::Word; + } + if Rope::from_str(value).is_whitespace() { + return EditClass::WhiteSpace; + } + EditClass::Other + } +} + #[derive(Debug, Clone)] enum EditKind { Insert { byte_idx: usize, text: String }, - Remove { range: Range }, Replace { range: Range, text: String }, + Remove { range: Range }, } impl EditKind { + fn get_class(&self) -> EditClass { + match self { + EditKind::Insert { text, .. } => EditClass::from(text.as_str()), + EditKind::Replace { text, .. } => EditClass::from(text.as_str()), + EditKind::Remove { .. } => EditClass::Remove, + } + } + fn apply(&self, rope: &mut Rope) -> EditKind { match self { Self::Insert { byte_idx, text } => { @@ -21,14 +61,6 @@ impl EditKind { range: *byte_idx..(*byte_idx + text.len()), } } - Self::Remove { range } => { - let text = rope.byte_slice(range.clone()).to_string(); - rope.remove(rope.byte_to_char(range.start)..rope.byte_to_char(range.end)); - Self::Insert { - byte_idx: range.start, - text, - } - } Self::Replace { range, text } => { let old = rope.byte_slice(range.clone()).to_string(); let char_range = rope.byte_to_char(range.start)..rope.byte_to_char(range.end); @@ -39,6 +71,14 @@ impl EditKind { text: old, } } + Self::Remove { range } => { + let text = rope.byte_slice(range.clone()).to_string(); + rope.remove(rope.byte_to_char(range.start)..rope.byte_to_char(range.end)); + Self::Insert { + byte_idx: range.start, + text, + } + } } } } @@ -46,6 +86,7 @@ impl EditKind { #[derive(Debug, Clone)] struct Frame { finished: bool, + edit_class: EditClass, cursor: Cursor, edits: Vec, dirty: bool, @@ -70,10 +111,9 @@ impl History { fn edit(&mut self, rope: &mut Rope, edit: EditKind) { match self.stack.last_mut() { Some(frame) => { + frame.edit_class = edit.get_class(); let inverse = edit.apply(rope); - if !frame.finished { - frame.edits.push(inverse); - } + frame.edits.push(inverse); } None => tracing::error!("Edited rope before starting new edit frame"), } @@ -105,6 +145,7 @@ impl History { self.stack.push(Frame { finished: false, + edit_class: EditClass::Other, cursor, edits: Vec::new(), dirty, @@ -116,7 +157,7 @@ impl History { pub fn finish(&mut self) { // maybe should be current_frame - if let Some(frame) = self.stack.last_mut() { + if let Some(frame) = self.stack.get_mut(self.current_frame as usize) { if !frame.finished { frame.finished = true; } @@ -128,33 +169,66 @@ impl History { return; } - let frame = &mut self.stack[self.current_frame as usize]; - for edit in frame.edits.iter_mut().rev() { - *edit = edit.apply(rope); - } - mem::swap(&mut frame.cursor, cursor); - mem::swap(&mut frame.dirty, dirty); - cursor.position = ensure_grapheme_boundary_next_byte(rope.slice(..), cursor.position); - cursor.anchor = ensure_grapheme_boundary_next_byte(rope.slice(..), cursor.anchor); + let mut last_class = None; + + while let Some(frame) = &mut self.stack.get_mut(self.current_frame as usize) { + for edit in frame.edits.iter_mut().rev() { + *edit = edit.apply(rope); + } + mem::swap(&mut frame.cursor, cursor); + mem::swap(&mut frame.dirty, dirty); + cursor.position = rope.ensure_grapheme_boundary_next_byte(cursor.position); + cursor.anchor = rope.ensure_grapheme_boundary_next_byte(cursor.anchor); + self.current_frame -= 1; + + if frame.finished { + break; + } - self.current_frame -= 1; + if let Some(frame) = &mut self.stack.get_mut(self.current_frame as usize) { + let earlier_class = frame.edit_class; + if let Some(last_class) = last_class { + if !EditClass::mergeable(&earlier_class, &last_class) { + break; + } + } + last_class = Some(earlier_class); + } + } } pub fn redo(&mut self, rope: &mut Rope, cursor: &mut Cursor, dirty: &mut bool) { - if self.current_frame + 1 >= self.stack.len() as i64 { - return; - } + let mut last_class = None; - self.current_frame += 1; + loop { + if self.current_frame + 1 >= self.stack.len() as i64 { + return; + } + self.current_frame += 1; + let frame = &mut self.stack[self.current_frame as usize]; - let frame = &mut self.stack[self.current_frame as usize]; - for edit in &mut frame.edits { - *edit = edit.apply(rope); + for edit in &mut frame.edits { + *edit = edit.apply(rope); + } + mem::swap(&mut frame.cursor, cursor); + mem::swap(&mut frame.dirty, dirty); + cursor.position = rope.ensure_grapheme_boundary_next_byte(cursor.position); + cursor.anchor = rope.ensure_grapheme_boundary_next_byte(cursor.anchor); + + if frame.finished { + break; + } + + if let Some(frame) = &mut self.stack.get_mut(self.current_frame as usize + 1) { + let earlier_class = frame.edit_class; + if let Some(last_class) = last_class { + if !EditClass::mergeable(&last_class, &earlier_class) { + break; + } + } + last_class = Some(earlier_class); + } } - mem::swap(&mut frame.cursor, cursor); - mem::swap(&mut frame.dirty, dirty); - cursor.position = ensure_grapheme_boundary_next_byte(rope.slice(..), cursor.position); - cursor.anchor = ensure_grapheme_boundary_next_byte(rope.slice(..), cursor.anchor); } pub fn save(&mut self) { diff --git a/crates/ferrite-utility/src/graphemes.rs b/crates/ferrite-utility/src/graphemes.rs index 7c4e4cd..4866940 100644 --- a/crates/ferrite-utility/src/graphemes.rs +++ b/crates/ferrite-utility/src/graphemes.rs @@ -267,7 +267,7 @@ pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize /// or the next grapheme boundary byte index if not. #[must_use] #[inline] -pub fn ensure_grapheme_boundary_next_byte(slice: RopeSlice, byte_idx: usize) -> usize { +fn ensure_grapheme_boundary_next_byte(slice: RopeSlice, byte_idx: usize) -> usize { if byte_idx == 0 { byte_idx } else {