Repository navigation
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
## Description Adds the Lexical playground as a native desktop app on the Rune UI framework (`rust/apps/lexical-playground-rune`), as tmm asked, after wevm/zero's zbugs-rune. It replaces the web playground idea: the Rust port is meant for native apps. - `rust/apps/lexical-playground-core` (in the `rust/` workspace) is the editor binding both desktop playgrounds share. It holds the document as blocks of styled runs, maps positions to Lexical points (UTF-16) and back, and runs edits as steps of the conformance script language through the same interpreter that replays `scripts/*`. Deleting at a caret first extends the selection over a grapheme, word or line, as the browser's `Selection.modify` does for Lexical, so every step runs the same in headless JS. - The Rune app draws a mirror of the document in Rune's `text_input`, which does layout, hit-testing, caret movement and input methods. Edits go through Lexical before the field sees them (typing, Return, Shift-Return, Backspace and Delete with Option and Command, Tab, paste, ⌘A/B/I/U), and a change the field makes itself (an IME commit, a cut) is replayed into Lexical. It has a format toolbar and a side panel with the tree, the JSON, and parity: Replay vectors runs the conformance report in the app, and Export session writes a vector file that `packages/lexical-conformance/src/replay.test.ts` replays in JS. - The app is its own Cargo workspace, pinned to Rune's toolchain (1.99.1). `bundle.sh` runs it as an app bundle on macOS. The new `rust-playground.yml` workflow lints, tests and renders it headless on Linux and macOS. - DESIGN.md: a Playgrounds section, and rich-text and history move to M2, because a text surface needs them first. ## Test plan ### Before The playgrounds did not exist. ### After ``` $ cd rust && cargo test -p lexical-playground-core test result: ok. 6 passed; 0 failed $ cd rust/apps/lexical-playground-rune && cargo clippy --all-targets -- -D warnings && cargo test test result: ok. 4 passed; 0 failed $ cargo run -- --snap typed.png --size 1100x720 --scene typed ``` The Rune tests drive the window's real key routing: typing, Return, Backspace and ⌘B in the field, a toolbar button that formats the selection and leaves the keyboard in the editor, and an IME-style change replayed in Lexical. A 19-step session exported from the core binding replays in JS with every step matching (`LEXICAL_REPLAY=… pnpm exec vitest run src/replay.test.ts` in `packages/lexical-conformance`). The same file with one recorded result changed fails at that step. Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
## Description JS's `deleteCharacter`, `deleteWord`, `deleteLine` and `modify` extend or move the selection with the DOM's `Selection.modify`, which only text layout can answer. A headless editor has none, and throws `window object not found` there, as JS does. This adds the platform's side as a hook an app sets from its text layout, as the Swift port's `SelectionMovement`: - `SelectionMovement::landed_point(from, is_backward, granularity)`, `Editor::set_selection_movement`, `SelectionGranularity` and `SelectionAlter`. - `SelectionRef::modify(alter, is_backward, granularity)`. - With a movement, `$extendSelectionForDeletion` and `modify` continue as JS does after `domSelection.modify`: the unmergeable-node edge (meta#7301), `applyDOMRange`'s model side (`$internalResolveSelectionPoint`, `$normalizeSelectionPointsForBoundaries`, `$normalizeSelection`), `$shrinkSelectionToRoot`, `$swapPoints`, and the full `$modifySelectionAroundDecoratorsAndBlocks`. Without one, nothing changes. ## Test plan ### Before Deletion inside text failed with `window object not found` in every editor. ### After ``` $ cargo test -p lexical-editor test result: ok. 15 passed; 0 failed $ cargo run -q --locked --release -p lexical-conformance | **Total** | 212 | 99 | 0 | 113 | 0 | | ``` `deletion_and_modify_with_a_movement_match_js` checks 288 cases (deletion by character, word and line, and `modify` for every alter, direction and granularity, over emoji, combining marks, format boundaries, unmergeable, segmented and token nodes, line breaks and blocks) against JS run in jsdom with a root element and `Selection.prototype.modify` stubbed by the same rules as the test's movement. `headless_deletion_matches_js` is unchanged. Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
## Description Adds the second desktop playground tmm asked for, on GPUI (through GPUI Kit, as wevm/zero's zbugs-gpui), beside the Rune one: - `rust/apps/lexical-playground-gpui`: an editor element of its own that draws the Lexical document, shapes and wraps it, hit-tests the pointer and moves the caret, and is the window's text input client (`EntityInputHandler`). Keys are GPUI actions bound as macOS binds them (Return, Shift-Return, Backspace/Delete by grapheme, Option by word, Command by line, ⌘A/B/I/U, copy, cut, paste). A toolbar, Tree, JSON and Parity panels, a status bar, and `bundle.sh` so `cargo run` launches a `.app`. - `Surface` in `lexical-playground-core`: the input-client side, shared by both apps. It maps UTF-16 ranges to Lexical points and keeps an input method's composition out of Lexical until it commits. - An end-to-end mode (`--features e2e`), as zbugs-gpui has: the app types, formats, composes, deletes and breaks lines in its own window through the input handler and key bindings, checks each step, and exports the session. The new `gpui-macos` CI job runs it and replays the session in JS. - `replay_vectors` returns a short summary sized for a side panel instead of the Markdown table, and the panels scroll and wrap. - Rune fixes found running it in a real macOS window: the editor's text was white on the light appearance, and the toolbar sat under the traffic lights. A `parity` scene renders the panel after Replay vectors, and CI snapshots it. ## Test plan ### Before No GPUI app. In a light-mode macOS window the Rune editor's text was invisible. ### After On a macOS 27 Mac: ``` $ cargo clippy --all-targets --features e2e -- -D warnings # and without e2e $ cargo build --locked --features e2e $ LEXICAL_PLAYGROUND_E2E=/tmp/gpui-e2e.json ./target/debug/lexical-playground-gpui e2e: passed $ cd packages/lexical-conformance $ LEXICAL_REPLAY=/tmp/gpui-e2e.json pnpm exec vitest run src/replay.test.ts Test Files 1 passed (1) ``` Driven by hand with real keystrokes and clicks (System Events and CGEvent): a 59-step GPUI session and a 37-step Rune session (typing, Return, ⌘B, Backspace, ⌥⌫, click to place the caret) each replay in JS with every step equal. Replay vectors in both apps reports 212 cases, 99 match JS, 113 wait on a package, 0 fail. ``` $ cd rust && cargo test --workspace --all-features --locked # all pass $ cd rust/apps/lexical-playground-rune && cargo test --locked # 4 passed ``` The GPUI app is not built on Linux. Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
## Description The GPUI playground's sources were committed before `cargo fmt`, so the `GPUI playground (macOS)` CI job failed its format check. This only reformats `app.rs`, `e2e.rs` and `editor.rs`; no behavior changes. ## Test plan ### Before ``` $ cargo fmt --check # in rust/apps/lexical-playground-gpui Diff in .../src/editor.rs:647: ... ``` ### After ``` $ cargo fmt --check && cargo clippy --all-targets --locked --features e2e -- -D warnings (no output, exit 0) ``` Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
## Description tmm asked for app icons like wevm/zero's playgrounds have, and noticed the caret didn't blink in either app. - `AppIcon.icon` in each app: an Icon Composer document with the Lexical mark (the Swift playground's layers, scaled to 80% and moved up and left) and the framework's mark as a badge in the bottom right corner, the Rune and GPUI badges zbugs-rune and zbugs-gpui use, in the same place. `bundle.sh` compiles it with `actool` when it changes and names it in the `.app`'s Info.plist, as zbugs' do, so the Dock shows it. - The caret blinks as a Mac text field's does: on and off every 500 ms, solid again after any key, edit or move, and hidden while the editor or the window doesn't have the keyboard. GPUI: a timer task restarted from keystrokes (before their actions), edits and moves, stopped on blur and window deactivation. Rune draws its field's caret solid, so the app's `poll` hook keeps the phase on the window clock and wakes for the next one, and the view paints the caret clear in the off phases. ## Test plan ### Before The `.app`s had the generic icon, and the caret was always drawn. ### After On a macOS 27 Mac, after `cargo run --release` in each app: - The Dock shows both icons, with the badges (`ictool` renders checked in light and dark too). - Screen samples 150 ms apart show the caret alternating while idle, solid after each of 6 keystrokes, and gone with Finder active, in both apps. `cargo fmt --check`, `cargo clippy --all-targets --locked -- -D warnings` (GPUI also with `--features e2e`) and the Rune tests pass. Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
## Description
Ports `@lexical/history` (`packages/lexical-history/src/index.ts`) to
`lexical-editor` behind a new `history` feature, so the desktop playgrounds
can undo and redo, and adds the core commands and tags it uses:
- `history::register_history(editor, state, delay, date_now, on_change,
max_depth)` and `create_empty_history_state()`, with the full merge
logic (`createMergeActionGetter`, `getChangeType`, `isTextNodeUnchanged`,
the `history-merge`/`history-push`/`historic` tags), undo, redo, clear,
and the CAN_UNDO/CAN_REDO notifications. The clock is injectable
(`date_now`, in ms) so merging by time replays deterministically.
- `commands.rs`: `UNDO_COMMAND`, `REDO_COMMAND`, `CAN_UNDO_COMMAND`,
`CAN_REDO_COMMAND`, `CLEAR_HISTORY_COMMAND`, `CLEAR_EDITOR_COMMAND`, as
JS core defines them, and the update tags history uses.
- The conformance script language, in `ops.ts` and `scripts.rs`: a script's
`history: {delay}` registers history on the session's clock; a step's
`now` (ms) sets that clock (`Date.now()` in JS while the step runs); and
`dispatch` and handlers name Lexical's own commands by their JS names.
- New JS-recorded vectors, `scripts/history` (10 cases: merging within the
delay and pushing after it, undo/redo and the empty stacks, redo cleared
by an edit, the push and merge tags, selection-only updates, formatting,
paragraphs, deletion, paste/cut, composition, clearing).
`HistoryExtension` waits for the extension API (PARITY.md).
## Test plan
### Before
No history in Rust; `scripts/history` didn't exist.
### After
```
$ cargo run -q --locked --release -p lexical-conformance
| `scripts/history` | 10 | 10 | 0 | 0 | 0 | pass |
| **Total** | 222 | 109 | 0 | 113 | 0 | |
$ cargo test --workspace --all-features --locked # all pass, 3 new history tests
$ cargo +1.88 check -p lexical-editor --all-features --locked
$ cargo check -p lexical-editor --locked # history compiled out
```
`pnpm run generate` in `packages/lexical-conformance` adds only
`scripts/history.json`; no other vector changes. `tsc`, ESLint and Prettier
are clean. Making the Rust delay comparison `<=` fails case 1 at step 5.
Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
## Description ⌘Z did nothing in either desktop playground, because the editor had no history. The previous commit ports `@lexical/history`, and this one wires it in: - `lexical-playground-core` registers history on the playground's session (300 ms delay, on a clock the session stamps on every step), and adds `undo()`, `redo()`, `can_undo()` and `can_redo()`. A click that moves the selection runs as its own select-only step before the next edit, so typing after a click merges into one undo step the way a browser does. Exported cases now carry the script's history and handlers, so they replay in JS. - Rune: ⌘Z runs undo and ⌘⇧Z runs redo. - GPUI: `Undo` and `Redo` actions bound to `cmd-z` and `cmd-shift-z`; the e2e run includes both. ## Test plan ### Before Pressing ⌘Z in either app on macOS left the text unchanged. ### After - `cargo test -p lexical-playground-core`: 10 passed, including `undo_takes_back_typing_merged_by_time_and_redo_brings_it_back`. - `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` and the Rune and GPUI app workspaces (GPUI with and without `e2e`, on macOS) are clean. - Conformance: 222 cases, 109 match, 113 need an unported package, 0 fail. - Exported sessions with undo and redo (21 steps from the core, 17 from the GPUI e2e run on macOS) replay in JS through `replay.test.ts` with every step equal. - On macOS with real keystrokes, ⌘Z and ⌘⇧Z undo and redo in both apps, and typing within 300 ms comes back as one step. Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
## Description The GPUI playground always drew light, because GPUI Kit's init sets the light theme and nothing changed it, while the Rune playground follows the system appearance. The window now syncs GPUI Kit's theme with its appearance when it opens and again on every appearance change, so it switches live. The highlight mark, the one color the editor hardcoded, becomes a deep amber in dark mode, so light text on it stays readable. ## Test plan ### Before With macOS in dark mode, the GPUI playground opened, and stayed, light. ### After - `cargo fmt --check` and `cargo clippy --all-targets --locked -- -D warnings` (with and without `e2e`) are clean on macOS. - `cargo run --release` on macOS: launched in dark mode the window is dark; switching System Settings' appearance light → dark → light while it runs redraws the toolbar, editor and side panel each time; highlighted text reads on amber in dark and on yellow in light. Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
## Description The Rune playground follows the Mac's appearance, but drew highlighted text on the same light marker yellow in both, so the light text of dark mode was hard to read on it. `decorations` now takes the window's appearance and uses a deep amber in dark mode, as the GPUI playground does since the previous commit. ## Test plan ### Before In dark mode, highlighted text was white on light yellow. ### After - `cargo fmt --check`, `cargo clippy --all-targets --locked -- -D warnings` and `cargo test --locked` (4 passed) in the Rune app workspace. - `cargo run --release` on macOS with everything highlighted: white text on amber in dark mode, dark text on yellow in light, switching live in both directions. Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
## Description In both desktop playgrounds, ⌘B over a selection bolded it, the next ⌘B did nothing (but added two steps), and only a third press unbolded it. `formatText` splits the selected text into its own node and selects that node. Its start is then both the start of the new node and the end of the plain node before it. The playground compared the editor's selection with the app's by point, and the app maps a position to the end of the node before it, so before the next edit it sent a `select` step moving the anchor there. The selection then took that plain node's format, and `formatText`, which toggles the selection's format, set bold again instead of clearing it. JS does the same given those steps; a browser just never sends them, since the DOM selection stays where Lexical put it. The playground now compares positions: when the editor's selection is already at the app's positions, it keeps Lexical's points and sends no `select` step. Every ⌘B now toggles in one step, as in JS. Also, GPUI's toolbar draws a format that is on as a filled button, as Rune does: the theme's selected look barely showed in dark mode. ## Test plan ### Before - `format_toggles_on_and_off_with_one_press_each` (new) fails at press 1: `left: ["dogs"] right: []`. - The GPUI e2e run (new ⌘B steps over "xt", after "ne"): `e2e: ⌘B again: "xt" bold is None, the toolbar's true, not false`. ### After - `cargo test -p lexical-playground-core`: 11 passed. The workspace's fmt, clippy and tests are clean; conformance: 222 cases, 109 match, 113 need an unported package, 0 fail. - GPUI e2e on macOS: `e2e: passed`; its 21-step session replays in JS through `replay.test.ts` with every step equal (`Tests 1 passed`). Rune's fmt, clippy and tests (4 passed) are clean. - Real keystrokes on macOS, ⇧← ×4 then ⌘B ×3: bold, plain, bold, one step each, in both apps, with the B button following. Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d
tmm
force-pushed
the
rust-playgrounds
branch
from
November 14, 2026 16:52
5df08d5 to
04907bc
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #120 (
rust-selection); review that one first. This adds the desktop playgrounds tmm asked for in place of a web one, on Rune and on GPUI, as wevm/zero has zbugs on both. It also adds theSelection.modifyeditor hook that #120 said was coming next.What changes
Selection.modifyas an editor hook (lexical-editor):SelectionMovement::landed_point,Editor::set_selection_movementandSelectionRef::modify. With a movement set,deleteCharacter/deleteWord/deleteLineandmodifycontinue as JS does afterdomSelection.modify. Without one, behavior is unchanged: headless deletion inside text throwswindow object not found, as in JS. 288 cases recorded from JS (jsdom withSelection.prototype.modifystubbed by the same rules as the test's movement) match.rust/apps/lexical-playground-core: the binding both apps share. Every edit is a step of the conformance script language, run by the sameSessionthat replaysscripts/*, so a session exports as a vector file JS replays (packages/lexical-conformance/src/replay.test.ts).Surfaceis the input-client side (UTF-16 ranges, an input method's composition kept out of Lexical until it commits).rust/apps/lexical-playground-rune: Rune'stext_inputdraws a mirror of the document; edits run through Lexical first.rust/apps/lexical-playground-gpui: an editor element of its own, the window'sEntityInputHandler, with macOS key bindings. It also has an end-to-end mode (--features e2e), as zbugs-gpui does.cargo run --releasein an app's directory launches it as a.app(bundle.sh)..github/workflows/rust-playground.yml): Rune builds, tests and renders headless on Linux and macOS, including the Parity panel. GPUI runs its end-to-end session in a real macOS window, then the job replays that session in JS.rust/DESIGN.md: the Playgrounds section and the milestone table.Test plan
On a macOS 27 Mac, with real keystrokes and clicks:
cargo test --workspace --all-featuresand the Rust CI pass. The GPUI app isn't built on Linux.Try it