Skip to content

feat(rust): carets and RangeSelection editing - #120

Merged
tmm merged 3 commits into
mainfrom
rust-selection
Nov 14, 2026
Merged

tmm merged 3 commits into
mainfrom
rust-selection

Conversation

@tmm

@tmm tmm commented Nov 14, 2026

Copy link
Copy Markdown
Member

Stacked on #119 (rust-update); review that one first. This finishes the editing part of M1 in rust/DESIGN.md: a Rust editor can now be typed into, not only updated through node methods.

What changes

  • Carets (caret.rs, caret_utils.rs), ported from LexicalCaret.ts and LexicalCaretUtils.ts: sibling, child and text point carets, caret ranges and text slices, $removeTextFromCaretRange, $normalizeCaret. One PointCaret type with a kind and a direction stands in for JS's six classes, as in Swift. Point.isBefore now goes through carets, and RangeSelection.getNodes/getTextContent are ported for both selection kinds.
  • Selection editing: insertText, insertRawText, insertNodes (both selection kinds), insertParagraph, insertLineBreak, removeText, formatText, toggleFormat, setFormat, setStyle, deleteCharacter, deleteWord, deleteLine, $insertNodes and $selectAll.
  • Deletion follows JS headless. Past the model, JS extends the selection with the DOM's Selection.modify. A headless editor has no window, so the update throws window object not found, and the vectors record that. A text layout will provide that step through an editor hook, as the Swift port's SelectionMovement does; that comes in the next PR, with the desktop playgrounds.
  • Conformance: the scripts interpreter is now a public Session in rust/conformance/src/scripts.rs, so the desktop playgrounds can drive their editor with it and export a session as a vector. The runner now also replays fuzz/core.

How it was checked

In rust/: cargo fmt --check, clippy -D warnings, cargo test, rustdoc -D warnings, cargo +1.88 check.

Vectors Cases Match JS Waiting on a package Fail
scripts/* (6 files) 47 47 0 0
fuzz/core 40 40 0 0
All 212 99 113 0

The 113 waiting cases need rich-text, list, link, code, hashtag or overflow.

Paths no vector covers are unit-tested against output recorded from the real JS: insertText redirects (token, segmented and tab nodes, element points), formatText with alignment, 120 headless deletion cases, tokenizeRawText, and getNodes/getTextContent. I also ran a temporary 1,500-session fuzz of formatText against JS, which matched on every step, but it isn't checked in.

Base automatically changed from rust-update to main November 14, 2026 16:40
Ports caret/LexicalCaret.ts and caret/LexicalCaretUtils.ts: sibling,
child and text point carets in both directions, caret ranges with their
text slices, $comparePointCaretNext, $normalizeCaret, $caretFromPoint,
$caretRangeFromSelection, $setSelectionFromCaretRange and
$removeTextFromCaretRange. Point.isBefore now orders points on different
nodes through carets, as JS does. RangeSelection and NodeSelection gain
getNodes and getTextContent ($getNodesFromCaretRangeCompat), with the
cached nodes cleared where JS clears them.

JS's six caret classes become one PointCaret with a kind and a direction,
as in the Swift port. Slots, NodeState and DecoratorNode branches are
no-ops until those are ported (rust/PARITY.md).

Checked in rust/: fmt, clippy -D warnings, cargo test (including
getNodes/getTextContent/isBackward for 10 range selections and a node
selection against output recorded from the JS package), rustdoc,
cargo +1.88 check. Conformance: scripts/caret 5/5 (was waiting on
carets); every other replayed case unchanged; 0 failures.

Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
## Description

The conformance runner's script interpreter (the one that replays
`scripts/*` vectors) moves to `rust/conformance/src/scripts.rs` as a public
`Session`: `Session::start(script)`, `session.editor()`, and
`session.run(step, snapshot)`, which returns the step's result in the field
order `ops.ts` writes. The suites call `scripts::check` as before. The
desktop playgrounds drive their editor through it, so a session can be
exported as a script and replayed in JS.

## Test plan

### Before

```
$ cargo run -q --locked --release -p lexical-conformance
| **Total** | 172 | 40 | 0 | 132 | 0 | |
```

### After

```
$ cargo run -q --locked --release -p lexical-conformance
| **Total** | 172 | 40 | 0 | 132 | 0 | |
```

Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
## Description

Ports the editing methods of `LexicalSelection.ts`: `insertText`,
`insertRawText`, `insertNodes` (both selection kinds), `insertParagraph`,
`insertLineBreak`, `removeText`, `formatText`, `toggleFormat`, `setFormat`,
`setStyle`, `deleteCharacter`, `deleteWord` and `deleteLine`, with
`$insertNodes` and `$selectAll`, `ParagraphNode.insertNewAfter`, and
`$splitTextPointCaretSlice`. The scripts interpreter runs the `selection`,
`selectAll`, `selectNodes` and `insertNodes` actions, and the conformance
runner now replays `fuzz/core`.

Deletion follows JS headless: past the model, JS extends the selection with
the DOM's `Selection.modify`, which a headless editor doesn't have, so the
update throws `window object not found`, as the vectors record.

## Test plan

### Before

```
$ cargo run -q --locked --release -p lexical-conformance
| `scripts/selection` | 19 | 2 | 0 | 17 | 0 | partial |
| `fuzz/core` | – | – | – | – | – | not implemented |
| **Total** | 172 | 40 | 0 | 132 | 0 | |
```

### After

```
$ cargo run -q --locked --release -p lexical-conformance
| `fuzz/core` | 40 | 40 | 0 | 0 | 0 | pass |
| `scripts/selection` | 19 | 19 | 0 | 0 | 0 | pass |
| **Total** | 212 | 99 | 0 | 113 | 0 | |
```

The remaining 113 cases wait on node packages (rich-text, list, link, code,
hashtag, overflow). Unit tests check paths no vector covers against JS's
output: insertText redirects, formatText with alignment, headless deletion
(120 cases), and `tokenizeRawText`. fmt, clippy, tests, rustdoc and the 1.88
check pass.

Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
@tmm
tmm merged commit 382186a into main Nov 14, 2026
20 checks passed
@tmm
tmm deleted the rust-selection branch November 14, 2026 16:52
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