-
Notifications
You must be signed in to change notification settings - Fork 112
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix: Undo and redo correctly updates editor modified status (#244)
* 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: * pop-os/cosmic-edit#116 * pop-os/cosmic-edit#128 * 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. * Simplify save point API * Implement more save point unit tests A unit test for an edge case currently fails but normal usage works. * Fix edge case for empty command index and pivot * More save point unit tests for common use cases
- Loading branch information
1 parent
b086769
commit ff5501d
Showing
2 changed files
with
295 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
#![cfg(feature = "vi")] | ||
|
||
use std::sync::OnceLock; | ||
|
||
use cosmic_text::{Buffer, Cursor, Edit, Metrics, SyntaxEditor, SyntaxSystem, ViEditor}; | ||
|
||
static SYNTAX_SYSTEM: OnceLock<SyntaxSystem> = OnceLock::new(); | ||
|
||
// New editor for tests | ||
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) | ||
} | ||
|
||
// 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()); | ||
} | ||
|
||
// 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(); | ||
|
||
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_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(), | ||
"Editor should be set to changed after insertion" | ||
); | ||
editor.save_point(); | ||
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!( | ||
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!( | ||
!editor.changed(), | ||
"Editor should be set to unchanged after undoing to save point" | ||
); | ||
} | ||
|
||
#[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_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 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!() | ||
// } |