From 880b9341e7e2895eecc28808bc77b4a16c27d39a Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Sat, 23 Mar 2024 22:50:24 -0400 Subject: [PATCH 1/6] Set an index for the last saved change I added an index that represents the last saved change. Editors are considered to be unsaved or modified if the current change is different from the save index. In other words, if the last saved change is `5`, undoing or redoing past that change should indicate that the editor has been modified. This is needed to fix two bugs in COSMIC Edit: * https://github.com/pop-os/cosmic-edit/issues/116 * https://github.com/pop-os/cosmic-edit/issues/128 --- src/edit/vi.rs | 98 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 88 insertions(+), 10 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 4e50bfb691..ba0e61a81a 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -31,13 +31,14 @@ fn finish_change<'buffer, E: Edit<'buffer>>( editor: &mut E, commands: &mut cosmic_undo_2::Commands, changed: &mut bool, + pivot: Pivot, ) -> Option { //TODO: join changes together match editor.finish_change() { Some(change) => { if !change.items.is_empty() { commands.push(change.clone()); - *changed = true; + *changed = eval_changed(commands, pivot); } Some(change) } @@ -45,6 +46,19 @@ fn finish_change<'buffer, E: Edit<'buffer>>( } } +fn eval_changed(commands: &cosmic_undo_2::Commands, pivot: Pivot) -> bool { + // Editors are considered modified if the current change index is unequal to the last + // saved index + commands + .current_command_index() + .map(|current| match pivot { + Pivot::Unsaved => true, + Pivot::Exact(i) => current == i, + Pivot::Saved => false, + }) + .unwrap_or(true) +} + fn search<'buffer, E: Edit<'buffer>>(editor: &mut E, value: &str, forwards: bool) -> bool { let mut cursor = editor.cursor(); let start_line = cursor.line; @@ -167,6 +181,7 @@ pub struct ViEditor<'syntax_system, 'buffer> { search_opt: Option<(String, bool)>, commands: cosmic_undo_2::Commands, changed: bool, + save_pivot: Pivot, } impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { @@ -179,6 +194,7 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { search_opt: None, commands: cosmic_undo_2::Commands::new(), changed: false, + save_pivot: Pivot::Unsaved, } } @@ -233,6 +249,38 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { self.changed = changed; } + /// Get current save pivot point + /// + /// See [`ViEditor::set_save_pivot`] for more details. + pub fn save_pivot(&self) -> Pivot { + match self.save_pivot { + Pivot::Exact(i) => { + if Some(i) == self.commands.current_command_index() { + Pivot::Saved + } else { + self.save_pivot + } + } + _ => self.save_pivot, + } + } + + /// Set last saved pivot point + /// + /// A pivot point is the last saved index. Anything before or after the pivot indicates that + /// the editor has been changed or is unsaved. + /// + /// The pivot point for an unsaved editor is `0`. In other words, all changes are unsaved. + /// The pivot point for a saved editor is the index of the last change - 1. + /// + /// Undoing changes down to the pivot point means that the editor is unchanged. + pub fn set_save_pivot(&mut self, pivot: Pivot) { + self.save_pivot = match pivot { + Pivot::Saved => Pivot::Exact(self.commands.current_command_index().unwrap_or_default()), + _ => pivot, + }; + } + /// Set passthrough mode (true will turn off vi features) pub fn set_passthrough(&mut self, passthrough: bool) { if passthrough != self.passthrough { @@ -251,9 +299,8 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { log::debug!("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; } + self.changed = eval_changed(&self.commands, self.save_pivot); } /// Undo a change @@ -261,9 +308,8 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { log::debug!("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; } + self.changed = eval_changed(&self.commands, self.save_pivot); } #[cfg(feature = "swash")] @@ -550,7 +596,12 @@ impl<'syntax_system, 'buffer> Edit<'buffer> for ViEditor<'syntax_system, 'buffer } fn finish_change(&mut self) -> Option { - finish_change(&mut self.editor, &mut self.commands, &mut self.changed) + finish_change( + &mut self.editor, + &mut self.commands, + &mut self.changed, + self.save_pivot, + ) } fn action(&mut self, font_system: &mut FontSystem, action: Action) { @@ -564,7 +615,12 @@ impl<'syntax_system, 'buffer> Edit<'buffer> for ViEditor<'syntax_system, 'buffer if self.passthrough { editor.action(font_system, action); // Always finish change when passing through (TODO: group changes) - finish_change(editor, &mut self.commands, &mut self.changed); + finish_change( + editor, + &mut self.commands, + &mut self.changed, + self.save_pivot, + ); return; } @@ -589,7 +645,12 @@ impl<'syntax_system, 'buffer> Edit<'buffer> for ViEditor<'syntax_system, 'buffer log::debug!("Pass through action {:?}", action); editor.action(font_system, action); // Always finish change when passing through (TODO: group changes) - finish_change(editor, &mut self.commands, &mut self.changed); + finish_change( + editor, + &mut self.commands, + &mut self.changed, + self.save_pivot, + ); return; } }; @@ -620,7 +681,12 @@ impl<'syntax_system, 'buffer> Edit<'buffer> for ViEditor<'syntax_system, 'buffer return; } Event::ChangeFinish => { - finish_change(editor, &mut self.commands, &mut self.changed); + finish_change( + editor, + &mut self.commands, + &mut self.changed, + self.save_pivot, + ); return; } Event::Delete => Action::Delete, @@ -687,7 +753,12 @@ impl<'syntax_system, 'buffer> Edit<'buffer> for ViEditor<'syntax_system, 'buffer } } } - finish_change(editor, &mut self.commands, &mut self.changed); + finish_change( + editor, + &mut self.commands, + &mut self.changed, + self.save_pivot, + ); } return; } @@ -1122,3 +1193,10 @@ impl<'font_system, 'syntax_system, 'buffer> self.inner.draw(self.font_system, cache, f); } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Pivot { + Unsaved, + Exact(usize), + Saved, +} From d823a6755653a68caba0d365b73886fbec6febbf Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Sun, 24 Mar 2024 02:07:58 -0400 Subject: [PATCH 2/6] Unit test that confirms pivot logic works I'll most likely simplify the API as end users don't have a way to cleanly use `Pivot::Exact` without access to the internal command buffer. --- src/edit/vi.rs | 3 +- tests/editor_modified_state.rs | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 tests/editor_modified_state.rs diff --git a/src/edit/vi.rs b/src/edit/vi.rs index ba0e61a81a..4bc42a560c 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -53,7 +53,7 @@ fn eval_changed(commands: &cosmic_undo_2::Commands, pivot: Pivot) -> boo .current_command_index() .map(|current| match pivot { Pivot::Unsaved => true, - Pivot::Exact(i) => current == i, + Pivot::Exact(i) => current != i, Pivot::Saved => false, }) .unwrap_or(true) @@ -279,6 +279,7 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { Pivot::Saved => Pivot::Exact(self.commands.current_command_index().unwrap_or_default()), _ => pivot, }; + self.changed = eval_changed(&self.commands, pivot); } /// Set passthrough mode (true will turn off vi features) diff --git a/tests/editor_modified_state.rs b/tests/editor_modified_state.rs new file mode 100644 index 0000000000..ddafe90606 --- /dev/null +++ b/tests/editor_modified_state.rs @@ -0,0 +1,53 @@ +use std::sync::OnceLock; + +use cosmic_text::{ + Buffer, Change, Cursor, Edit, Metrics, Pivot, SyntaxEditor, SyntaxSystem, ViEditor, +}; + +static SYNTAX_SYSTEM: OnceLock = OnceLock::new(); + +fn editor() -> ViEditor<'static, 'static> { + // More or less copied from cosmic-edit + let font_size: f32 = 14.0; + let line_height = (font_size * 1.4).ceil(); + + let metrics = Metrics::new(font_size, line_height); + let buffer = Buffer::new_empty(metrics); + let editor = SyntaxEditor::new( + buffer, + SYNTAX_SYSTEM.get_or_init(SyntaxSystem::new), + "base16-eighties.dark", + ) + .expect("Default theme `base16-eighties.dark` should be found"); + + ViEditor::new(editor) +} + +#[test] +fn undo_to_last_save() { + let mut editor = editor(); + + // Latest saved change is the first change + editor.start_change(); + let cursor = editor.insert_at(Cursor::new(0, 0), "Ferris is Rust's ", None); + editor.finish_change(); + assert!(editor.changed()); + editor.set_save_pivot(Pivot::Saved); + assert_eq!(Pivot::Saved, editor.save_pivot()); + assert!(!editor.changed()); + + // A new insert should set the editor as modified and the pivot should still be on the first + // change from earlier + editor.start_change(); + editor.insert_at(cursor, "mascot", None); + editor.finish_change(); + assert_eq!(Pivot::Exact(0), editor.save_pivot()); + assert!(editor.changed()); + + // Undoing the latest change should set the editor to unmodified again + editor.start_change(); + editor.undo(); + editor.finish_change(); + assert_eq!(Pivot::Saved, editor.save_pivot()); + assert!(!editor.changed()); +} From 15511bfe4779962210a0ad9050f594461c3ae5bd Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Sun, 24 Mar 2024 08:22:25 -0400 Subject: [PATCH 3/6] Simplify save point API --- src/edit/vi.rs | 53 +++++++++------------------------- tests/editor_modified_state.rs | 24 ++++++++++----- 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index 4bc42a560c..c60602443d 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -31,7 +31,7 @@ fn finish_change<'buffer, E: Edit<'buffer>>( editor: &mut E, commands: &mut cosmic_undo_2::Commands, changed: &mut bool, - pivot: Pivot, + pivot: Option, ) -> Option { //TODO: join changes together match editor.finish_change() { @@ -46,16 +46,17 @@ fn finish_change<'buffer, E: Edit<'buffer>>( } } -fn eval_changed(commands: &cosmic_undo_2::Commands, pivot: Pivot) -> bool { +fn eval_changed(commands: &cosmic_undo_2::Commands, pivot: Option) -> bool { // Editors are considered modified if the current change index is unequal to the last - // saved index + // saved index or if `pivot` is None. + // The latter case handles a never saved editor with a current command index of 0. + // Check the unit tests for an example. commands .current_command_index() - .map(|current| match pivot { - Pivot::Unsaved => true, - Pivot::Exact(i) => current != i, - Pivot::Saved => false, - }) + .zip(pivot) + .map(|(current, pivot)| current != pivot) + // Default to true because it's safer to assume a buffer has been modified so as to not + // lose changes .unwrap_or(true) } @@ -181,7 +182,7 @@ pub struct ViEditor<'syntax_system, 'buffer> { search_opt: Option<(String, bool)>, commands: cosmic_undo_2::Commands, changed: bool, - save_pivot: Pivot, + save_pivot: Option, } impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { @@ -194,7 +195,7 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { search_opt: None, commands: cosmic_undo_2::Commands::new(), changed: false, - save_pivot: Pivot::Unsaved, + save_pivot: None, } } @@ -249,22 +250,6 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { self.changed = changed; } - /// Get current save pivot point - /// - /// See [`ViEditor::set_save_pivot`] for more details. - pub fn save_pivot(&self) -> Pivot { - match self.save_pivot { - Pivot::Exact(i) => { - if Some(i) == self.commands.current_command_index() { - Pivot::Saved - } else { - self.save_pivot - } - } - _ => self.save_pivot, - } - } - /// Set last saved pivot point /// /// A pivot point is the last saved index. Anything before or after the pivot indicates that @@ -274,12 +259,9 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { /// The pivot point for a saved editor is the index of the last change - 1. /// /// Undoing changes down to the pivot point means that the editor is unchanged. - pub fn set_save_pivot(&mut self, pivot: Pivot) { - self.save_pivot = match pivot { - Pivot::Saved => Pivot::Exact(self.commands.current_command_index().unwrap_or_default()), - _ => pivot, - }; - self.changed = eval_changed(&self.commands, pivot); + pub fn save_point(&mut self) { + self.save_pivot = Some(self.commands.current_command_index().unwrap_or_default()); + self.changed = false; } /// Set passthrough mode (true will turn off vi features) @@ -1194,10 +1176,3 @@ impl<'font_system, 'syntax_system, 'buffer> self.inner.draw(self.font_system, cache, f); } } - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Pivot { - Unsaved, - Exact(usize), - Saved, -} diff --git a/tests/editor_modified_state.rs b/tests/editor_modified_state.rs index ddafe90606..3b33e5f931 100644 --- a/tests/editor_modified_state.rs +++ b/tests/editor_modified_state.rs @@ -1,8 +1,6 @@ use std::sync::OnceLock; -use cosmic_text::{ - Buffer, Change, Cursor, Edit, Metrics, Pivot, SyntaxEditor, SyntaxSystem, ViEditor, -}; +use cosmic_text::{Buffer, Change, Cursor, Edit, Metrics, SyntaxEditor, SyntaxSystem, ViEditor}; static SYNTAX_SYSTEM: OnceLock = OnceLock::new(); @@ -23,6 +21,18 @@ fn editor() -> ViEditor<'static, 'static> { ViEditor::new(editor) } +// Tests that inserting into an empty editor correctly sets the editor as modified. +#[test] +fn insert_in_empty_editor_sets_changed() { + let mut editor = editor(); + + assert!(!editor.changed()); + editor.start_change(); + editor.insert_at(Cursor::new(0, 0), "Robert'); DROP TABLE Students;--", None); + editor.finish_change(); + assert!(editor.changed()); +} + #[test] fn undo_to_last_save() { let mut editor = editor(); @@ -32,8 +42,8 @@ fn undo_to_last_save() { let cursor = editor.insert_at(Cursor::new(0, 0), "Ferris is Rust's ", None); editor.finish_change(); assert!(editor.changed()); - editor.set_save_pivot(Pivot::Saved); - assert_eq!(Pivot::Saved, editor.save_pivot()); + editor.save_point(); + // assert_eq!(Pivot::Saved, editor.save_pivot()); assert!(!editor.changed()); // A new insert should set the editor as modified and the pivot should still be on the first @@ -41,13 +51,13 @@ fn undo_to_last_save() { editor.start_change(); editor.insert_at(cursor, "mascot", None); editor.finish_change(); - assert_eq!(Pivot::Exact(0), editor.save_pivot()); + // assert_eq!(Pivot::Exact(0), editor.save_pivot()); assert!(editor.changed()); // Undoing the latest change should set the editor to unmodified again editor.start_change(); editor.undo(); editor.finish_change(); - assert_eq!(Pivot::Saved, editor.save_pivot()); + // assert_eq!(Pivot::Saved, editor.save_pivot()); assert!(!editor.changed()); } From dc22c0508b0663519b9d46362065d3d9a9f9fceb Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Sun, 24 Mar 2024 21:40:19 -0400 Subject: [PATCH 4/6] Implement more save point unit tests A unit test for an edge case currently fails but normal usage works. --- src/edit/vi.rs | 9 ++-- tests/editor_modified_state.rs | 75 ++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index c60602443d..dbb9920c06 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -46,6 +46,7 @@ fn finish_change<'buffer, E: Edit<'buffer>>( } } +/// Evaluate if an [`ViEditor`] changed based on its last saved state. fn eval_changed(commands: &cosmic_undo_2::Commands, pivot: Option) -> bool { // Editors are considered modified if the current change index is unequal to the last // saved index or if `pivot` is None. @@ -250,15 +251,15 @@ impl<'syntax_system, 'buffer> ViEditor<'syntax_system, 'buffer> { self.changed = changed; } - /// Set last saved pivot point + /// Set current change as the save (or pivot) point. /// /// A pivot point is the last saved index. Anything before or after the pivot indicates that /// the editor has been changed or is unsaved. /// - /// The pivot point for an unsaved editor is `0`. In other words, all changes are unsaved. - /// The pivot point for a saved editor is the index of the last change - 1. + /// Undoing changes down to the pivot point sets the editor as unchanged. + /// Redoing changes up to the pivot point sets the editor as unchanged. /// - /// Undoing changes down to the pivot point means that the editor is unchanged. + /// Undoing or redoing changes beyond the pivot point sets the editor to changed. pub fn save_point(&mut self) { self.save_pivot = Some(self.commands.current_command_index().unwrap_or_default()); self.changed = false; diff --git a/tests/editor_modified_state.rs b/tests/editor_modified_state.rs index 3b33e5f931..8e1d1b6103 100644 --- a/tests/editor_modified_state.rs +++ b/tests/editor_modified_state.rs @@ -33,6 +33,23 @@ fn insert_in_empty_editor_sets_changed() { assert!(editor.changed()); } +#[test] +fn insert_and_undo_in_unsaved_editor_is_unchanged() { + let mut editor = editor(); + + assert!(!editor.changed()); + editor.start_change(); + editor.insert_at(Cursor::new(0, 0), "loop {}", None); + editor.finish_change(); + assert!(editor.changed()); + + // Undoing the above change should set the editor as unchanged even if the save state is unset + editor.start_change(); + editor.undo(); + editor.finish_change(); + assert!(!editor.changed()); +} + #[test] fn undo_to_last_save() { let mut editor = editor(); @@ -61,3 +78,61 @@ fn undo_to_last_save() { // assert_eq!(Pivot::Saved, editor.save_pivot()); assert!(!editor.changed()); } + +#[test] +fn redoing_to_save_point_sets_editor_as_unchanged() { + let mut editor = editor(); + + // Initial change + assert!( + !editor.changed(), + "Editor should start in an unchanged state" + ); + editor.start_change(); + editor.insert_at(Cursor::new(0, 0), "editor.start_change();", None); + editor.finish_change(); + assert!( + editor.changed(), + "Editor should be set as modified after insert() and finish_change()" + ); + editor.save_point(); + assert!( + !editor.changed(), + "Editor should be unchanged after setting a save point" + ); + + // Change to undo then redo + editor.start_change(); + editor.insert_at(Cursor::new(1, 0), "editor.finish_change()", None); + editor.finish_change(); + assert!( + editor.changed(), + "Editor should be set as modified after insert() and finish_change()" + ); + editor.save_point(); + assert!( + !editor.changed(), + "Editor should be unchanged after setting a save point" + ); + + editor.undo(); + assert!( + editor.changed(), + "Undoing past save point should set editor as changed" + ); + editor.redo(); + assert!( + !editor.changed(), + "Redoing to save point should set editor as unchanged" + ); +} + +#[test] +fn redoing_past_save_point_sets_editor_as_changed() { + unimplemented!() +} + +#[test] +fn undo_all_changes() { + unimplemented!() +} From 2a07f9143fcbcbb794c9cf5538717c35d93eba98 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Mon, 25 Mar 2024 01:18:03 -0400 Subject: [PATCH 5/6] Fix edge case for empty command index and pivot --- src/edit/vi.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/edit/vi.rs b/src/edit/vi.rs index dbb9920c06..e1913ccd2f 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -50,15 +50,18 @@ fn finish_change<'buffer, E: Edit<'buffer>>( fn eval_changed(commands: &cosmic_undo_2::Commands, pivot: Option) -> bool { // Editors are considered modified if the current change index is unequal to the last // saved index or if `pivot` is None. - // The latter case handles a never saved editor with a current command index of 0. + // The latter case handles a never saved editor with a current command index of None. // Check the unit tests for an example. - commands - .current_command_index() - .zip(pivot) - .map(|(current, pivot)| current != pivot) + match (commands.current_command_index(), pivot) { + (Some(current), Some(pivot)) => current != pivot, + // Edge case for an editor with neither a save point nor any changes. + // This could be a new editor or an editor without a save point where undo() is called + // until the editor is fresh. + (None, None) => false, // Default to true because it's safer to assume a buffer has been modified so as to not // lose changes - .unwrap_or(true) + _ => true, + } } fn search<'buffer, E: Edit<'buffer>>(editor: &mut E, value: &str, forwards: bool) -> bool { From 3df45af6940447cd0e5ba68fccaeb9eef435746e Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Mon, 25 Mar 2024 01:36:24 -0400 Subject: [PATCH 6/6] More save point unit tests for common use cases --- tests/editor_modified_state.rs | 115 +++++++++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 13 deletions(-) diff --git a/tests/editor_modified_state.rs b/tests/editor_modified_state.rs index 8e1d1b6103..01ac00888c 100644 --- a/tests/editor_modified_state.rs +++ b/tests/editor_modified_state.rs @@ -1,9 +1,12 @@ +#![cfg(feature = "vi")] + use std::sync::OnceLock; -use cosmic_text::{Buffer, Change, Cursor, Edit, Metrics, SyntaxEditor, SyntaxSystem, ViEditor}; +use cosmic_text::{Buffer, Cursor, Edit, Metrics, SyntaxEditor, SyntaxSystem, ViEditor}; static SYNTAX_SYSTEM: OnceLock = OnceLock::new(); +// New editor for tests fn editor() -> ViEditor<'static, 'static> { // More or less copied from cosmic-edit let font_size: f32 = 14.0; @@ -33,6 +36,8 @@ fn insert_in_empty_editor_sets_changed() { assert!(editor.changed()); } +// Tests an edge case where a save point is never set. +// Undoing changes should set the editor back to unmodified. #[test] fn insert_and_undo_in_unsaved_editor_is_unchanged() { let mut editor = editor(); @@ -51,32 +56,41 @@ fn insert_and_undo_in_unsaved_editor_is_unchanged() { } #[test] -fn undo_to_last_save() { +fn undo_to_save_point_sets_editor_to_unchanged() { let mut editor = editor(); // Latest saved change is the first change editor.start_change(); let cursor = editor.insert_at(Cursor::new(0, 0), "Ferris is Rust's ", None); editor.finish_change(); - assert!(editor.changed()); + assert!( + editor.changed(), + "Editor should be set to changed after insertion" + ); editor.save_point(); - // assert_eq!(Pivot::Saved, editor.save_pivot()); - assert!(!editor.changed()); + assert!( + !editor.changed(), + "Editor should be set to unchanged after setting a save point" + ); // A new insert should set the editor as modified and the pivot should still be on the first // change from earlier editor.start_change(); editor.insert_at(cursor, "mascot", None); editor.finish_change(); - // assert_eq!(Pivot::Exact(0), editor.save_pivot()); - assert!(editor.changed()); + assert!( + editor.changed(), + "Editor should be set to changed after inserting text after a save point" + ); // Undoing the latest change should set the editor to unmodified again editor.start_change(); editor.undo(); editor.finish_change(); - // assert_eq!(Pivot::Saved, editor.save_pivot()); - assert!(!editor.changed()); + assert!( + !editor.changed(), + "Editor should be set to unchanged after undoing to save point" + ); } #[test] @@ -128,11 +142,86 @@ fn redoing_to_save_point_sets_editor_as_unchanged() { } #[test] -fn redoing_past_save_point_sets_editor_as_changed() { - unimplemented!() +fn redoing_past_save_point_sets_editor_to_changed() { + let mut editor = editor(); + + // Save point change to undo to and then redo past. + editor.start_change(); + editor.insert_string("Walt Whitman ", None); + editor.finish_change(); + + // Set save point to the change above. + assert!( + editor.changed(), + "Editor should be set as modified after insert() and finish_change()" + ); + editor.save_point(); + assert!( + !editor.changed(), + "Editor should be unchanged after setting a save point" + ); + + editor.start_change(); + editor.insert_string("Allen Ginsberg ", None); + editor.finish_change(); + + editor.start_change(); + editor.insert_string("Jack Kerouac ", None); + editor.finish_change(); + + assert!(editor.changed(), "Editor should be modified insertion"); + + // Undo to Whitman + editor.undo(); + editor.undo(); + assert!( + !editor.changed(), + "Editor should be unmodified after undoing to the save point" + ); + + // Redo to Kerouac + editor.redo(); + editor.redo(); + assert!( + editor.changed(), + "Editor should be modified after redoing past the save point" + ); } #[test] -fn undo_all_changes() { - unimplemented!() +fn undoing_past_save_point_sets_editor_to_changed() { + let mut editor = editor(); + + editor.start_change(); + editor.insert_string("Robert Fripp ", None); + editor.finish_change(); + + // Save point change to undo past. + editor.start_change(); + editor.insert_string("Thurston Moore ", None); + editor.finish_change(); + + assert!(editor.changed(), "Editor should be changed after insertion"); + editor.save_point(); + assert!( + !editor.changed(), + "Editor should be unchanged after setting a save point" + ); + + editor.start_change(); + editor.insert_string("Kim Deal ", None); + editor.finish_change(); + + // Undo to the first change + editor.undo(); + editor.undo(); + assert!( + editor.changed(), + "Editor should be changed after undoing past save point" + ); } + +// #[test] +// fn undo_all_changes() { +// unimplemented!() +// }