Type: Change Case, Smart Punctuation and other commands that change nothing record no undo step

These commands edited the document even when nothing changed, so the
call recorded an undo step, emptied Redo and marked the document
modified:

- Change Case to the case the text has, Smart Punctuation on type with
  nothing left to change or on a selection with no type in it, and
  Find and Replace when every match is replaced with the text it is.
  All three rewrite the text runs through edit_runs, which now edits
  only when a run changes.
- Convert To Area Type with no point type among the objects, and
  Convert To Point Type with no area type among them.
- Type on a Path Options given the values the type has, without Flip:
  OK in the dialog with nothing changed, or a Type on a Path effect
  the type has.
- Fill with Placeholder Text on type that holds its placeholder text
  already.
- type.insert with an empty text on type that ends with a run of
  text.

These commands now return without editing the document. The replies
have the same form as before. A call that changes something still
records one undo step.

One line in the evidence list of docs/target-app-parity.md names every
command above except type.insert.
This commit is contained in:
dotCooCoo
2026-10-12 08:45:22 +07:00
committed by bflatastic
parent 256cd1f85a
commit a24269eb15
4 changed files with 178 additions and 4 deletions
+46 -4
View File
@@ -132,9 +132,10 @@ fn ids_json(ids: &[NodeId]) -> Value {
json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() })
}
/// Rewrite every run of the given text objects with `f(run text, state)`.
/// Rewrite every run of the given text objects with `f(run text, state)`. When no run changes, the
/// document and its undo history stay as they are.
fn edit_runs<S: Default>(s: &mut Session, label: &str, ids: &[NodeId], f: impl Fn(&str, &mut S) -> String) -> Result<usize> {
s.edit(label, |d, _| {
super::overprint::edit_counted(s, label, |d| {
let mut changed = 0;
for id in ids {
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue };
@@ -260,6 +261,11 @@ fn smart_punctuation(s: &mut Session, p: &Value) -> Result<Value> {
fn to_area(s: &mut Session, p: &Value) -> Result<Value> {
let ids = texts(s, p, "type.convertToAreaType")?;
// With no point type among the objects, the document and its undo history stay as they are.
let d = &s.doc()?.doc;
if !ids.iter().any(|id| matches!(d.node(*id).map(|n| &n.kind), Some(NodeKind::Text(t)) if matches!(t.kind, TextKind::Point))) {
return Ok(ids_json(&ids));
}
s.edit("Convert To Area Type", |d, _| {
for id in &ids {
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue };
@@ -302,6 +308,11 @@ fn locate(runs: &[TextRun], byte: usize) -> Option<(usize, usize)> {
fn to_point(s: &mut Session, p: &Value) -> Result<Value> {
let ids = texts(s, p, "type.convertToPointType")?;
// With no area type among the objects, the document and its undo history stay as they are.
let d = &s.doc()?.doc;
if !ids.iter().any(|id| matches!(d.node(*id).map(|n| &n.kind), Some(NodeKind::Text(t)) if matches!(t.kind, TextKind::Area { .. }))) {
return Ok(ids_json(&ids));
}
s.edit("Convert To Point Type", |d, _| {
for id in &ids {
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue };
@@ -418,6 +429,22 @@ fn path_options(s: &mut Session, p: &Value) -> Result<Value> {
None => None,
};
let spacing = p.get("spacing").and_then(Value::as_f64).filter(|x| x.is_finite()).map(|x| x.clamp(-1000.0, 1000.0));
// Without a flip, when every object already has every value given, the document and its undo
// history stay as they are.
let has_values = |t: &TextObject| match &t.kind {
TextKind::OnPath { start: st, end: en, .. } => {
start.is_none_or(|v| v == *st)
&& end.is_none_or(|v| v == *en)
&& effect.is_none_or(|e| t.path_effect == e)
&& align.is_none_or(|a| t.path_align == a)
&& spacing.is_none_or(|v| t.path_spacing == v)
}
_ => true,
};
let d = &s.doc()?.doc;
if !flip && ids.iter().all(|id| path_text(d.node(*id)).is_none_or(has_values)) {
return options(s);
}
s.edit("Type on a Path Options", |d, _| {
for id in &ids {
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue };
@@ -457,13 +484,18 @@ start as rough drafts like this one. Keep going until the frame is full and the
fn fill_placeholder(s: &mut Session, p: &Value) -> Result<Value> {
let ids = texts(s, p, "type.fillPlaceholder")?;
s.edit("Fill with Placeholder Text", |d, _| {
// When filling leaves every object as it was, the document and its undo history stay as they
// are.
super::overprint::edit_counted(s, "Fill with Placeholder Text", |d| {
let mut changed = 0;
for id in &ids {
if let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) {
let before = t.clone();
fill_with_placeholder(t);
changed += usize::from(*t != before);
}
}
Ok(())
Ok(changed)
})?;
Ok(ids_json(&ids))
}
@@ -539,6 +571,16 @@ fn insert_char(s: &mut Session, p: &Value) -> Result<Value> {
_ => return Err(bad(C, "give `char` or `text`")),
};
let ids = texts(s, p, C)?;
// When `text` is empty and every object ends with a run of text, the document and its undo
// history stay as they are.
let d = &s.doc()?.doc;
let ends_with_text = |id: &NodeId| match d.node(*id).map(|n| &n.kind) {
Some(NodeKind::Text(t)) => t.runs.last().is_some_and(|r| r.inline.is_none()),
_ => false,
};
if text.is_empty() && ids.iter().all(ends_with_text) {
return Ok(ids_json(&ids));
}
s.edit("Typing", |d, _| {
for id in &ids {
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue };