mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-11 04:40:05 -06:00
* Backspace after a floating shape's anchor joins paragraphs and keeps the shape (#4592) * wc-upkeep-6: protect Backspace joins and markers --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Brandon Thomas
parent
74b2cbb5c9
commit
2e790d549c
@@ -1,5 +1,5 @@
|
||||
use super::enter_tab::tab_indent_on;
|
||||
use super::{Block, CmdResult, NumRef, Pos, Selection, Session, StoryRef, Value, delete_selection, delete_selection_effect, sel_result};
|
||||
use super::{Block, CmdResult, NumRef, OBJ, Pos, Selection, Session, StoryRef, Value, delete_selection, delete_selection_effect, sel_result};
|
||||
|
||||
pub(super) fn backspace(s: &mut Session, v: &Value) -> CmdResult {
|
||||
let r = backspace_inner(s, v)?;
|
||||
@@ -43,8 +43,17 @@ fn backspace_inner(s: &mut Session, _: &Value) -> CmdResult {
|
||||
});
|
||||
return sel_result(s);
|
||||
}
|
||||
let f = s.sel.focus.clone();
|
||||
let mut f = s.sel.focus.clone();
|
||||
let Some(para) = s.doc.para_at(&f) else { return sel_result(s) };
|
||||
// Only floating objects before the caret: they are anchors, not characters, so this is the
|
||||
// paragraph start and the shape stays (Word).
|
||||
let before = para.text.get(..f.off).unwrap_or_default();
|
||||
if !before.is_empty()
|
||||
&& before.chars().all(|c| c == OBJ)
|
||||
&& para.objects_with_offsets().take_while(|(o, _)| *o < f.off).all(|(_, o)| o.is_marker() || o.is_floating())
|
||||
{
|
||||
f.off = 0;
|
||||
}
|
||||
if f.off > 0 {
|
||||
let a = Pos { off: para.backspace_from(f.off), ..f.clone() };
|
||||
if let Some(c) = s.autocap.as_mut() {
|
||||
@@ -98,6 +107,12 @@ fn backspace_inner(s: &mut Session, _: &Value) -> CmdResult {
|
||||
delete_selection(s)?;
|
||||
return sel_result(s);
|
||||
}
|
||||
// Joining moves a content control whose start is at the paragraph edge. Check its
|
||||
// contents explicitly: the paragraph-mark range itself ends before those contents.
|
||||
for control in s.doc.control_ranges(f.story).into_iter().filter(|r| r.start.path == f.path && r.start.off == f.off) {
|
||||
crate::cmd::controls::check_edit(s, &control.content_start(), &control.end)?;
|
||||
}
|
||||
crate::cmd::controls::check_delete(s, &a, &f)?;
|
||||
let at = s.doc.delete_range(&a, &f)?;
|
||||
s.sel = Selection::caret(at);
|
||||
}
|
||||
|
||||
@@ -3458,3 +3458,51 @@ fn double_space_adds_a_period_when_asked() {
|
||||
typ(&mut t, "Hello world");
|
||||
assert_eq!(text(&t), "Hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace_after_a_floating_shape_anchor_joins_paragraphs_and_keeps_the_shape() {
|
||||
let mut s = typed("Line one\nLine two");
|
||||
run(&mut s, "caret.set", json!({"pos": {"story": "body", "path": [1], "off": 0}}));
|
||||
run(&mut s, "insert.textBox", json!({"text": "Box"}));
|
||||
let (a, f) = (Pos::body(1, 0), Pos::body(1, 3));
|
||||
run(&mut s, "select.range", json!({"anchor": a, "focus": f}));
|
||||
run(&mut s, "arrange.wrap", json!({"wrap": "inFrontOfText"}));
|
||||
let objects = |s: &Session| s.doc.para_at(&Pos::body(1, 0)).map(|p| p.object_offsets().len());
|
||||
assert_eq!(objects(&s), Some(1));
|
||||
run(&mut s, "caret.set", json!({"pos": {"story": "body", "path": [1], "off": 3}}));
|
||||
run(&mut s, "text.backspace", json!({}));
|
||||
assert_eq!(s.doc.body.iter().filter(|b| b.as_para().is_some()).count(), 1);
|
||||
assert_eq!(objects(&s), None);
|
||||
assert_eq!(
|
||||
s.doc.para_at(&Pos::body(0, 0)).map(|p| (p.text.replace('\u{FFFC}', ""), p.object_offsets().len())),
|
||||
Some(("Line oneLine two".to_string(), 1))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace_after_marker_and_floating_anchors_still_joins_paragraphs() {
|
||||
let mut s = typed("Line one\nLine two");
|
||||
run(&mut s, "caret.set", json!({"pos": {"story": "body", "path": [1], "off": 0}}));
|
||||
run(&mut s, "insert.textBox", json!({"text": "Box"}));
|
||||
let (a, f) = (Pos::body(1, 0), Pos::body(1, 3));
|
||||
run(&mut s, "select.range", json!({"anchor": a, "focus": f}));
|
||||
run(&mut s, "arrange.wrap", json!({"wrap": "inFrontOfText"}));
|
||||
s.doc.insert_object(&Pos::body(1, 0), wordcraft_doc::InlineObject::BookmarkStart { name: "anchor".into() }, &Default::default()).unwrap();
|
||||
s.doc.insert_object(&Pos::body(1, 0), wordcraft_doc::InlineObject::CommentStart { id: 1 }, &Default::default()).unwrap();
|
||||
run(&mut s, "caret.set", json!({"pos": {"story": "body", "path": [1], "off": 9}}));
|
||||
run(&mut s, "text.backspace", json!({}));
|
||||
assert_eq!(s.doc.body.iter().filter(|b| b.as_para().is_some()).count(), 1);
|
||||
assert_eq!(s.doc.para_at(&Pos::body(0, 0)).map(|p| p.text.replace('\u{FFFC}', "")), Some("Line oneLine two".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace_cannot_join_into_locked_content_control() {
|
||||
let mut s = typed("Above\nInside");
|
||||
run(&mut s, "select.range", json!({"anchor": {"story": "body", "path": [1], "off": 0}, "focus": {"story": "body", "path": [1], "off": 6}}));
|
||||
run(&mut s, "developer.richText", json!({"tag": "locked"}));
|
||||
run(&mut s, "developer.properties", json!({"control": "locked", "lockEdit": true}));
|
||||
run(&mut s, "caret.set", json!({"pos": {"story": "body", "path": [1], "off": 0}}));
|
||||
let before = s.doc.clone();
|
||||
assert!(s.run("text.backspace", &json!({})).is_err());
|
||||
assert_eq!(s.doc, before);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ Bullets and Numbering decide whether to toggle off from the selected paragraphs'
|
||||
| type, select some of it, type over it | two undo steps: the first ⌘Z brings the selected text back (still selected), the second removes the typing. Uninterrupted typing stays one step | `typing_then_selecting_then_typing_are_two_undo_steps` |
|
||||
| ⌘B with nothing selected, ⌘Z, type | the text isn't bold (Undo takes the waiting caret formatting back too); ⌘Y brings it back | `undoing_bold_at_the_caret_turns_bold_typing_off_again` |
|
||||
| ⌫ at the start of the document, or typing nothing | nothing happens: no undo step is added, the document is not marked edited, and the redo stack survives ³ | `backspace_at_the_start_of_the_document_is_not_an_edit`, `a_command_that_changes_nothing_is_not_an_edit`, `a_no_op_command_keeps_the_redo_stack` |
|
||||
| ⌫ right after a floating shape's anchor at the paragraph start (In Front of Text, Square…) | joins the paragraph above and keeps the shape; the anchor is not a character to delete (checked in Word for Windows, #4592) | `backspace_after_a_floating_shape_anchor_joins_paragraphs_and_keeps_the_shape` |
|
||||
|
||||
³ Not re-observed side by side in Word for this change: Word's undo list does not grow when a
|
||||
key changes nothing, and the document is not marked edited. The gap was found by the read-only
|
||||
@@ -565,3 +566,5 @@ again.
|
||||
| 2026-10-10 | minor | Track Changes rows: tracked Enter, Backspace and Delete on paragraph marks (#244) |
|
||||
| 2026-10-10 | major | First version: lists, AutoCorrect and AutoFormat as you type, observed against Word (#204) |
|
||||
| 2026-10-15 | trivial | Text direction: typed and inserted text takes its direction from its own letters, not the caret's run; spaces typed into reopened Hebrew draw in the Latin font until reopened (#1543) |
|
||||
|
||||
| 2026-10-27 | trivial | Backspace after a floating shape's anchor at a paragraph start joins paragraphs and keeps the shape (#4592) |
|
||||
Reference in New Issue
Block a user