mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 16:33:22 -06:00
Type: Undo and Redo put the caret where the undone or redone edit was
While the Type tool edits type, Edit > Undo and Redo (the menu, the keyboard, the History panel and MCP all run edit.undo and edit.redo) left the caret at its byte offset, clamped to the text. Typing "abc" before "XYZ" and pressing Undo left the caret after "XYZ". The engine now tells the active tool about edit.undo and edit.redo before it finishes the command. The Type tool keeps the edited text and a list of the steps that changed it, and puts the caret, with nothing selected, at the end of the span the step changed: where typed text began after its Undo, after restored text after the Undo of a deletion, and after retyped text after Redo. For the steps the tool made, it keeps where the span ended, so text that repeats the characters next to it puts the caret at the right end. A step that changes no characters, such as a formatting change, leaves the caret and the selection as they are. Typed lists keep their caret positions.
This commit is contained in:
@@ -121,7 +121,7 @@ impl Session {
|
||||
}
|
||||
if self.depth == 0 {
|
||||
self.journal_in_undo_groups(doc, &grouped, journal);
|
||||
self.after_command();
|
||||
self.after_command(Some(id));
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ mod transparencygrid;
|
||||
mod trap;
|
||||
mod typearea;
|
||||
mod typescale;
|
||||
mod typeundo;
|
||||
mod units;
|
||||
mod variables;
|
||||
mod webexport;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//! Edit › Undo and Redo while the Type tool edits: the caret goes to where the step changed the text.
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::{NodeId, NodeKind};
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
||||
|
||||
use crate::*;
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.prefs.placeholder_text = false;
|
||||
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn plain(s: &Session, id: NodeId) -> String {
|
||||
match &s.doc().unwrap().doc.node(id).unwrap().kind {
|
||||
NodeKind::Text(t) => t.plain_text(),
|
||||
_ => panic!("not text"),
|
||||
}
|
||||
}
|
||||
|
||||
fn caret(s: &Session) -> (u64, u64) {
|
||||
let o = s.tool_options();
|
||||
(o["caret"].as_u64().unwrap(), o["anchor"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
/// "abc" typed before "XYZ": Undo puts the caret before "XYZ" and Redo after "abc", from the
|
||||
/// menu command and from the History panel's steps alike (both run `edit.undo` and `edit.redo`).
|
||||
#[test]
|
||||
fn undo_and_redo_put_the_caret_where_the_step_changed_the_text() {
|
||||
let v = ViewInfo::default();
|
||||
let mut s = session();
|
||||
let r = s.execute("text.create", &json!({"x": 100, "y": 100, "text": "XYZ", "size": 20})).unwrap();
|
||||
let id = NodeId(r["id"].as_u64().unwrap());
|
||||
s.select_tool("type", v).unwrap();
|
||||
for k in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(k, 101.0, 95.0), v).unwrap();
|
||||
}
|
||||
s.set_tool_option("select", &json!({"start": 0, "end": 0}));
|
||||
s.tool_text("abc", v).unwrap();
|
||||
assert_eq!((plain(&s, id).as_str(), caret(&s)), ("abcXYZ", (3, 3)));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!((plain(&s, id).as_str(), caret(&s)), ("XYZ", (0, 0)));
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!((plain(&s, id).as_str(), caret(&s)), ("abcXYZ", (3, 3)));
|
||||
// A deleted character comes back with the caret after it, wherever the caret went since.
|
||||
s.tool_key(ToolKey::End, Mods::default(), v).unwrap();
|
||||
s.tool_key(ToolKey::Backspace, Mods::default(), v).unwrap();
|
||||
assert_eq!(plain(&s, id), "abcXY");
|
||||
s.tool_key(ToolKey::Home, Mods::default(), v).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!((plain(&s, id).as_str(), caret(&s)), ("abcXYZ", (6, 6)));
|
||||
}
|
||||
@@ -181,7 +181,7 @@ impl Session {
|
||||
let r = self.apply_actions(acts);
|
||||
self.keep_tool_settings();
|
||||
self.tool = lender;
|
||||
self.after_command();
|
||||
self.after_command(None);
|
||||
r.map(drop)
|
||||
}
|
||||
|
||||
@@ -678,10 +678,15 @@ impl Session {
|
||||
|
||||
/// Let the active tool finish work a command from outside it ended ([`vectorcraft_tools::Tool::after_command`]):
|
||||
/// the Type tool and the Eyedropper clear a character range whose text is no longer selected.
|
||||
pub(crate) fn after_command(&mut self) {
|
||||
/// After `edit.undo` or `edit.redo` (`cmd`), the tool is told first, so the Type tool puts the
|
||||
/// caret where the step changed the text.
|
||||
pub(crate) fn after_command(&mut self, cmd: Option<&str>) {
|
||||
if self.in_tool_actions || (!self.tool.wants_text() && self.tool.id() != "eyedropper") {
|
||||
return;
|
||||
}
|
||||
if let Some(cmd @ ("edit.undo" | "edit.redo")) = cmd {
|
||||
self.with_tool_cx(self.last_view, |t, cx| t.notify(cx, cmd));
|
||||
}
|
||||
let acts = self.with_tool_cx(self.last_view, |t, cx| t.after_command(cx));
|
||||
// The command itself succeeded: a failure here only leaves the edit as it was.
|
||||
if let Err(e) = self.apply_actions(acts) {
|
||||
|
||||
Reference in New Issue
Block a user