Skip to content

fix(editor): edits that lost text or acted on the wrong place - #29

Merged
baseballyama merged 1 commit into
mainfrom
fix/adversarial-editor
Oct 5, 2026
Merged

baseballyama merged 1 commit into
mainfrom
fix/adversarial-editor

Conversation

@baseballyama

Copy link
Copy Markdown
Member

Summary

This fixes editor bugs found by adversarially testing the /editor canvas. Several of them lost text, or made the screen differ from the saved document. Others acted on the wrong place: the previous caret, the paragraph above a table, or the whole run instead of a word.

Motivation

The maintainer reported that editing "still behaves strangely" and asked for thorough adversarial testing. Every finding below was reproduced in Chromium with a harness. After each action, the harness checks three things:

  • each rendered paragraph and table cell against the model;
  • that the serialized package is valid;
  • the DOM caret against the model caret.

There is no linked issue.

Changes

Lost or diverging text (canvas)

  • A key pressed right after a click acted on the previous caret, because selectionchange had not fired yet. For example, Enter + typing landed at the top of the document. The selection is now synced on every keydown and beforeinput.

  • Typing into a paragraph with no runs was rendered but never reached the model. This covers Word's empty <w:p/> and the cells of a row added with Tab. Typing and IME there now go through insertTextCommand.

  • These inputs used to let the browser restructure the DOM, and the model silently diverged from the screen:

    • ⌥⌫ / ⌘⌫ (word or line delete across runs);
    • Enter while an IME is composing;
    • a delete that empties a run;
    • insertParagraph from virtual keyboards;
    • spelling suggestions;
    • text drops;
    • Undo from the browser menu.

    beforeinput now leaves only plain typing, composition and one-character deletes inside a run to the browser. Everything else maps to the matching command, using getTargetRanges().

  • ⌘A selected the page box, so typing over it wrecked the pages. ⌘A now selects the story: the body, or the open header or footer.

  • ⌘↑ / ⌘↓ / ⌘Home / ⌘End left the caret between paragraphs. They now jump paragraph by paragraph and to the document start and end, as in Word.

  • A click in the header or footer margin left the caret where it was. It now moves to the nearest body line.

Model commands (@office-kit/docx-editor)

  • deleteSelectionCommand across table cells threw "not supported", and the canvas ignored the throw. Now:
    • within one table, the covered cells are emptied;
    • a selection that leaves the table deletes the rows it touches, and the text outside up to the table.
  • mergeBackCommand after a table joined the paragraph to the one above the table. Now a paragraph with text stays put. An empty one is removed, unless it is the body's last paragraph, and the caret moves to the end of the last cell.
  • Character formatting with a caret formatted the whole run. It now works as in Word:
    • inside a word, it formats the word (Intl.Segmenter, so Japanese works);
    • elsewhere, an empty run at the caret carries the formatting for the next typed text.
  • runPoint steps over the zero-width placeholder of an empty run, so typing there stays in that run.
  • New export: isEmptyParagraph.

Word behaviour (canvas)

  • Enter in an empty list item moves it up a level, and at the top level ends the list.
  • Backspace at the start of a list item removes its number.
  • Backspace and Delete at paragraph edges never fall through to the browser.

Testing

  • New unit tests:
    • range-edit.test.ts: cross-cell delete, delete from a paragraph into a table and from a table out of it, Backspace after a table (3 cases).
    • text-selection.test.ts: caret formatting for a word, a Japanese word, text typed next, and a double toggle.
  • commands.test.ts: two bold tests used to pass only because a caret bolded the whole run. They now select the text they mean.
  • pnpm test: 1278 passed, 8 skipped. pnpm typecheck, pnpm -r run typecheck, svelte-check, check:api-page and check:tree-shake all pass. oxlint reports the 2 known warnings.
  • Chromium, /editor, en and ja: every scenario above was reproduced before the fix and passes after it. That includes CDP IME composition, Track Changes on, vertical text, a document with a bare <w:p/>, and a 40-paragraph two-page document.
  • Random key fuzzing: 5 seeds, 200–250 steps each, plus 2 seeds with Track Changes on. The steps mix typing, Enter, Backspace/Delete, word delete, arrows, Shift selection, undo/redo, Bold, Tab, ⌘ jumps, ⌘A, IME and clicks. No DOM/model mismatch and no invalid package.

Breaking changes

None. Character formatting at a caret behaves differently, as described above, to match Word.

Checklist

  • I have read CLAUDE.md and followed the project's conventions.
  • I have added or updated tests for the change.
  • I have added or updated documentation where user-visible behavior changed.
  • If this is a breaking change, I have added a changeset / CHANGELOG entry and flagged it above.
  • I have re-read my own diff and removed dead code, debug prints, and stale comments.
  • If I used an LLM to draft this PR, I have verified each change myself, the PR represents real work that warrants a maintainer's review, and I am willing to defend each line in review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UAiVj14HzKtRDqK12BXTr6

Fixes found by adversarial testing of the editor canvas:

- A key pressed right after a click acted on the previous caret.
- Typing into an empty paragraph saved by Word (<w:p/>) or a new table row was lost.
- Cmd+A, Cmd+Up/Down, Cmd+Home/End left the caret outside every paragraph.
- Word / line deletes (Option/Cmd+Backspace), Enter during IME composition,
  spelling suggestions and browser-menu undo let the browser restructure the
  DOM, so the screen and the saved document diverged.
- Deleting a selection that crosses table cells did nothing.
- Backspace after a table joined the paragraph to the one above the table.
- Enter / Backspace in an empty list item now leave the list as in Word.
- Caret formatting (Cmd+B with no selection) formats the word or the text
  typed next instead of the whole run.
- A click in the header / footer margin moves the caret to the nearest line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UAiVj14HzKtRDqK12BXTr6
Copilot AI balanced review requested due to automatic review settings October 5, 2026 18:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@baseballyama
baseballyama merged commit ecb138d into main Oct 5, 2026
5 checks passed
@baseballyama
baseballyama deleted the fix/adversarial-editor branch October 5, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants