Skip to content

Write: native table and grid cells (rewrite of table/grid editing) - #39

Merged
vlidholt merged 16 commits into
mainfrom
feat/native-table-grid-cells
Oct 28, 2026
Merged

vlidholt merged 16 commits into
mainfrom
feat/native-table-grid-cells

Conversation

@vlidholt

@vlidholt vlidholt commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Why

Table cells and grid items were edited through TextFields overlaid on read-only embed nodes. Two editors competed for focus, IME, gestures and selection, and every fix (tap regions, click counting, SE-14 gesture opt-out) left another gap — e.g. double-clicking another word of the cell being edited. This PR removes the second editor: cells and grid items are ordinary document text.

Spec: specs/wonky_write/12-native-cells.md (new; specs 02, 04 §18, 11 §2.3 and the index point to it).

What changes

Document model (wonky_write_core)

  • A table is a run of lines with the line attribute cell: {table, row, align?}; a grid a run of lines with grid: {id, columns, bullets}. Order is the structure: columns are positional, row 0 is the header. Validator and normalizer rules are line-local.
  • CellEdits builds every structural edit (rows, columns, moves, alignment, set cell, replace from rows, grid columns/bullets/items) as a minimal change that keeps untouched cells' lines (text, comments, row ids), so concurrent typing in other cells transforms cleanly. Ragged rows are completed as part of any edit.
  • DocTreeBuilder builds TableBlock/GridBlock from lines or legacy embeds, so every exporter keeps working; importers now produce cell lines. The Markdown exporter picks the emphasis style per cell, as it already did for paragraphs.
  • CellUpgrade converts legacy table/grid embeds (ids and box kept).

Server

  • CellUpgrader commits the upgrade as a system op on load and after any commit that still holds a legacy embed (old clients, restores). It commits only at the head it was computed from, so it is idempotent and never duplicates.
  • MCP write.insert_table / write.update_table run on CellEdits (same arguments).

super_editor — patch SE-15

  • DocumentCellLayout groups consecutive components into rows or a flow (DocumentCellGroupView). Nearest-component lookup, slot hit testing and drag selection work in 2D. Nothing changes without a cell layout.

Write

  • Cell and item lines are ParagraphNodes. WriteCellLayout places them and paints borders, the header and grid bullets. Container frames and chrome wrap the group; CellStylePhase handles padding, alignment and header weight.
  • Keys: Enter, Shift+Enter, Backspace, Delete and deleting a selection are request handlers, so hardware keys, IME and macOS selectors behave alike. They never merge or split cells. Tab and ↑/↓ use the laid-out boxes.
  • Commands are rebuilt on CellEdits: each is one undo step and none is suggestable. The table popover is kept and applies replaceTable. Pasting into a cell joins the pasted lines; a pasted table gets new ids; plain-text copy writes TSV rows. In Pages mode, a table row moves to the next page as one.
  • Removed: InlineTableEditing, InlineGridEditing, FieldClicks, the tableEditing/gridEditing handles and Write's use of SE-14. Legacy embeds render read-only until the server upgrades them.

Right to left (follow-up commits)

  • Tables and grids take their direction from their first strong character (spec 12 §3.10): right to left, column 0 and the first item are on the right, bullets too; cells without strong characters follow the table; columns without an alignment are start-aligned. SE-15 DocumentCellPlacement.textDirection.
  • A cell group is no wider than the text column (it was as wide as the window).
  • ←/→ move visually in right-to-left blocks (also ⌥/⌘); SE-5 fixes the right-to-left line start/end (⌘← went to offset −1) and the task checkbox padding. Quote bars and step badges sit on the right of right-to-left blocks.
  • Spellcheck skips words in a script the dictionary has no letters of (no squiggles under Arabic in an English document).
  • Selection highlights and spelling/comment underlines of right-to-left text are painted on the text (they were a block's width to the right; SE-5 item 8).
  • At the end of a chip, button or arrow link in a right-to-left block, ← moves the caret out of it and → back in.

Chips, buttons and links (follow-up commits)

  • A click past a chip, button or arrow link at the end of a line puts the caret outside it (like →); a click on its end puts it inside (spec 11 §7.4).
  • The link card no longer opens when the pointer rests right of a line ending in a link or button: the hover needs a character under the pointer (DocumentGeometry.characterUnder).

Tests

  • Core: cells_test, cell_edits_test, the upgrade property test, round trips with CellCanon, and generators that produce (sometimes ragged) cell and grid lines, so the OT and normalizer fuzz tests cover them. 873+ tests pass.
  • super_editor: se15_cell_groups_test (71 tests), se5_fixes_test (right-to-left selection highlight) plus the existing layout and selection suites.
  • Write: rtl_test (layout of every right-to-left block, arrows, tables, grids, table width, chips); editor_clicks_test (clicks past and on line-end chips, buttons and arrow links, the link card on hover); widget tests for clicks, double-clicking another word in the same cell, clicks below short cells, typing, Tab, ↑/↓, Enter, Backspace, deletion across cells with undo, grid flow, Enter/Backspace/Tab in items. The full suite passes (811).
  • Server: cell_upgrader_test (load, idempotency, legacy commit by an old client, restore, Markdown export unchanged) and MCP table tools on lines.

Known limitations / follow-ups

  • A ⋯ menu in the table chrome (the cell context menu has the row and column commands).
  • The upgrade drops comments anchored on a whole legacy embed (the thread is orphaned), and a suggestion on an embed may notify its author that it was rejected (spec 12 §1.5).

🤖 Generated with Claude Code

vlidholt and others added 16 commits October 27, 2026 23:30
Cell and grid item line attributes, validator and normalizer rules, the
legacy embed upgrade, DocTree from cell lines, plain text and stats, and
CellEdits for every structural table and grid edit (spec 12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd tests

DocumentEdits builds table changes with CellEdits, the generators make
cell and grid lines, the Markdown exporter picks the emphasis style per
cell, and CellCanon compares tables in tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ditors

Cell and grid item lines map to paragraph nodes; table and grid commands
build their changes with CellEdits; InlineTableEditing, InlineGridEditing
and FieldClicks are gone; legacy embeds render read-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DocumentCellLayout groups consecutive components into rows or a flow;
nearest-component and region selection work in 2D (spec 12 §3.2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CellUpgrader commits the upgrade of legacy table and grid embeds as a
system op on load and after commits that still hold one. Write handles
Enter, Shift+Enter, Backspace, Delete, Tab, arrows and deleting
selections in cells without merging or splitting them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WriteCellLayout places cell and item nodes in SE-15 groups with borders,
header, bullets, container frames and chrome; CellStylePhase styles
them; widget tests cover clicks, double clicks, Tab, arrows, Enter and
deletion in cells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rids

Pasting into a cell joins the lines; a pasted table gets new ids; plain
text copies rows as TSV. Table, grid and popover tests move to cell
lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the index

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…0 px

Adds a test for a mouse drag across cells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Arabic or Hebrew words in an English document were all marked
misspelled. The checker now accepts a word that shares no letter with
the dictionary's alphabet (its words' letters and TRY, in either case).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Tables and grids take their direction from their first strong
  character: right to left, column 0 and the first grid item are on the
  right, bullets too; cells without strong characters follow the table
  and columns without an alignment are start-aligned (spec 12 §3.10).
- SE-15: DocumentCellPlacement.textDirection mirrors a group; a group is
  no wider than its first component's maxWidth (tables and grids were
  as wide as the window, not the text column).
- ←/→ move visually in right-to-left blocks, also by word and line.
- SE-5: right-to-left line start/end (⌘←/⌘→ went to offset -1) and the
  task checkbox padding.
- Quote bars, their indent and step badges sit on the right of
  right-to-left blocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The layers beneath a TextComponent's text are zero-size painters in a
Stack aligned top start, which in the right-to-left Directionality of a
paragraph, list item or task is the top right: selections and underlines
were drawn a block's width to the right. Aligned top left (SE-5 item 8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…et outside

A click beyond the last character's box of a chip, button or arrow link
leaves the caret outside the element, like →; a click on its end puts it
back inside. In right-to-left blocks the pill boundary keys swap (← out,
→ in) instead of being taken by the visual arrow handling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hover took the nearest caret position, and linkAt also looks at the
character before it, so the pointer anywhere right of a line ending in a
link or button showed its card (also after a click there, which leaves
the caret outside). The hover now needs a character under the pointer
(DocumentGeometry.characterUnder).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vlidholt
vlidholt marked this pull request as ready for review October 28, 2026 06:54
@vlidholt
vlidholt merged commit b367f80 into main Oct 28, 2026
41 checks passed
@vlidholt
vlidholt deleted the feat/native-table-grid-cells branch October 28, 2026 06:55
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant