From 396548522a353fc6339a91d55e9d0229b5387259 Mon Sep 17 00:00:00 2001 From: Amp Date: Sat, 14 Nov 2026 00:19:30 +0000 Subject: [PATCH 01/10] feat(rust): desktop playground on Rune MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- .github/workflows/rust-playground.yml | 114 + .../lexical-conformance/src/replay.test.ts | 49 + rust/Cargo.lock | 15 + rust/Cargo.toml | 2 +- rust/DESIGN.md | 33 +- rust/apps/lexical-playground-core/Cargo.toml | 19 + rust/apps/lexical-playground-core/src/lib.rs | 940 ++++ .../apps/lexical-playground-core/src/tests.rs | 138 + .../.cargo/config.toml | 4 + rust/apps/lexical-playground-rune/.gitignore | 1 + rust/apps/lexical-playground-rune/Cargo.lock | 4187 +++++++++++++++++ rust/apps/lexical-playground-rune/Cargo.toml | 28 + rust/apps/lexical-playground-rune/README.md | 61 + rust/apps/lexical-playground-rune/bundle.sh | 38 + .../rust-toolchain.toml | 5 + rust/apps/lexical-playground-rune/src/main.rs | 246 + .../apps/lexical-playground-rune/src/tests.rs | 96 + rust/apps/lexical-playground-rune/src/view.rs | 241 + 18 files changed, 6212 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/rust-playground.yml create mode 100644 packages/lexical-conformance/src/replay.test.ts create mode 100644 rust/apps/lexical-playground-core/Cargo.toml create mode 100644 rust/apps/lexical-playground-core/src/lib.rs create mode 100644 rust/apps/lexical-playground-core/src/tests.rs create mode 100644 rust/apps/lexical-playground-rune/.cargo/config.toml create mode 100644 rust/apps/lexical-playground-rune/.gitignore create mode 100644 rust/apps/lexical-playground-rune/Cargo.lock create mode 100644 rust/apps/lexical-playground-rune/Cargo.toml create mode 100644 rust/apps/lexical-playground-rune/README.md create mode 100755 rust/apps/lexical-playground-rune/bundle.sh create mode 100644 rust/apps/lexical-playground-rune/rust-toolchain.toml create mode 100644 rust/apps/lexical-playground-rune/src/main.rs create mode 100644 rust/apps/lexical-playground-rune/src/tests.rs create mode 100644 rust/apps/lexical-playground-rune/src/view.rs diff --git a/.github/workflows/rust-playground.yml b/.github/workflows/rust-playground.yml new file mode 100644 index 00000000000..4b8303c9262 --- /dev/null +++ b/.github/workflows/rust-playground.yml @@ -0,0 +1,114 @@ +name: Rust playgrounds + +# rust/apps: the desktop playgrounds on the Rust port. Each is a Cargo +# workspace of its own (Rune needs a newer Rust than lexical-editor's 1.88). +# Path-filtered because macOS runners are slow to start. Linux checks that +# the Rune app builds, passes its tests, and renders headless; macOS builds +# and renders it as it runs on tmm's Mac. + +on: + push: + branches: [main] + paths: + - 'rust/**' + - 'packages/lexical-conformance/vectors/**' + - '.github/workflows/rust-playground.yml' + # Any base, so a PR stacked on another one gets checked too. + pull_request: + paths: + - 'rust/**' + - 'packages/lexical-conformance/vectors/**' + - '.github/workflows/rust-playground.yml' + +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + rune-linux: + name: Rune playground (Linux) + runs-on: ubuntu-24.04 + timeout-minutes: 40 + defaults: + run: + working-directory: rust/apps/lexical-playground-rune + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + # Rune's windowing libraries, and a software Vulkan driver for its + # headless renderer on a runner without a GPU. + - name: Install system libraries + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libxkbcommon-dev libwayland-dev mesa-vulkan-drivers + # rustup is preinstalled; rust-toolchain.toml picks the version. + - name: Install the toolchain + run: rustup show active-toolchain || rustup toolchain install + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/apps/lexical-playground-rune/target + key: lexical-playground-rune-${{ runner.os }}-${{ hashFiles('rust/apps/lexical-playground-rune/Cargo.lock', 'rust/apps/lexical-playground-rune/rust-toolchain.toml') }} + restore-keys: lexical-playground-rune-${{ runner.os }}- + - name: Format + run: cargo fmt --check + - name: Lint + run: cargo clippy --all-targets --locked -- -D warnings + - name: Test + run: cargo test --locked + # The app's first frame with a typed session, drawn by Rune's own + # renderer as a window would. + - name: Render headless + run: | + cargo run --locked -- --snap "$RUNNER_TEMP/lexical-playground-rune-linux.png" --size 1100x720 --scene typed + test -s "$RUNNER_TEMP/lexical-playground-rune-linux.png" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: always() + with: + name: lexical-playground-rune-linux-snapshot + path: ${{ runner.temp }}/lexical-playground-rune-linux.png + if-no-files-found: ignore + + rune-macos: + name: Rune playground (macOS) + runs-on: macos-26 + timeout-minutes: 40 + defaults: + run: + working-directory: rust/apps/lexical-playground-rune + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Install the toolchain + run: rustup show active-toolchain || rustup toolchain install + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/apps/lexical-playground-rune/target + key: lexical-playground-rune-${{ runner.os }}-${{ hashFiles('rust/apps/lexical-playground-rune/Cargo.lock', 'rust/apps/lexical-playground-rune/rust-toolchain.toml') }} + restore-keys: lexical-playground-rune-${{ runner.os }}- + - name: Lint + run: cargo clippy --all-targets --locked -- -D warnings + - name: Test + run: cargo test --locked + - name: Render headless + run: | + cargo build --locked --release + ./target/release/lexical-playground-rune --snap "$RUNNER_TEMP/lexical-playground-rune-macos.png" --size 1100x720 --scene typed + test -s "$RUNNER_TEMP/lexical-playground-rune-macos.png" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: always() + with: + name: lexical-playground-rune-macos-snapshot + path: ${{ runner.temp }}/lexical-playground-rune-macos.png + if-no-files-found: ignore diff --git a/packages/lexical-conformance/src/replay.test.ts b/packages/lexical-conformance/src/replay.test.ts new file mode 100644 index 00000000000..e72d5811b10 --- /dev/null +++ b/packages/lexical-conformance/src/replay.test.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {readFileSync} from 'node:fs'; +import {describe, expect, test} from 'vitest'; + +import {type Script, startSession, type StepResult} from './ops'; + +// Replays a session another port recorded, such as one exported from a +// desktop playground in rust/apps, and checks that JS Lexical produces the +// same result at every step: +// +// cd packages/lexical-conformance +// LEXICAL_REPLAY=/path/to/session.json pnpm exec vitest run src/replay.test.ts +// +// The file is a `scripts/*` vector file (`{kind: 'script', cases}`) or one +// case. Without LEXICAL_REPLAY the test is skipped. + +type Case = Script & {results: StepResult[]}; + +const path = process.env.LEXICAL_REPLAY; + +function cases(): Case[] { + const file = JSON.parse(readFileSync(path as string, 'utf8')) as + | Case + | {cases: Case[]}; + return 'cases' in file ? file.cases : [file]; +} + +describe.skipIf(path === undefined)('replay', () => { + for (const c of path === undefined ? [] : cases()) { + test(c.name, () => { + const session = startSession(c); + c.steps.forEach((step, i) => { + const expected = c.results[i]; + const actual = session.run(step, expected.state !== undefined); + expect( + {...actual, step: i}, + `step ${i}: ${JSON.stringify(step)}`, + ).toEqual({...expected, step: i}); + }); + }); + } +}); diff --git a/rust/Cargo.lock b/rust/Cargo.lock index b8a922beecb..64578cd37b8 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -12,3 +12,18 @@ dependencies = [ [[package]] name = "lexical-editor" version = "0.0.0" + +[[package]] +name = "lexical-playground-core" +version = "0.0.0" +dependencies = [ + "lexical-conformance", + "lexical-editor", + "unicode-segmentation", +] + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 6f04ed712a3..1fc2df24247 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["lexical-editor", "conformance"] +members = ["lexical-editor", "conformance", "apps/lexical-playground-core"] [workspace.package] version = "0.0.0" diff --git a/rust/DESIGN.md b/rust/DESIGN.md index 310b0cb2936..e68ef167eda 100644 --- a/rust/DESIGN.md +++ b/rust/DESIGN.md @@ -170,16 +170,41 @@ Each milestone ends with green CI and a report to the Space. | Milestone | Scope | Vectors that pass when done | Status | | --- | --- | --- | --- | | M0 | The `rust/` workspace, JSON as JS writes it, the core nodes (root, paragraph, text, tab, line break), `parseEditorState` and `toJSON` in both forms, the conformance runner, CI | `meta`, `serialization/core`, the core-only cases of `property/documents` (JSON and text) | done | -| M1 | The core model: `editor.update` and the commit (dirty nodes, normalization, transforms, listeners, commands, tags), node creation and methods (`splitText`, `mergeWithSibling`, `spliceText`, `insertAfter`, `remove`, `replace`...) with UTF-16 offsets, `RangeSelection` and `NodeSelection` editing, carets, NodeState, slots, the schema facility, `DecoratorNode`, the extension API (`buildEditorFromExtensions`), the parity ledger | `scripts/*`, `fuzz/core`, `file/*` | in progress: the update cycle, `setEditorState`, commands, transforms, listeners and the node methods are done (`scripts/updates`, `commands` pass; `listeners`, `transforms` pass but for the cases that type); carets and selection reading (`getNodes`, `getTextContent`) are done (`scripts/caret` passes); next, `RangeSelection` editing | -| M2 | Node packages: `rich-text`, `list`, `link`, `code`, `overflow`, `hashtag`, with `selection` and `utils` | `serialization/*`, `property/documents` (JSON and text), `entities/hashtag-*` | | -| M3 | `history`, and the commands `property/sequences` dispatches | `property/sequences` | | +| M1 | The core model: `editor.update` and the commit (dirty nodes, normalization, transforms, listeners, commands, tags), node creation and methods (`splitText`, `mergeWithSibling`, `spliceText`, `insertAfter`, `remove`, `replace`...) with UTF-16 offsets, `RangeSelection` and `NodeSelection` editing, carets, NodeState, slots, the schema facility, `DecoratorNode`, the extension API (`buildEditorFromExtensions`), the parity ledger | `scripts/*`, `fuzz/core`, `file/*` | in progress: the update cycle, `setEditorState`, commands, transforms, listeners, the node methods, carets, selection reading and `RangeSelection`/`NodeSelection` editing are done (`scripts/*` and `fuzz/core` pass); next, the platform's `Selection.modify` as an editor hook, NodeState, `DecoratorNode`, the extension API | +| P | Desktop playgrounds (`apps/`): `lexical-playground-rune` on Rune, then `lexical-playground-gpui` on GPUI, sharing `lexical-playground-core`; they grow with each milestone below | the vectors, replayed in the app; a session exported from the app replays in JS (`replay.test.ts`) | in progress | +| M2 | `rich-text` (headings, quotes, and the command handlers a text surface dispatches: typing, Enter, Backspace, formatting) and `history` (undo), which the playgrounds need first; then the node packages `list`, `link`, `code`, `overflow`, `hashtag`, with `selection` and `utils` | `serialization/*`, `property/documents` (JSON and text), `entities/hashtag-*` | | +| M3 | The commands `property/sequences` dispatches | `property/sequences` | | | M4 | `html`: a DOM abstraction over an HTML parser, CSS declarations, export and import | `html/*`, `css/*`, `property/documents` (HTML) | | | M5 | `markdown` | `markdown/import-export`, `property/documents` (Markdown) | | | M6 | `yjs`: a port of `@lexical/yjs` on `yrs` (behind the feature), with new JS vectors for the binding (sevki/lexical-rs's `lexical-yjs` and jpcamara/yrby's `lexical-yjs-html` only as cross-checks) | the new collab vectors | | | M7 | Releases: the mirror, changesets, a name tmm settles | | | The `devtools/tree` vectors come with whichever milestone a tool needs -them; the `scenarios/*` vectors are Tier 2 and stay unreplayed. +them. The `scenarios/*` vectors, recorded from a real browser, need a text +layout behind `Selection.modify`; the playgrounds' binding replays them once +rich-text is in. + +## Playgrounds + +tmm asked for desktop playgrounds, as wevm/zero has zbugs on GPUI and on +Rune, rather than a web one: the Rust port is meant for native apps. Each +app is its own Cargo workspace under `apps/` (Rune needs Rust 1.90 or +later; the crate stays on 1.88), and both depend on +`apps/lexical-playground-core`, which is in this workspace: + +- Lexical owns the document and the selection; the framework's text field + draws a mirror of them, lays it out, hit-tests and moves the caret. Keys + that edit run as Lexical updates before the field sees them, and a change + the field makes itself (an input method's commit, a cut) is replayed into + Lexical as the text it replaced; then the mirror is rebuilt, as Lexical's + reconciler rebuilds the DOM. This is the Swift port's split with TextKit. +- Every edit is a step of the conformance script language, run by the same + interpreter that replays `scripts/*`. Export session writes the steps + and their results as a vector file that JS replays and compares + (`packages/lexical-conformance/src/replay.test.ts`); Replay vectors runs + the conformance report in the app. +- To try one: `cargo run --release` in its directory. On macOS a + `bundle.sh` runner makes a `.app` that stays in the Dock, as zbugs' do. ## Releases (planned for M7) diff --git a/rust/apps/lexical-playground-core/Cargo.toml b/rust/apps/lexical-playground-core/Cargo.toml new file mode 100644 index 00000000000..15d7a00e175 --- /dev/null +++ b/rust/apps/lexical-playground-core/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "lexical-playground-core" +description = "The editor binding the desktop playgrounds share: the document as text runs, offsets to selection points and back, and edits as conformance script steps." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish = false + +[dependencies] +lexical.workspace = true +lexical-conformance = { path = "../../conformance" } +# Characters and words as text layout steps over them (grapheme clusters, +# UAX #29 words), for deleting at a caret. +unicode-segmentation = "1" + +[lints] +workspace = true diff --git a/rust/apps/lexical-playground-core/src/lib.rs b/rust/apps/lexical-playground-core/src/lib.rs new file mode 100644 index 00000000000..639a98bc319 --- /dev/null +++ b/rust/apps/lexical-playground-core/src/lib.rs @@ -0,0 +1,940 @@ +//! The editor binding the desktop playgrounds share +//! (`lexical-playground-rune`, `lexical-playground-gpui`). +//! +//! Lexical owns the document and the selection; the app draws them, as +//! Lexical's reconciler draws the DOM. This crate gives the app: +//! +//! - [`Document`]: the editor state as blocks of styled runs, each block's +//! text in one string, so a UI framework can lay it out and hit-test it in +//! byte offsets. +//! - [`Pos`] and [`Document::point_at`] / [`Document::pos_of`]: between a +//! position in that text and a Lexical selection point (UTF-16 offsets). +//! - [`Playground`]: edits (typing, Enter, Backspace, formatting) run as +//! steps of the conformance script language (`ops.ts`), through the same +//! interpreter that replays the vectors. Every step and its result are +//! kept, so [`Playground::export_case`] writes the session as a +//! `scripts/*` vector case that JS Lexical replays +//! (`packages/lexical-conformance/src/replay.test.ts`): parity for a +//! session typed by hand. +//! - [`replay_vectors`]: the conformance report, run in the app. + +use lexical::json::{self, Object, Value}; +use lexical::{NodeRef, get_root}; +use lexical_conformance::scripts::Session; + +/// A run of a block: a text node, a tab, or a line break. +#[derive(Clone, Debug, PartialEq)] +pub struct Run { + pub key: String, + pub kind: RunKind, + /// The run's text: the node's text, `"\t"` for a tab, `"\n"` for a + /// line break. + pub text: String, + /// The text format bits (`IS_BOLD`, ...); 0 for a line break. + pub format: u32, + /// The node's inline CSS (`style`). + pub style: String, + /// The run's start in its block's text, in bytes. + pub start: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RunKind { + Text, + Tab, + LineBreak, +} + +impl Run { + pub fn end(&self) -> usize { + self.start + self.text.len() + } + + /// Whether a selection point can be inside it (a `TextNode`, which a + /// tab is too). + pub fn is_text(&self) -> bool { + self.kind != RunKind::LineBreak + } + + pub fn has_format(&self, name: &str) -> bool { + self.format & format_flag(name) != 0 + } +} + +/// A child of the root: a paragraph, or later a heading, quote, list ... +#[derive(Clone, Debug, PartialEq)] +pub struct Block { + pub key: String, + /// The node's type (`"paragraph"`, `"heading"`, ...). + pub kind: String, + /// A heading's `tag` (`"h1"` ...), from its JSON. + pub tag: Option, + /// The element format (`"left"`, `"center"`, ...; `""` for none). + pub align: String, + pub runs: Vec, + /// The runs' text, joined. + pub text: String, +} + +/// A position in the document: a block and a byte offset in its text. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default)] +pub struct Pos { + pub block: usize, + pub byte: usize, +} + +/// What a deletion at a caret takes: Backspace, Option-Backspace or +/// Command-Backspace. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Unit { + Character, + Word, + Line, +} + +/// A selection in positions; `anchor == focus` is a caret. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub struct Sel { + pub anchor: Pos, + pub focus: Pos, +} + +impl Sel { + pub fn caret(pos: Pos) -> Sel { + Sel { + anchor: pos, + focus: pos, + } + } + + pub fn is_collapsed(&self) -> bool { + self.anchor == self.focus + } + + /// The start and end, in document order. + pub fn ordered(&self) -> (Pos, Pos) { + if self.anchor <= self.focus { + (self.anchor, self.focus) + } else { + (self.focus, self.anchor) + } + } +} + +/// A selection point as the scripts write it: `[key, offset, type]`, the +/// offset in UTF-16 code units for a text point. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Point { + pub key: String, + pub offset: usize, + pub text: bool, +} + +impl Point { + fn to_json(&self) -> Value { + Value::Array(vec![ + self.key.as_str().into(), + (self.offset as f64).into(), + (if self.text { "text" } else { "element" }).into(), + ]) + } + + fn from_json(v: &Value) -> Option { + let a = v.as_array()?; + Some(Point { + key: a.first()?.as_str()?.to_owned(), + offset: a.get(1)?.as_f64()? as usize, + text: a.get(2)?.as_str()? == "text", + }) + } +} + +/// The editor state, as the app draws it. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Document { + pub blocks: Vec, +} + +impl Document { + /// Reads the current editor state. Call it in a read of the editor. + fn read() -> Document { + let blocks = get_root().get_children().iter().map(read_block).collect(); + Document { blocks } + } + + /// The last position. + pub fn end(&self) -> Pos { + let block = self.blocks.len().saturating_sub(1); + Pos { + block, + byte: self.blocks.get(block).map_or(0, |b| b.text.len()), + } + } + + /// The selection point at `pos`. Inside or at the end of a text run it + /// is a text point in that run (the run before when `pos` is between + /// two); next to line breaks and in an empty block it is an element + /// point in the block. + pub fn point_at(&self, pos: Pos) -> Point { + let Some(block) = self.blocks.get(pos.block) else { + return Point { + key: "root".into(), + offset: self.blocks.len(), + text: false, + }; + }; + let byte = floor_char_boundary(&block.text, pos.byte); + let in_run = |r: &&Run| r.is_text() && r.start < byte && byte <= r.end(); + let at_start = |r: &&Run| r.is_text() && r.start == byte; + if let Some(run) = block + .runs + .iter() + .find(in_run) + .or_else(|| block.runs.iter().find(at_start)) + { + return Point { + key: run.key.clone(), + offset: utf16_len(&run.text[..byte - run.start]), + text: true, + }; + } + Point { + key: block.key.clone(), + offset: block.runs.iter().filter(|r| r.start < byte).count(), + text: false, + } + } + + /// The position of a selection point, if its node is drawn. + pub fn pos_of(&self, point: &Point) -> Option { + if point.key == "root" { + return Some(match self.blocks.get(point.offset) { + Some(_) => Pos { + block: point.offset, + byte: 0, + }, + None => self.end(), + }); + } + for (i, block) in self.blocks.iter().enumerate() { + if block.key == point.key { + let byte = block + .runs + .get(point.offset) + .map_or(block.text.len(), |r| r.start); + return Some(Pos { block: i, byte }); + } + if let Some(run) = block.runs.iter().find(|r| r.key == point.key) { + let byte = if point.text { + run.start + utf16_to_byte(&run.text, point.offset) + } else { + run.start + }; + return Some(Pos { block: i, byte }); + } + } + None + } + + /// The document as one string: the blocks' text joined with `\n`, as + /// text fields that keep one paragraph per line hold it. + pub fn text(&self) -> String { + let texts: Vec<&str> = self.blocks.iter().map(|b| b.text.as_str()).collect(); + texts.join("\n") + } + + /// `pos` as a byte offset in [`Document::text`]. + pub fn offset_of(&self, pos: Pos) -> usize { + let before: usize = self.blocks[..pos.block.min(self.blocks.len())] + .iter() + .map(|b| b.text.len() + 1) + .sum(); + before + pos.byte + } + + /// The position of a byte offset in [`Document::text`] (clamped). + pub fn pos_at(&self, mut offset: usize) -> Pos { + for (block, b) in self.blocks.iter().enumerate() { + if offset <= b.text.len() { + return Pos { + block, + byte: floor_char_boundary(&b.text, offset), + }; + } + offset -= b.text.len() + 1; + } + self.end() + } + + /// The line indices in [`Document::text`] that a line break starts + /// (a `LineBreakNode`, not a new block): Rune's soft breaks. + pub fn soft_breaks(&self) -> Vec { + let mut out = Vec::new(); + let mut line = 0u32; + for block in &self.blocks { + for run in &block.runs { + if run.kind == RunKind::LineBreak { + line += 1; + out.push(line); + } + } + line += 1; + } + out + } + + /// The tree as text, a node per line with its key, like Lexical's + /// TreeView: `(1) paragraph`, then ` (2) text "Hello" bold`. + pub fn tree(&self) -> String { + const FORMATS: [&str; 11] = [ + "bold", + "italic", + "strikethrough", + "underline", + "code", + "subscript", + "superscript", + "highlight", + "lowercase", + "uppercase", + "capitalize", + ]; + let mut out = String::from("(root) root\n"); + for block in &self.blocks { + let tag = block + .tag + .as_deref() + .map(|t| format!(" {t}")) + .unwrap_or_default(); + out.push_str(&format!(" ({}) {}{tag}\n", block.key, block.kind)); + for run in &block.runs { + let name = match run.kind { + RunKind::Text => "text", + RunKind::Tab => "tab", + RunKind::LineBreak => "linebreak", + }; + out.push_str(&format!(" ({}) {name}", run.key)); + if run.kind == RunKind::Text { + out.push_str(&format!(" {:?}", run.text)); + } + for format in FORMATS.iter().filter(|f| run.has_format(f)) { + out.push_str(&format!(" {format}")); + } + out.push('\n'); + } + } + out + } + + /// Where deleting one `unit` from the caret at `pos` reaches, inside + /// its block: what the DOM's `Selection.modify('extend', ...)` gives + /// Lexical in a browser. `None` at the block's edge (Lexical merges + /// blocks itself, without the DOM). A line is the block's line between + /// line breaks; wrapped lines need the app's layout. + pub fn deletion_extent(&self, pos: Pos, backward: bool, unit: Unit) -> Option { + use unicode_segmentation::UnicodeSegmentation; + let text = &self.blocks.get(pos.block)?.text; + let byte = floor_char_boundary(text, pos.byte); + let (before, after) = text.split_at(byte); + let reach = match (unit, backward) { + (Unit::Character, true) => before.grapheme_indices(true).next_back().map(|(i, _)| i), + (Unit::Character, false) => after.graphemes(true).next().map(|g| byte + g.len()), + (Unit::Word, true) => { + // Spaces before the caret, then the word before them. + let words: Vec<(usize, &str)> = before.split_word_bound_indices().collect(); + let mut at = None; + for (i, w) in words.iter().rev() { + at = Some(*i); + if !w.trim().is_empty() { + break; + } + } + at + } + (Unit::Word, false) => { + let mut at = None; + for (i, w) in after.split_word_bound_indices() { + at = Some(byte + i + w.len()); + if !w.trim().is_empty() { + break; + } + } + at + } + (Unit::Line, true) => Some(before.rfind('\n').map_or(0, |i| i + 1)) + .filter(|&i| i < byte) + .or_else(|| before.ends_with('\n').then(|| byte - 1)), + (Unit::Line, false) => Some(after.find('\n').map_or(text.len(), |i| byte + i)) + .filter(|&i| i > byte) + .or_else(|| after.starts_with('\n').then_some(byte + 1)), + }?; + (reach != byte).then_some(Pos { + block: pos.block, + byte: reach, + }) + } + + /// The position one character before or after `pos`, across blocks. + pub fn step(&self, pos: Pos, backward: bool) -> Pos { + let Some(block) = self.blocks.get(pos.block) else { + return self.end(); + }; + if backward { + if pos.byte > 0 { + let byte = block.text[..pos.byte] + .char_indices() + .next_back() + .map_or(0, |(i, _)| i); + return Pos { + block: pos.block, + byte, + }; + } + if pos.block > 0 { + let block = pos.block - 1; + return Pos { + block, + byte: self.blocks[block].text.len(), + }; + } + return pos; + } + if let Some(c) = block.text[pos.byte..].chars().next() { + return Pos { + block: pos.block, + byte: pos.byte + c.len_utf8(), + }; + } + if pos.block + 1 < self.blocks.len() { + return Pos { + block: pos.block + 1, + byte: 0, + }; + } + pos + } +} + +fn read_block(node: &NodeRef) -> Block { + let json = node.export_json(false); + let str_field = |name: &str| json.get(name).and_then(Value::as_str).map(str::to_owned); + let mut text = String::new(); + let mut runs = Vec::new(); + for child in node.get_children() { + let (kind, run_text, format, style) = match child.get_type() { + "linebreak" => (RunKind::LineBreak, "\n".to_owned(), 0, String::new()), + _ if child.is_text() => { + let data = child.text_data(); + let kind = if child.get_type() == "tab" { + RunKind::Tab + } else { + RunKind::Text + }; + ( + kind, + data.get_text().to_owned(), + data.get_format() as u32, + data.get_style().to_owned(), + ) + } + // Inline elements (links) and decorators come with their + // packages; their text stands in for them until then. + _ => (RunKind::Text, child.get_text_content(), 0, String::new()), + }; + runs.push(Run { + key: child.get_key().to_owned(), + kind, + start: text.len(), + text: run_text.clone(), + format, + style, + }); + text.push_str(&run_text); + } + Block { + key: node.get_key().to_owned(), + kind: node.get_type().to_owned(), + tag: str_field("tag"), + align: str_field("format").unwrap_or_default(), + runs, + text, + } +} + +/// The flag of a text format (`"bold"` → 1), as `TEXT_TYPE_TO_FORMAT`. +pub fn format_flag(name: &str) -> u32 { + lexical::toggle_text_format_type(0, name, None) as u32 +} + +fn utf16_len(s: &str) -> usize { + s.encode_utf16().count() +} + +/// The byte offset of the UTF-16 offset `units` in `s`, clamped; an offset +/// inside a surrogate pair rounds down to the character's start. +fn utf16_to_byte(s: &str, units: usize) -> usize { + let mut seen = 0; + for (i, c) in s.char_indices() { + if seen + c.len_utf16() > units { + return i; + } + seen += c.len_utf16(); + } + s.len() +} + +/// The length in bytes of the longest common prefix, on a char boundary. +fn common_prefix(a: &str, b: &str) -> usize { + a.char_indices() + .zip(b.chars()) + .find(|((_, x), y)| x != y) + .map_or(a.len().min(b.len()), |((i, _), _)| i) + .min(a.len()) + .min(b.len()) +} + +/// The length in bytes of the longest common suffix, on a char boundary. +fn common_suffix(a: &str, b: &str) -> usize { + let mut n = 0; + for (x, y) in a.chars().rev().zip(b.chars().rev()) { + if x != y { + break; + } + n += x.len_utf8(); + } + n +} + +/// `JSON.stringify(value, null, 2)`. +pub fn pretty_json(value: &Value) -> String { + fn write(value: &Value, indent: usize, out: &mut String) { + let pad = |n: usize| " ".repeat(n); + match value { + Value::Array(items) if !items.is_empty() => { + out.push_str("[\n"); + for (i, item) in items.iter().enumerate() { + out.push_str(&pad(indent + 1)); + write(item, indent + 1, out); + out.push_str(if i + 1 < items.len() { ",\n" } else { "\n" }); + } + out.push_str(&pad(indent)); + out.push(']'); + } + Value::Object(object) if !object.is_empty() => { + out.push_str("{\n"); + let n = object.len(); + for (i, (key, item)) in object.iter().enumerate() { + out.push_str(&pad(indent + 1)); + out.push_str(&Value::from(key).to_string()); + out.push_str(": "); + write(item, indent + 1, out); + out.push_str(if i + 1 < n { ",\n" } else { "\n" }); + } + out.push_str(&pad(indent)); + out.push('}'); + } + other => out.push_str(&other.to_string()), + } + } + let mut out = String::new(); + write(value, 0, &mut out); + out +} + +fn floor_char_boundary(s: &str, byte: usize) -> usize { + let mut byte = byte.min(s.len()); + while !s.is_char_boundary(byte) { + byte -= 1; + } + byte +} + +/// One edit's outcome. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Outcome { + /// Why the step failed, when it did (the editor discarded the update). + pub error: Option, +} + +/// A playground editor: a script session, the document it draws, and the +/// selection. +pub struct Playground { + session: Session, + steps: Vec, + results: Vec, + document: Document, + /// The editor's selection, as the last step recorded it. + model_selection: Value, + /// The selection the app shows; it reaches the editor with the next + /// edit (as a `select` action), as Lexical reads the DOM selection. + selection: Option, + last_error: Option, +} + +impl Playground { + /// A playground whose root starts with `children` (serialized nodes). + pub fn new(children: Vec) -> Result { + let mut script = Object::new(); + script.insert("name", "playground".into()); + let session = Session::start(&Value::Object(script))?; + let mut playground = Playground { + session, + steps: Vec::new(), + results: Vec::new(), + document: Document::default(), + model_selection: Value::Null, + selection: None, + last_error: None, + }; + let mut append = Object::new(); + append.insert("do", "append".into()); + append.insert("key", "root".into()); + append.insert("nodes", Value::Array(children)); + playground.update(vec![Value::Object(append)]); + match playground.last_error.take() { + Some(e) => Err(e), + None => Ok(playground), + } + } + + /// [`Playground::new`] with the root's children from an editor state's + /// JSON (`{"root": {"children": [...]}}`). + pub fn from_json(state: &str) -> Result { + let state = json::parse(state).map_err(|e| e.to_string())?; + let children = state + .get("root") + .and_then(|r| r.get("children")) + .and_then(Value::as_array) + .cloned() + .ok_or("no root.children")?; + Playground::new(children) + } + + pub fn document(&self) -> &Document { + &self.document + } + + pub fn selection(&self) -> Option { + self.selection + } + + /// Moves the selection the app shows; the editor gets it with the next + /// edit. + pub fn set_selection(&mut self, selection: Option) { + self.selection = selection; + } + + /// The last edit's error, if it failed. + pub fn last_error(&self) -> Option<&str> { + self.last_error.as_deref() + } + + /// The selection's pending format (what typed text gets). + pub fn selection_format(&self) -> u32 { + self.model_selection + .as_array() + .filter(|a| a.first().and_then(Value::as_str) == Some("range")) + .and_then(|a| a.get(3)) + .and_then(Value::as_f64) + .map_or(0, |f| f as u32) + } + + /// Whether `name` (`"bold"`, ...) is on at the selection: in the pending + /// format, as Lexical's toolbar reads it. + pub fn has_format(&self, name: &str) -> bool { + self.selection_format() & format_flag(name) != 0 + } + + /// The editor state's JSON (`JSON.stringify(editorState.toJSON())`). + pub fn state_json(&self) -> String { + self.session + .editor() + .get_editor_state() + .to_json(false) + .to_string() + } + + /// The editor state's JSON, indented. + pub fn state_pretty_json(&self) -> String { + pretty_json(&self.session.editor().get_editor_state().to_json(false)) + } + + /// How many steps the session has run. + pub fn step_count(&self) -> usize { + self.steps.len() + } + + pub fn insert_text(&mut self, text: &str) -> Outcome { + self.edit("insertText", vec![text.into()]) + } + + pub fn insert_raw_text(&mut self, text: &str) -> Outcome { + self.edit("insertRawText", vec![text.into()]) + } + + pub fn insert_paragraph(&mut self) -> Outcome { + self.edit("insertParagraph", vec![]) + } + + pub fn insert_line_break(&mut self) -> Outcome { + self.edit("insertLineBreak", vec![]) + } + + /// Backspace (`backward`) or Delete: `deleteCharacter`, `deleteWord` or + /// `deleteLine`. At a caret inside a block the selection is first + /// extended over what the key deletes ([`Document::deletion_extent`]), + /// as the browser's `Selection.modify` extends it for Lexical; at a + /// block's edge Lexical merges the blocks itself. So each step runs the + /// same in JS headless, and an exported session replays there. + /// Backspace at the document's start and Delete at its end do nothing, + /// as in a browser (headless, JS has no `Selection.modify` and throws). + pub fn delete(&mut self, unit: Unit, backward: bool) -> Outcome { + if let Some(sel) = self.selection + && sel.is_collapsed() + { + match self.document.deletion_extent(sel.focus, backward, unit) { + Some(reach) => { + self.selection = Some(Sel { + anchor: sel.focus, + focus: reach, + }); + } + None if sel.focus + == if backward { + Pos::default() + } else { + self.document.end() + } => + { + return Outcome::default(); + } + None => {} + } + } + let method = match unit { + Unit::Character => "deleteCharacter", + Unit::Word => "deleteWord", + Unit::Line => "deleteLine", + }; + self.edit(method, vec![backward.into()]) + } + + pub fn delete_character(&mut self, backward: bool) -> Outcome { + self.delete(Unit::Character, backward) + } + + pub fn delete_word(&mut self, backward: bool) -> Outcome { + self.delete(Unit::Word, backward) + } + + pub fn delete_line(&mut self, backward: bool) -> Outcome { + self.delete(Unit::Line, backward) + } + + pub fn remove_text(&mut self) -> Outcome { + self.edit("removeText", vec![]) + } + + /// `formatText(name)`: toggles a format on the selected text, or on the + /// pending format at a caret. + pub fn format_text(&mut self, name: &str) -> Outcome { + self.edit("formatText", vec![name.into()]) + } + + /// `$selectAll()`. + pub fn select_all(&mut self) -> Outcome { + let mut action = Object::new(); + action.insert("do", "selectAll".into()); + self.update(vec![Value::Object(action)]) + } + + /// The text the selection covers. + pub fn selected_text(&self) -> String { + let Some(sel) = self.selection else { + return String::new(); + }; + let (start, end) = sel.ordered(); + let blocks = &self.document.blocks; + let mut out = String::new(); + for (i, block) in blocks + .iter() + .enumerate() + .take(end.block + 1) + .skip(start.block) + { + let text = &block.text; + let from = if i == start.block { start.byte } else { 0 }; + let to = if i == end.block { end.byte } else { text.len() }; + if i > start.block { + out.push_str("\n\n"); + } + out.push_str(&text[from.min(to)..to]); + } + out + } + + /// Replays a change a text field made on its own (an input method's + /// commit, a cut, a drag) as a Lexical edit: the text that differs + /// between [`Document::text`] and `new_text` is selected and replaced + /// (`insertText`, `insertRawText` when it has new lines, or `removeText`). + /// Returns `None` when the text is the same. + pub fn apply_text(&mut self, new_text: &str) -> Option { + let old = self.document.text(); + if old == new_text { + return None; + } + let prefix = common_prefix(&old, new_text); + let suffix = common_suffix(&old[prefix..], &new_text[prefix..]); + let inserted = &new_text[prefix..new_text.len() - suffix]; + self.selection = Some(Sel { + anchor: self.document.pos_at(prefix), + focus: self.document.pos_at(old.len() - suffix), + }); + Some(if inserted.is_empty() { + self.remove_text() + } else if inserted.contains('\n') { + self.insert_raw_text(inserted) + } else { + self.insert_text(inserted) + }) + } + + fn edit(&mut self, method: &str, args: Vec) -> Outcome { + let mut action = Object::new(); + action.insert("do", "selection".into()); + action.insert("method", method.into()); + action.insert("args", Value::Array(args)); + self.update(vec![Value::Object(action)]) + } + + /// Runs one update step: first a `select` action when the app's + /// selection isn't the editor's, then `actions`. + pub fn update(&mut self, actions: Vec) -> Outcome { + let mut all = self.select_action().into_iter().collect::>(); + all.extend(actions); + let mut step = Object::new(); + step.insert("update", Value::Array(all)); + self.run(Value::Object(step)) + } + + fn select_action(&self) -> Option { + let sel = self.selection?; + let anchor = self.document.point_at(sel.anchor).to_json(); + let focus = self.document.point_at(sel.focus).to_json(); + let current = self.model_selection.as_array(); + if let Some(c) = current + && c.first().and_then(Value::as_str) == Some("range") + && c.get(1) == Some(&anchor) + && c.get(2) == Some(&focus) + { + return None; + } + let mut action = Object::new(); + action.insert("do", "select".into()); + action.insert("anchor", anchor); + action.insert("focus", focus); + Some(Value::Object(action)) + } + + fn run(&mut self, step: Value) -> Outcome { + let result = match self.session.run(&step, true) { + Ok(r) => r, + Err(e) => { + self.last_error = Some(e.clone()); + return Outcome { error: Some(e) }; + } + }; + self.steps.push(step); + self.results.push(result.json.clone()); + self.document = self + .session + .editor() + .read(Document::read) + .unwrap_or_default(); + self.model_selection = result.json.get("selection").cloned().unwrap_or(Value::Null); + self.selection = self.selection_from_model(); + self.last_error = result.error.map(|e| e.to_string()); + Outcome { + error: self.last_error.clone(), + } + } + + fn selection_from_model(&self) -> Option { + let s = self.model_selection.as_array()?; + if s.first()?.as_str()? != "range" { + return None; + } + let anchor = self.document.pos_of(&Point::from_json(s.get(1)?)?)?; + let focus = self.document.pos_of(&Point::from_json(s.get(2)?)?)?; + Some(Sel { anchor, focus }) + } + + /// The session as a `scripts/*` vector case: its steps, and the result + /// of each as this editor recorded it. JS Lexical replays it and + /// compares (`packages/lexical-conformance/src/replay.test.ts`). + pub fn export_case(&self, name: &str) -> Value { + let mut case = Object::new(); + case.insert("name", name.into()); + case.insert("steps", Value::Array(self.steps.clone())); + case.insert("results", Value::Array(self.results.clone())); + Value::Object(case) + } +} + +impl Playground { + /// [`Playground::export_case`] as a `scripts/*` vector file, which + /// `LEXICAL_REPLAY= pnpm exec vitest run src/replay.test.ts`, in + /// `packages/lexical-conformance`, replays in JS. + pub fn export_vector_file(&self, name: &str) -> String { + let mut file = Object::new(); + file.insert( + "description", + "A playground session, recorded by lexical-editor.".into(), + ); + file.insert("formatVersion", 1.0.into()); + file.insert("kind", "script".into()); + file.insert("cases", Value::Array(vec![self.export_case(name)])); + pretty_json(&Value::Object(file)) + } +} + +/// Replays the conformance vectors (`packages/lexical-conformance/vectors`, +/// or `$LEXICAL_CONFORMANCE_VECTORS`) and returns the report as Markdown, +/// and whether nothing failed. +pub fn replay_vectors() -> Result<(String, bool), String> { + let report = lexical_conformance::run(None)?; + Ok((report.markdown(), report.ok())) +} + +/// The document the playgrounds open with. +pub fn example() -> Vec { + let text = |text: &str, format: f64| { + let mut o = Object::new(); + o.insert("type", "text".into()); + o.insert("text", text.into()); + o.insert("format", format.into()); + Value::Object(o) + }; + let paragraph = |children: Vec| { + let mut o = Object::new(); + o.insert("type", "paragraph".into()); + o.insert("children", Value::Array(children)); + Value::Object(o) + }; + vec![ + paragraph(vec![ + text("Hello from ", 0.0), + text("Rust", 1.0), + text(". This document lives in a ", 0.0), + text("lexical-editor", 16.0), + text(" editor.", 0.0), + ]), + paragraph(vec![text( + "Type, delete, press Enter, or select text and make it bold.", + 0.0, + )]), + ] +} + +#[cfg(test)] +mod tests; diff --git a/rust/apps/lexical-playground-core/src/tests.rs b/rust/apps/lexical-playground-core/src/tests.rs new file mode 100644 index 00000000000..2487af8bc90 --- /dev/null +++ b/rust/apps/lexical-playground-core/src/tests.rs @@ -0,0 +1,138 @@ +use super::*; + +fn playground() -> Playground { + Playground::new(example()).expect("the example loads") +} + +#[test] +fn reads_the_example_as_blocks_of_runs() { + let p = playground(); + let doc = p.document(); + assert_eq!(doc.blocks.len(), 2); + let first = &doc.blocks[0]; + assert_eq!(first.kind, "paragraph"); + assert_eq!( + first.text, + "Hello from Rust. This document lives in a lexical-editor editor." + ); + assert!(first.runs[1].has_format("bold")); + assert_eq!(first.runs[1].start, "Hello from ".len()); +} + +#[test] +fn points_and_positions_round_trip_across_utf16() { + let mut p = Playground::new(vec![ + json::parse(r#"{"type":"paragraph","children":[{"type":"text","text":"a😀b"},{"type":"linebreak"},{"type":"text","text":"c","format":1}]}"#).unwrap(), + json::parse(r#"{"type":"paragraph","children":[]}"#).unwrap(), + ]) + .unwrap(); + let doc = p.document().clone(); + // After the emoji: byte 5, UTF-16 offset 3. + let point = doc.point_at(Pos { block: 0, byte: 5 }); + assert_eq!((point.offset, point.text), (3, true)); + assert_eq!(doc.pos_of(&point), Some(Pos { block: 0, byte: 5 })); + // Right after the line break: the start of the next text run. + let after_break = doc.point_at(Pos { block: 0, byte: 7 }); + assert_eq!((after_break.offset, after_break.text), (0, true)); + // The empty paragraph: an element point. + let empty = doc.point_at(Pos { block: 1, byte: 0 }); + assert_eq!((empty.offset, empty.text), (0, false)); + p.set_selection(Some(Sel::caret(Pos { block: 1, byte: 0 }))); + assert_eq!(p.insert_text("x").error, None); + assert_eq!(p.document().blocks[1].text, "x"); + assert_eq!(p.selection(), Some(Sel::caret(Pos { block: 1, byte: 1 }))); +} + +#[test] +fn typing_enter_and_formatting_edit_the_document() { + let mut p = playground(); + let end = Pos { + block: 1, + byte: p.document().blocks[1].text.len(), + }; + p.set_selection(Some(Sel::caret(end))); + assert_eq!(p.insert_paragraph().error, None); + assert_eq!(p.document().blocks.len(), 3); + assert_eq!(p.format_text("bold").error, None); + assert!(p.has_format("bold")); + assert_eq!(p.insert_text("Bold").error, None); + let block = &p.document().blocks[2]; + assert_eq!(block.text, "Bold"); + assert!(block.runs[0].has_format("bold")); + // Select "Hello" and italicize it. + p.set_selection(Some(Sel { + anchor: Pos { block: 0, byte: 0 }, + focus: Pos { block: 0, byte: 5 }, + })); + assert_eq!(p.selected_text(), "Hello"); + assert_eq!(p.format_text("italic").error, None); + let first = &p.document().blocks[0]; + assert_eq!(first.runs[0].text, "Hello"); + assert!(first.runs[0].has_format("italic")); + assert_eq!( + p.export_case("session") + .get("steps") + .and_then(Value::as_array) + .map(Vec::len), + Some(p.step_count()) + ); +} + +#[test] +fn a_change_the_field_made_replays_as_a_lexical_edit() { + let mut p = playground(); + let old = p.document().text(); + // An input method commits "家" over "Hello" in the first block. + let new = old.replacen("Hello", "家", 1); + assert_eq!(p.apply_text(&new).map(|o| o.error), Some(None)); + assert_eq!(p.document().text(), new); + // A cut across the two blocks. + let cut = format!("家{}", &new[new.find("Type").unwrap()..]); + assert_eq!(p.apply_text(&cut).map(|o| o.error), Some(None)); + assert_eq!(p.document().blocks.len(), 1); + assert_eq!(p.document().text(), cut); + assert_eq!(p.apply_text(&cut), None); + // Offsets round-trip through the joined text. + let doc = p.document(); + let pos = doc.pos_at(doc.text().len()); + assert_eq!(doc.offset_of(pos), doc.text().len()); +} + +#[test] +fn deleting_at_a_caret_takes_a_grapheme_a_word_or_a_line() { + let mut p = Playground::new(vec![ + json::parse(r#"{"type":"paragraph","children":[{"type":"text","text":"one two👍🏽"},{"type":"linebreak"},{"type":"text","text":"three"}]}"#).unwrap(), + json::parse(r#"{"type":"paragraph","children":[{"type":"text","text":"x"}]}"#).unwrap(), + ]) + .unwrap(); + let end_of_first_line = "one two👍🏽".len(); + let at = |byte| Some(Sel::caret(Pos { block: 0, byte })); + // Backspace takes the emoji with its skin tone whole. + p.set_selection(at(end_of_first_line)); + assert_eq!(p.delete_character(true).error, None); + assert_eq!(p.document().blocks[0].text, "one two\nthree"); + // Option-Backspace takes "two". + p.set_selection(at("one two".len())); + assert_eq!(p.delete_word(true).error, None); + assert_eq!(p.document().blocks[0].text, "one \nthree"); + // Command-Delete from the start of "three" to the block's end. + p.set_selection(at("one \n".len())); + assert_eq!(p.delete_line(false).error, None); + assert_eq!(p.document().blocks[0].text, "one \n"); + // Backspace at a block's start merges it into the one before. + p.set_selection(Some(Sel::caret(Pos { block: 1, byte: 0 }))); + assert_eq!(p.delete_character(true).error, None); + assert_eq!(p.document().blocks.len(), 1); + assert_eq!(p.document().blocks[0].text, "one \nx"); +} + +#[test] +fn deleting_past_the_documents_ends_does_nothing() { + let mut p = playground(); + p.set_selection(Some(Sel::caret(Pos::default()))); + assert_eq!(p.delete_character(true), Outcome::default()); + let end = p.document().end(); + p.set_selection(Some(Sel::caret(end))); + assert_eq!(p.delete_word(false), Outcome::default()); + assert_eq!(p.step_count(), 1, "no step ran"); +} diff --git a/rust/apps/lexical-playground-rune/.cargo/config.toml b/rust/apps/lexical-playground-rune/.cargo/config.toml new file mode 100644 index 00000000000..c01e660664b --- /dev/null +++ b/rust/apps/lexical-playground-rune/.cargo/config.toml @@ -0,0 +1,4 @@ +# `cargo run` on macOS runs the app from inside an app bundle (bundle.sh), +# rather than as a bare executable, as wevm/zero's zbugs-rune does. +[target.'cfg(target_os = "macos")'] +runner = "./bundle.sh" diff --git a/rust/apps/lexical-playground-rune/.gitignore b/rust/apps/lexical-playground-rune/.gitignore new file mode 100644 index 00000000000..b83d22266ac --- /dev/null +++ b/rust/apps/lexical-playground-rune/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/rust/apps/lexical-playground-rune/Cargo.lock b/rust/apps/lexical-playground-rune/Cargo.lock new file mode 100644 index 00000000000..fd995030ca2 --- /dev/null +++ b/rust/apps/lexical-playground-rune/Cargo.lock @@ -0,0 +1,4187 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accesskit" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e883d1d28c5fd5568c79ed2d2bd48d9f2fd3945c8974ed0f53690ceddb051be" +dependencies = [ + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f5baf62e34a4a6c30e3d697e4782f821a7337049c44c3b40576a27dfbb580d" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "phf", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3da8f27636015191b2264ee1df681503170a632cafd832fdc1edfdd4e3a2fd3" +dependencies = [ + "accesskit", + "hashbrown 0.17.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123828ceeec755b05255e3976cb0852993202de98a0626c97dd8d34246991f84" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5192871228c54f1b0fb4a85cef000e11a3804f25d2165f14afa0daa67f3f6" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498668b90da9995302a0055aaecad4756932333da2ddef6baee1c00844c430fc" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.17.1", + "static_assertions", + "windows", + "windows-core", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.2", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.21", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "arboard" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb9553e62312c8756a5979254fc6d7da8bbeb5986a1e17037fab932f7037917" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.61.2", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f426a31126c735126297bc5ea7a521263c9faff4f63ca415267ee625651c4f37" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.6", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0876700b0d007ad25d5508865a4259c3af9af6d296bf663a0d586d0bc3369f6f" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.6", +] + +[[package]] +name = "async-recursion" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.6", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.3", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.5", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a0cf66fd1eb2fb7b822cb78370d82303750809bc6ba855f3f58e517d3238db" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.2", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ef9e3e8154a45c7a95cff4c1299702106ad94062d7bb85fdc5d3a5afd91d45" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "clipboard-win" +version = "5.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052af7878466feccda12ba7a6a67d019d712f296809559b47c12c8cc196d3a9a" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b929dd2a135771f40afabd1073f85eed59c800b7c61901e5c254270497dad64" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35d6e78f4cebb90e51d543db8fe41528851d6deeef6ef21ebd4bfdc1e784776" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "fixedbitset" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5905ec755cc8386d7928353e4d356d2be3e89e81e5ca01fe630a1ca4d08dc175" + +[[package]] +name = "flate2" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626d4928326b646e70f65ed36fc7cf9f206bdb68a0019ee7c6b4709da4e084c4" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontique" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf1df16263783918b7da396077521f92b729ef7c429444dfcf03581f1448b77" +dependencies = [ + "hashbrown 0.17.1", + "linebender_resource_handle", + "memmap2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-text", + "objc2-foundation 0.3.2", + "parlance", + "read-fonts", + "roxmltree", + "smallvec", + "windows", + "windows-core", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.6", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.21", + "windows", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "read-fonts", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406e9ae5b95d8aa37db6293405931b766a6172d1848daee22d70eb44aa78a43c" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3632ebbd0c5c3637ba1a40bdf8288f2cb9d2268e73441a69d5fc73d7cfbaa87c" + +[[package]] +name = "icu_normalizer" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" + +[[package]] +name = "icu_properties" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b195dc605f81aa181bec9b3bd14e26cc71b903e4644f6af578ca9772c7a710" +dependencies = [ + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "smallvec", + "utf8_iter", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913a7d294ad056474749152107309fdee3ec17d2482d1ab70fdc61a81c64b9a3" + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itoa" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd1574e617a9a6c1f8f8181bd7e841e1f20e4277607ef28945765188d6284d4" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lexical-conformance" +version = "0.0.0" +dependencies = [ + "lexical-editor", +] + +[[package]] +name = "lexical-editor" +version = "0.0.0" + +[[package]] +name = "lexical-playground-core" +version = "0.0.0" +dependencies = [ + "lexical-conformance", + "lexical-editor", + "unicode-segmentation", +] + +[[package]] +name = "lexical-playground-rune" +version = "0.0.0" +dependencies = [ + "lexical-playground-core", + "rune", + "rune-kit", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "bitflags 2.13.2", + "libc", + "plain", + "redox_syscall 0.9.5", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "litrs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0ca1155a9ecc25aa12e8313a119f32edf0255fea2f44839c4ac0a12b2ac157" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "naga" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0dd362bf26c0ffdd143df6da66a25d287e196eff482f117771010b207ad163" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.17.1", + "indexmap", + "libm", + "log", + "naga-types", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.21", + "unicode-ident", +] + +[[package]] +name = "naga-types" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14474170feb673cfb18b3565111a9869797cc1d290fb9ec969c53f5e1e231c6d" +dependencies = [ + "hashbrown 0.17.1", + "indexmap", + "rustc-hash 1.1.0", + "thiserror 2.0.21", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d5490aaf8f1d7cf7688dfa9b0ce07900e168852c45cd2c03f534dfd27cfd0b" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.3", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-av-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478ae33fcac9df0a18db8302387c666b8ef08a3e2d62b510ca4fc278a384b6c0" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-media", + "objc2-core-video", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.5", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "dispatch2", + "libc", + "objc2 0.6.5", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-media" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.5", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-video", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.3", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "libc", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "dispatch2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "5.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f59006ce1e204c30956638535b74d317bca99477b9356dfaec54c37f6f600ab" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "parlance" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73072a9470554c8e794778acece2312661218aaabb635600957d952cf01c716e" + +[[package]] +name = "parley" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61bd0c0c1737f0b3d3726dc06b225083af213b61913e4fbae1670613a106c6ce" +dependencies = [ + "fontique", + "hashbrown 0.17.1", + "linebender_resource_handle", + "parlance", + "parley_engine", + "smallvec", +] + +[[package]] +name = "parley_data" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43b63f808f6cc8f4a3b07de0d62a977bddd2e0bb5c232bd9e93ba8ef92e5e4b" +dependencies = [ + "icu_collections", + "icu_properties", +] + +[[package]] +name = "parley_emoji" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c4482d912d7671a7cd2089794f90ef3bb5f5f8c738230fb0fcd50fa020f0ed3" + +[[package]] +name = "parley_engine" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74c0d373e462e504ad4436710a39f099e18e848e1ee711f70cd2dcd5a69b290" +dependencies = [ + "fontique", + "harfrust", + "hashbrown 0.17.1", + "icu_locale_core", + "icu_normalizer", + "icu_properties", + "icu_segmenter", + "linebender_resource_handle", + "parlance", + "parley_data", + "parley_emoji", + "skrifa", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.6", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types", + "once_cell", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_syscall" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "580c875845b41ecb1564ad1ae4703044cef774145e3d7a94dc2b3fa7c97868fd" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "roxmltree" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da7f59328b09405b181fee1018925fad50f157afb43c0c0f5c522c256dd5907" +dependencies = [ + "memchr", +] + +[[package]] +name = "rune" +version = "0.0.1" +source = "git+https://github.com/anomalyco/rune?rev=0add0d576dfe85712ddbf8b51d2a24221b45e786#0add0d576dfe85712ddbf8b51d2a24221b45e786" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "arboard", + "blake3", + "block2 0.5.1", + "block2 0.6.2", + "bytemuck", + "console_error_panic_hook", + "fontique", + "half", + "icu_properties", + "icu_segmenter", + "image", + "js-sys", + "libc", + "libloading", + "objc2 0.5.3", + "objc2 0.6.5", + "objc2-app-kit 0.2.2", + "objc2-av-foundation", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-media", + "objc2-core-video", + "objc2-foundation 0.2.2", + "objc2-foundation 0.3.2", + "objc2-io-surface", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "parking_lot", + "parley", + "png", + "pollster", + "portable-atomic", + "raw-window-handle", + "raw-window-metal", + "rune-motion", + "rune-staging", + "rune-taffy", + "rustc-demangle", + "rustc-hash 2.1.3", + "serde", + "serde_json", + "skrifa", + "swash", + "taffy", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", + "web-sys", + "web-time", + "wgpu", + "wgpu-core", + "wgpu-hal", + "wgpu-naga-bridge", + "windows", + "windows-sys 0.59.0", + "winit", + "x11rb", + "xkbcommon", + "zeroize", +] + +[[package]] +name = "rune-highlights" +version = "0.0.1" +source = "git+https://github.com/anomalyco/rune?rev=0add0d576dfe85712ddbf8b51d2a24221b45e786#0add0d576dfe85712ddbf8b51d2a24221b45e786" +dependencies = [ + "unicode-ident", + "wasmparser", +] + +[[package]] +name = "rune-kit" +version = "0.0.1" +source = "git+https://github.com/anomalyco/rune?rev=0add0d576dfe85712ddbf8b51d2a24221b45e786#0add0d576dfe85712ddbf8b51d2a24221b45e786" +dependencies = [ + "rune", + "rune-highlights", + "rune-motion", + "unicode-segmentation", +] + +[[package]] +name = "rune-motion" +version = "0.1.0" +source = "git+https://github.com/anomalyco/rune?rev=0add0d576dfe85712ddbf8b51d2a24221b45e786#0add0d576dfe85712ddbf8b51d2a24221b45e786" + +[[package]] +name = "rune-staging" +version = "0.0.1" +source = "git+https://github.com/anomalyco/rune?rev=0add0d576dfe85712ddbf8b51d2a24221b45e786#0add0d576dfe85712ddbf8b51d2a24221b45e786" +dependencies = [ + "wgpu", +] + +[[package]] +name = "rune-taffy" +version = "0.0.1" +source = "git+https://github.com/anomalyco/rune?rev=0add0d576dfe85712ddbf8b51d2a24221b45e786#0add0d576dfe85712ddbf8b51d2a24221b45e786" +dependencies = [ + "taffy", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "log", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "149309498a5f7334b12ddae0cad8aba81ce785fe91755068867e017cf98fee46" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.2", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "swash" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae9ccbea3a7b0b09443036490c8f34a8b1b760fd345e5c0dae0aac718faea79" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "taffy" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f7bd2d97198df05186f93a3a3c306845f40e8b3c22bf9279cdd7e5cf5fea7d" +dependencies = [ + "serde", + "slotmap", + "smallvec", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.6", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml_datetime" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173f25c883cd2176a2d8212155638c0a60d484a9d84546d270866f40f56b6e49" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.19+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd43688c4858d7264ae1cf9a9f874a67c3feb74a883b1cd7bebd6a4ea8307218" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", +] + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557" + +[[package]] +name = "utf8_iter" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3bd9d3e571c26fcce9d033b87ea5ab061b8d2ffe9b7a5675d810d68f03edf0" +dependencies = [ + "utf_types", +] + +[[package]] +name = "utf_types" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6" + +[[package]] +name = "uuid" +version = "1.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bed99139f7dac9b5cfe5f17be4d66dc7079a34dee1d74bcc77217119240e267c" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" +dependencies = [ + "cfg-if", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e14e0327cb5368ad60ccb22d04a9827a693c95a926d8d125e9fa39e0baec501" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.10", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasmparser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" +dependencies = [ + "bitflags 2.13.2", + "indexmap", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.6", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix 1.1.6", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.13.2", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.6", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d179596ff5ec0f6de4db64d54011bde6289cc49603d3a6e4ce3f329207ab88" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146fc3d654250e6207921a66c87534c621bc886c70df83dcf466c1d66813810e" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad661da2ee99f208c30090b585284b2ab5b8bf2358ddf2d2215592808ec62f1a" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.13.2", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "indexmap", + "log", + "naga", + "naga-types", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.21", + "wgpu-core-deps-apple", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3f2f24aee039ffe9ef527300daf6b7428c58edeefdaa1700f4fa0fd754df16" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1331b08fa2362c5d454eccc4448e6e4f01eccfea569a00e310adbfc4415f2bdc" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a57452fe453658e141aafdb09de1028afa498805fab518238ddf372a518813ff" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.13.2", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "gpu-allocator", + "hashbrown 0.17.1", + "libc", + "libloading", + "log", + "naga", + "naga-types", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "static_assertions", + "thiserror 2.0.21", + "wgpu-naga-bridge", + "wgpu-types", + "windows", + "windows-core", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44a62ead68aad7cd40ac9e2cd2b8ec64f32dfbad2765f429b9fb74b8b735869f" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9f6ee0bd50f5ab79fb8c25e3f33afb27126f034af4ba5909c7a0967058659e6" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "js-sys", + "log", + "naga-types", + "raw-window-handle", + "static_assertions", + "web-sys", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.2", + "block2 0.5.1", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3b93199df0ab874cdf0c6a94f0a0dc3423fe41f1cafb62b8bd7b1deb277aef" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wl-clipboard-rs" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "959c7ac3b8afde864f7a7448ac16f29b2b83350b7eccbf77fb781c3999aa6848" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.1.6", + "thiserror 2.0.21", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.6", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a974f48060a14e95705c01f24ad9c3345022f4d97441b8a36beb7ed5c4a02d" +dependencies = [ + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.2", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "yazi" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac366ea532fa8ee6a992cec9dea525b1dc284e69702f36c2ab5fbebadc81ec86" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.6", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.10", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812a12be4ead7a59e18ea8f047d08add0066b73bc62fc1467066a2472ae088dd" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "939f186d9ecebe044315a5cb6d6f78568bbc21152adc954d5a0c2f58bdbc9920" +dependencies = [ + "serde", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bf6d6cd6a4b601acb64cfb8284cdc643fcfa75ef26ae3c46ec8aa4e8a1fbb7" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2672456ba1cb54b5eaec136e63135b3c7495fb938e53b28a62e633496d486d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.10", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.10", + "winnow", +] diff --git a/rust/apps/lexical-playground-rune/Cargo.toml b/rust/apps/lexical-playground-rune/Cargo.toml new file mode 100644 index 00000000000..b7ed7c50a14 --- /dev/null +++ b/rust/apps/lexical-playground-rune/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "lexical-playground-rune" +version = "0.0.0" +description = "The Lexical playground as a native desktop app: lexical-editor drawn with the Rune UI framework." +edition = "2024" +license = "MIT" +publish = false + +# Its own workspace: Rune needs a newer Rust than lexical-editor's 1.88 +# (rust-toolchain.toml), and the app's dependencies stay out of rust/'s. +[workspace] + +[dependencies] +lexical-playground-core = { path = "../lexical-playground-core" } +# Rune is pre-release: pinned to a rev of `dev`, which is never rewritten +# (the rev wevm/zero's zbugs-rune uses). +rune = { git = "https://github.com/anomalyco/rune", rev = "0add0d576dfe85712ddbf8b51d2a24221b45e786", default-features = false, features = [ + "accessibility", + "embedded-fonts", + "metal-backend", +] } +rune-kit = { git = "https://github.com/anomalyco/rune", rev = "0add0d576dfe85712ddbf8b51d2a24221b45e786" } + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +all = { level = "warn", priority = -1 } diff --git a/rust/apps/lexical-playground-rune/README.md b/rust/apps/lexical-playground-rune/README.md new file mode 100644 index 00000000000..42e52b5a5df --- /dev/null +++ b/rust/apps/lexical-playground-rune/README.md @@ -0,0 +1,61 @@ +# Lexical playground on Rune + +The Lexical playground as a native desktop app: a document in +[`lexical-editor`](../../lexical-editor), the Rust port of Lexical, drawn and +edited with the [Rune](https://github.com/anomalyco/rune) UI framework. +macOS first; it also builds and renders on Linux. + +- **The editor.** Lexical owns the document and the selection. Rune's + `text_input` draws a mirror of them and does the layout, hit-testing, + caret movement and input-method composition. Every change goes through + Lexical: + - Typing, Return (`insertParagraph`), Shift-Return (`insertLineBreak`), + Backspace and Delete (with Option for a word, Command for a line), Tab, + paste (`insertRawText`), ⌘A, ⌘B, ⌘I and ⌘U run as Lexical updates + before the field sees them. + - A change the field still makes itself, such as an input method's + commit or a cut, is replayed into Lexical as the text it replaced. + - After each edit the mirror is rebuilt from Lexical's state. + - The shared binding, [`lexical-playground-core`](../lexical-playground-core), + also backs the GPUI playground. +- **The toolbar.** Bold, italic, underline, strikethrough, code, highlight, + subscript and superscript follow the selection's format, as in the web + playground. Select all, and Reset. +- **The side panel.** + - **Tree:** the node tree with keys. + - **JSON:** the editor state's JSON. + - **Parity:** **Replay vectors** runs the conformance vectors JS recorded + (`packages/lexical-conformance/vectors`) in the app. **Export session** + writes this session as a vector file (`$TMPDIR/lexical-playground-session.json`, + or `$LEXICAL_PLAYGROUND_EXPORT`), which JS Lexical replays and compares + at every step: + + ```sh + cd packages/lexical-conformance + LEXICAL_REPLAY=/path/to/lexical-playground-session.json pnpm exec vitest run src/replay.test.ts + ``` + +Undo, headings, lists, links, code blocks, HTML and Markdown arrive as the +port gets the packages ([DESIGN.md](../../DESIGN.md), milestones). + +## Running it + +```sh +cd rust/apps/lexical-playground-rune +cargo run --release +``` + +Rune is pre-release and pinned to a rev of its `dev` branch (the one +wevm/zero's zbugs-rune uses). It needs Rust 1.90 or later, so the app is a +Cargo workspace of its own with Rune's toolchain (`rust-toolchain.toml`), +apart from `rust/`, which builds on 1.88. rustup installs that toolchain on +the first build. + +On macOS, `cargo run` runs the app as an app: `bundle.sh`, Cargo's runner +there (`.cargo/config.toml`), makes `Lexical Playground (Rune).app` beside the +executable and runs the app from inside it, so it shows in the Dock. The +bundle stays, to open from Finder. + +Like every Rune app, it renders headless too: +`cargo run -- --snap playground.png --size 1100x720 --scene typed` (and +`--tree`, `--script`; see Rune's README). The scenes are `typed` and `json`. diff --git a/rust/apps/lexical-playground-rune/bundle.sh b/rust/apps/lexical-playground-rune/bundle.sh new file mode 100755 index 00000000000..70f3604212b --- /dev/null +++ b/rust/apps/lexical-playground-rune/bundle.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# Cargo's runner on macOS (.cargo/config.toml): runs the playground from +# inside an app bundle, `Lexical Playground (Rune).app` beside the +# executable, so it launches as an app in the Dock and the app switcher. The +# bundle stays, to open from Finder or keep in the Dock. Tests and other +# executables run as they are. +set -eu +exe=$1 +shift +[ "$(basename "$exe")" = lexical-playground-rune ] || exec "$exe" "$@" + +app="$(cd "$(dirname "$exe")" && pwd)/Lexical Playground (Rune).app" +mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" +cat > "$app/Contents/Info.plist" <<'PLIST' + + + + + CFBundleExecutable + lexical-playground-rune + CFBundleIdentifier + dev.lexical.playground-rune + CFBundleName + Lexical Playground (Rune) + CFBundlePackageType + APPL + LSApplicationCategoryType + public.app-category.developer-tools + NSHighResolutionCapable + + + +PLIST +# A hard link, not a copy: the executable can be large in a debug build. +ln -f "$exe" "$app/Contents/MacOS/lexical-playground-rune" +/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \ + -f "$app" +exec "$app/Contents/MacOS/lexical-playground-rune" "$@" diff --git a/rust/apps/lexical-playground-rune/rust-toolchain.toml b/rust/apps/lexical-playground-rune/rust-toolchain.toml new file mode 100644 index 00000000000..2bf148648b2 --- /dev/null +++ b/rust/apps/lexical-playground-rune/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +# Rune's own toolchain (it needs at least 1.90), as wevm/zero's zbugs-rune. +channel = "1.99.1" +components = ["rustfmt", "clippy"] +profile = "minimal" diff --git a/rust/apps/lexical-playground-rune/src/main.rs b/rust/apps/lexical-playground-rune/src/main.rs new file mode 100644 index 00000000000..856dac99e78 --- /dev/null +++ b/rust/apps/lexical-playground-rune/src/main.rs @@ -0,0 +1,246 @@ +//! The Lexical playground on Rune: a document in `lexical-editor`, drawn +//! and edited in a native window. +//! +//! Lexical owns the document and the selection. The editor is Rune's +//! `text_input` over a mirror of the document (`mirror`): the field lays the +//! text out, hit-tests the pointer, moves the caret and composes input +//! methods, but every change goes through Lexical. Keys that edit (typing, +//! Return, Backspace, ⌘B) are taken before the field sees them and run as +//! Lexical updates, and anything the field still changes on its own (an +//! input method's commit, a cut) is replayed into Lexical as the text it +//! replaced. After each edit the mirror is rebuilt from Lexical's state, as +//! Lexical's reconciler rebuilds the DOM. + +mod view; + +use lexical_playground_core::{Document, Outcome, Playground, Pos, RunKind, Sel, example}; +use rune::{AppSpec, Block, ClipboardContent, Key, KeyEvent, Mark, MarkRange, TextEdit, Tx}; + +/// The side panel's tab. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Panel { + Tree, + Json, + Parity, +} + +pub struct State { + pub playground: Playground, + /// The field's model: the document's text, formats and line breaks. + pub edit: TextEdit, + pub panel: Panel, + /// The conformance report, after Replay vectors. + pub report: Option<(String, bool)>, + /// What the last action said (an export's path, an edit's error). + pub status: Option, +} + +impl State { + fn new() -> State { + let playground = Playground::new(example()).expect("the example document loads"); + let mut state = State { + playground, + edit: TextEdit::new(""), + panel: Panel::Tree, + report: None, + status: None, + }; + state.edit.focused = true; + state.sync(); + state + } + + /// Rebuilds the field's model from the editor (keeping its focus). + pub fn sync(&mut self) { + let focused = self.edit.focused; + self.edit = mirror(self.playground.document(), self.playground.selection()); + self.edit.focused = focused; + } + + /// Hands the field's selection to the editor, before an edit. + fn take_selection(&mut self) { + let doc = self.playground.document(); + let sel = Sel { + anchor: doc.pos_at(self.edit.anchor), + focus: doc.pos_at(self.edit.focus), + }; + self.playground.set_selection(Some(sel)); + } + + /// Runs an edit at the field's selection, then redraws. + pub fn edit_with(&mut self, f: impl FnOnce(&mut Playground) -> Outcome) { + self.take_selection(); + let outcome = f(&mut self.playground); + self.status = outcome.error; + self.sync(); + } + + /// A toolbar button's edit: as [`State::edit_with`], then the keyboard + /// goes back to the editor, as a toolbar button in a web editor keeps + /// the selection. + pub fn toolbar_edit(&mut self, f: impl FnOnce(&mut Playground) -> Outcome) { + self.edit_with(f); + self.edit.focused = true; + } + + /// A key, before the field: true when it was an edit Lexical made. + fn key(&mut self, ev: &KeyEvent) -> bool { + let m = ev.mods; + match &ev.key { + Key::Text(s) if !m.cmd => { + let s = s.clone(); + self.edit_with(|p| p.insert_text(&s)); + } + Key::Enter if m.shift => self.edit_with(Playground::insert_line_break), + Key::Enter => self.edit_with(Playground::insert_paragraph), + Key::Backspace | Key::Delete => { + let backward = ev.key == Key::Backspace; + if m.cmd { + self.edit_with(|p| p.delete_line(backward)); + } else if m.alt { + self.edit_with(|p| p.delete_word(backward)); + } else { + self.edit_with(|p| p.delete_character(backward)); + } + } + Key::Tab if !m.shift => self.edit_with(|p| p.insert_raw_text("\t")), + Key::Command(c) => match c { + 'b' => self.edit_with(|p| p.format_text("bold")), + 'i' => self.edit_with(|p| p.format_text("italic")), + 'u' => self.edit_with(|p| p.format_text("underline")), + 'a' => self.edit_with(Playground::select_all), + // Undo comes with @lexical/history; until then the field's + // own undo would edit behind Lexical's back. + 'z' | 'y' => {} + _ => return false, + }, + _ => return false, + } + true + } + + /// Pasted text: `insertRawText`, as Lexical pastes plain text. + fn paste(&mut self, content: &ClipboardContent) -> bool { + let text = content.text().to_owned(); + self.edit_with(|p| p.insert_raw_text(&text)); + true + } + + /// The field changed its text itself: replay that change in Lexical. + fn changed(&mut self) { + let text = self.edit.text.clone(); + let outcome = self.playground.apply_text(&text); + if let Some(outcome) = outcome { + self.status = outcome.error; + } + self.sync(); + } +} + +/// The field's model for `doc`: its text, a mark per format the field +/// draws, and its line breaks as soft breaks. +pub fn mirror(doc: &Document, selection: Option) -> TextEdit { + let mut marks = Vec::new(); + let mut offset = 0; + for block in &doc.blocks { + for run in &block.runs { + if run.kind == RunKind::Text { + let range = offset + run.start..offset + run.end(); + for (name, mark) in [ + ("bold", Mark::Bold), + ("italic", Mark::Italic), + ("underline", Mark::Underline), + ("strikethrough", Mark::Strike), + ("code", Mark::Code), + ] { + if run.has_format(name) && !range.is_empty() { + marks.push(MarkRange::new(range.clone(), mark)); + } + } + } + } + offset += block.text.len() + 1; + } + let text = doc.text(); + let lines = text.matches('\n').count() + 1; + let mut edit = TextEdit::from_parts(text, marks, vec![Block::Paragraph; lines]) + .with_soft_breaks(doc.soft_breaks()); + let sel = selection.unwrap_or(Sel::caret(Pos::default())); + edit.set_selection(doc.offset_of(sel.anchor), doc.offset_of(sel.focus)); + edit +} + +/// Highlight, subscript and superscript, which the field has no marks for, +/// drawn over it. +pub fn decorations(doc: &Document) -> Vec { + let mut spans = Vec::new(); + let mut offset = 0; + for block in &doc.blocks { + for run in &block.runs { + let (start, end) = (offset + run.start, offset + run.end()); + if run.kind != RunKind::Text || start == end { + continue; + } + if run.has_format("highlight") { + let mut span = rune::Span::new(start, end); + span.bg = Some(rune::oklch(0.92, 0.12, 95.0)); + spans.push(span); + } + if run.has_format("subscript") || run.has_format("superscript") { + let mut span = rune::Span::new(start, end); + span.size = Some(11.0); + spans.push(span); + } + } + offset += block.text.len() + 1; + } + spans +} + +pub fn on_key(s: &mut State, ev: &KeyEvent, _: &mut Tx) -> bool { + s.key(ev) +} + +pub fn on_paste(s: &mut State, content: &ClipboardContent, _: &mut Tx) -> bool { + s.paste(content) +} + +pub fn on_change(s: &mut State, _: &mut Tx) { + s.changed(); +} + +/// A session headless snapshots show: typed text, a new paragraph, bold. +fn typed(s: &mut State) { + let end = s.playground.document().end(); + s.playground.set_selection(Some(Sel::caret(end))); + s.playground.insert_paragraph(); + s.playground.format_text("bold"); + s.playground.insert_text("Typed in Rust"); + s.playground.format_text("bold"); + s.playground.insert_text(", then plain."); + s.sync(); +} + +fn main() { + rune::run(AppSpec:: { + scenes: &[ + ("typed", typed), + ("json", |s| { + typed(s); + s.panel = Panel::Json; + }), + ], + // Inline code (`Mark::Code`) draws in the mono slot: the system's + // monospaced face (SF Mono on macOS). + families: ("Inter", "ui-monospace"), + ..AppSpec::new( + "Lexical Playground (Rune)", + (1100.0, 720.0), + State::new, + view::view, + ) + }); +} + +#[cfg(test)] +mod tests; diff --git a/rust/apps/lexical-playground-rune/src/tests.rs b/rust/apps/lexical-playground-rune/src/tests.rs new file mode 100644 index 00000000000..ff834017262 --- /dev/null +++ b/rust/apps/lexical-playground-rune/src/tests.rs @@ -0,0 +1,96 @@ +use super::*; +use rune::{Env, Mods, Runtime}; + +fn runtime() -> Runtime { + let mut rt = Runtime::new( + State::new(), + view::view, + (1100.0, 720.0), + Env::new(false, 2.0), + ); + rt.frame(0.0); + rt +} + +fn press(rt: &mut Runtime, key: Key, mods: Mods) { + rt.key(KeyEvent { key, mods }); + rt.frame(0.0); +} + +fn typed(rt: &mut Runtime, s: &str) { + for c in s.chars() { + press(rt, Key::Text(c.to_string()), Mods::default()); + } +} + +#[test] +fn typing_return_and_backspace_edit_the_lexical_document() { + let mut rt = runtime(); + // The caret starts at the document's start. + typed(&mut rt, "Hi "); + press(&mut rt, Key::Enter, Mods::default()); + typed(&mut rt, "x"); + press(&mut rt, Key::Backspace, Mods::default()); + let doc = rt.state.playground.document(); + assert_eq!(rt.state.playground.last_error(), None); + assert_eq!(doc.blocks.len(), 3); + assert_eq!(doc.blocks[0].text, "Hi "); + assert!(doc.blocks[1].text.starts_with("Hello from ")); + // The field mirrors the document, with the caret where Lexical put it. + assert_eq!(rt.state.edit.text, doc.text()); + assert_eq!(rt.state.edit.range(), ("Hi \n".len(), "Hi \n".len())); +} + +#[test] +fn cmd_b_bolds_what_is_typed_next_and_the_field_draws_it_bold() { + let mut rt = runtime(); + let cmd = Mods { + cmd: true, + ..Mods::default() + }; + press(&mut rt, Key::Command('b'), cmd); + typed(&mut rt, "Bold"); + let doc = rt.state.playground.document(); + let run = &doc.blocks[0].runs[0]; + assert_eq!((run.text.as_str(), run.has_format("bold")), ("Bold", true)); + assert!( + rt.state + .edit + .marks() + .iter() + .any(|m| m.mark == Mark::Bold && m.range() == (0..4)) + ); +} + +#[test] +fn the_fields_own_change_replays_in_lexical() { + let mut rt = runtime(); + // As an input method's commit: the field's text changes by itself. + let new = rt.state.edit.text.replacen("Hello", "家", 1); + rt.state.edit.text = new.clone(); + rt.state.changed(); + assert_eq!(rt.state.playground.document().text(), new); + assert_eq!(rt.state.playground.last_error(), None); +} + +#[test] +fn a_toolbar_button_formats_the_selection_and_keeps_the_keyboard() { + let mut rt = runtime(); + // Select "Hello" in the field, as a drag would. + rt.update(|s, _| s.edit.set_selection(0, 5)); + rt.frame(0.0); + let (x, y) = rt.find("format-italic").expect("the italic button"); + rt.pointer_down(x, y); + rt.pointer_up(x, y); + rt.frame(0.0); + let doc = rt.state.playground.document(); + assert_eq!(doc.blocks[0].runs[0].text, "Hello"); + assert!(doc.blocks[0].runs[0].has_format("italic")); + assert!(rt.state.edit.focused); + typed(&mut rt, "!"); + assert!( + rt.state.playground.document().blocks[0] + .text + .starts_with("!") + ); +} diff --git a/rust/apps/lexical-playground-rune/src/view.rs b/rust/apps/lexical-playground-rune/src/view.rs new file mode 100644 index 00000000000..c25fde2a5d3 --- /dev/null +++ b/rust/apps/lexical-playground-rune/src/view.rs @@ -0,0 +1,241 @@ +//! The window: a toolbar, the editor, and a side panel with the editor's +//! tree, its JSON, and parity with JS Lexical. + +use crate::{Panel, State, decorations, on_change, on_key, on_paste}; +use lexical_playground_core::{Playground, example, replay_vectors}; +use rune::{Color, Cx, El, ScrollSpec, TextStyle, col, row, text, text_input}; +use rune_kit::{Button, ButtonVariant, Kit, SegmentedControl}; +use std::sync::OnceLock; + +fn kit(dark: bool) -> &'static Kit { + static DARK: OnceLock = OnceLock::new(); + static LIGHT: OnceLock = OnceLock::new(); + if dark { + DARK.get_or_init(|| Kit::neutral(true)) + } else { + LIGHT.get_or_init(|| Kit::neutral(false)) + } +} + +const FORMATS: [(&str, &str); 8] = [ + ("bold", "B"), + ("italic", "I"), + ("underline", "U"), + ("strikethrough", "S"), + ("code", ""), + ("highlight", "Mark"), + ("subscript", "x₂"), + ("superscript", "x²"), +]; + +pub fn view(s: &State, cx: &mut Cx) -> El { + let k = kit(cx.system_dark()); + col() + .w_full() + .h_full() + .fill(k.c.page) + .child(toolbar(s, k)) + .child( + row() + .w_full() + .grow(1.0) + .gap(16.0) + .padding(16.0) + .child(editor(s, k)) + .child(panel(s, k)), + ) + .child(status_bar(s, k)) +} + +fn toolbar(s: &State, k: &Kit) -> El { + let mut bar = row().w_full().gap(6.0).padding(10.0).fill(k.c.bg_base); + bar = bar.child(text("Lexical").font(k.ty.body_medium).color(k.c.text_base)); + for (name, label) in FORMATS { + let on = s.playground.has_format(name); + bar = bar.child( + Button::new(label) + .variant(if on { + ButtonVariant::Contrast + } else { + ButtonVariant::Ghost + }) + .help(format!("Format {name}")) + .on_press(move |s: &mut State| s.toolbar_edit(|p| p.format_text(name))) + .build(k) + .key(format!("format-{name}")), + ); + } + bar.child( + Button::new("Select all") + .variant(ButtonVariant::Ghost) + .on_press(|s: &mut State| s.toolbar_edit(Playground::select_all)) + .build(k) + .key("select-all"), + ) + .child(rune::spacer()) + .child( + Button::new("Reset") + .variant(ButtonVariant::Ghost) + .on_press(|s: &mut State| { + s.playground = Playground::new(example()).expect("the example loads"); + s.status = None; + s.sync(); + s.edit.focused = true; + }) + .build(k) + .key("reset"), + ) +} + +fn editor(s: &State, k: &Kit) -> El { + let field = text_input(&s.edit, TextStyle::new(16.0, 400.0)) + .key("editor") + .w_full() + .grow(1.0) + .decorations(decorations(s.playground.document())) + .on_edit(|s: &mut State| &mut s.edit) + .on_input_key(on_key) + .on_input_paste(on_paste) + .on_input_change(on_change); + col() + .grow(1.0) + .h_full() + .padding(20.0) + .radius(10.0) + .fill(k.c.bg_base) + .border(1.0, k.c.border_base) + .child(field) +} + +fn panel(s: &State, k: &Kit) -> El { + let tabs = [Panel::Tree, Panel::Json, Panel::Parity]; + let picked = tabs.iter().position(|p| *p == s.panel); + let content = match s.panel { + Panel::Tree => mono(&s.playground.document().tree(), k), + Panel::Json => mono(&s.playground.state_pretty_json(), k), + Panel::Parity => parity(s, k), + }; + col() + .w(420.0) + .h_full() + .gap(10.0) + .child( + SegmentedControl::new(&["Tree", "JSON", "Parity"], picked) + .on_pick(move |s: &mut State, i, _| s.panel = tabs[i]) + .build(k), + ) + .child( + col() + .w_full() + .grow(1.0) + .padding(12.0) + .radius(10.0) + .fill(k.c.bg_base) + .border(1.0, k.c.border_base) + .scroll(ScrollSpec { + stick_bottom: false, + }) + .child(content), + ) +} + +fn mono(s: &str, k: &Kit) -> El { + text(s.to_owned()) + .font(k.ty.mono) + .color(k.c.text_base) + .selectable(true) + .w_full() +} + +fn parity(s: &State, k: &Kit) -> El { + let explain = "The editor here is lexical-editor. Replay runs the conformance \ + vectors that JS Lexical recorded; Export writes this session as a vector \ + that JS replays step by step, in packages/lexical-conformance:\n\n\ + LEXICAL_REPLAY= pnpm exec vitest run src/replay.test.ts"; + let mut out = col() + .w_full() + .gap(10.0) + .child( + text(explain) + .font(k.ty.small) + .color(k.c.text_muted) + .selectable(true) + .w_full(), + ) + .child( + row() + .gap(8.0) + .child( + Button::new("Replay vectors") + .on_press(|s: &mut State| s.report = Some(replay())) + .build(k) + .key("replay"), + ) + .child( + Button::new("Export session") + .on_press(|s: &mut State| s.status = Some(export(s))) + .build(k) + .key("export"), + ), + ); + if let Some((report, ok)) = &s.report { + out = out.child( + text(if *ok { + "Every replayed case matches JS." + } else { + "Some cases differ from JS." + }) + .font(k.ty.body_medium) + .color(if *ok { ok_color() } else { error_color() }), + ); + out = out.child(mono(report, k)); + } + out +} + +fn replay() -> (String, bool) { + replay_vectors().unwrap_or_else(|e| (e, false)) +} + +/// Writes the session's vector file; returns what to tell the user. +fn export(s: &State) -> String { + let path = std::env::var_os("LEXICAL_PLAYGROUND_EXPORT") + .map(std::path::PathBuf::from) + .unwrap_or_else(|| std::env::temp_dir().join("lexical-playground-session.json")); + match std::fs::write(&path, s.playground.export_vector_file("rune playground")) { + Ok(()) => format!( + "Exported {} steps to {}", + s.playground.step_count(), + path.display() + ), + Err(e) => format!("Can't write {}: {e}", path.display()), + } +} + +fn status_bar(s: &State, k: &Kit) -> El { + let (message, color) = match (&s.status, s.playground.last_error()) { + (_, Some(error)) => (format!("The last edit failed: {error}"), error_color()), + (Some(status), None) => (status.clone(), k.c.text_muted), + (None, None) => ( + format!( + "lexical-editor · {} steps · {} blocks", + s.playground.step_count(), + s.playground.document().blocks.len() + ), + k.c.text_muted, + ), + }; + row() + .w_full() + .px(16.0) + .py(8.0) + .child(text(message).font(k.ty.small).color(color).key("status")) +} + +fn ok_color() -> Color { + rune::oklch(0.63, 0.17, 149.0) +} + +fn error_color() -> Color { + rune::oklch(0.58, 0.22, 27.0) +} From a2e677bbaf58705e0a6eb5d52df0aced6c2259e8 Mon Sep 17 00:00:00 2001 From: Amp Date: Sat, 14 Nov 2026 00:27:40 +0000 Subject: [PATCH 02/10] feat(rust): Selection.modify as an editor hook ## 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 (#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 --- rust/PARITY.md | 18 +- rust/lexical-editor/src/editor.rs | 19 + rust/lexical-editor/src/lib.rs | 1 + rust/lexical-editor/src/selection_deletion.rs | 1139 ++++++++++++++++- .../src/selection_insert_nodes.rs | 2 +- 5 files changed, 1128 insertions(+), 51 deletions(-) diff --git a/rust/PARITY.md b/rust/PARITY.md index 04ecf87fa72..5b95781317f 100644 --- a/rust/PARITY.md +++ b/rust/PARITY.md @@ -22,7 +22,7 @@ updated with each milestone. | Selection reading | `LexicalSelection.ts`: `Point.isBefore`, `isBackward` across nodes, `RangeSelection.getNodes` (`$getNodesFromCaretRangeCompat`) and `getTextContent`, `NodeSelection.getNodes` and `getTextContent`, the `_cachedNodes` cache | `selection` | `scripts/caret`; a unit test against JS's output | | Carets | `caret/LexicalCaret.ts`: sibling, child and text point carets in both directions and their methods (`getAdjacentCaret`, `getNodeAtCaret`, `getFlipped`, `getParentCaret`, `getChildCaret`, `insert`, `remove`, `replaceOrInsert`, `splice`, iteration), `$getSiblingCaret`, `$getChildCaret`, `$getTextPointCaret`, `$getTextNodeOffset`, text slices, caret ranges (`$getCaretRange`, `iterNodeCarets`, `getTextSlices`, `isCollapsed`), `$getAdjacentChildCaret`, `$getCaretInDirection`, `$extendCaretToRange`, `$comparePointCaretNext`, `$getCommonAncestor`, `makeStepwiseIterator` | `caret` | `scripts/caret` | | Caret utilities | `caret/LexicalCaretUtils.ts`: `$caretFromPoint`, `$setPointFromCaret`, `$caretRangeFromSelection`, `$setSelectionFromCaretRange`, `$updateRangeSelectionFromCaretRange`, `$rewindSiblingCaret`, `$removeTextFromCaretRange` (block merges, empty-container repair), `$normalizeCaret`, `$isExtendableTextPointCaret`, `$getCaretRangeInDirection`, `$getChildCaretAtIndex`, `$getAdjacentSiblingOrParentSiblingCaret`, `$getTextPointCaretSliceForNode`; with `$getNodeByKeyOrThrow`, `$removeFromParent`, `$restoreEmptyContainerParagraph`, `$isShadowRootNode` and `INTERNAL_$isBlock` from `LexicalUtils.ts` | `caret_utils`, `utils` | `scripts/caret` | -| Selection editing | `LexicalSelection.ts`: `insertText` (`$insertText`, `$insertTextAtPoint`), `insertRawText` (`tokenizeRawText`, `$generateNodesFromRawText`), `insertNodes` for both selection kinds (`$removeTextAndSplitBlock`, `$splitNodeAtPoint`, `$wrapInlineNodes`), `insertParagraph`, `insertLineBreak`, `removeText`, `formatText` (`$updateTextFormat`, `$setTextFormat`, `$getCharacterOffsets`), `toggleFormat`, `setFormat`, `setStyle`, `deleteCharacter`, `deleteWord` and `deleteLine` up to `Selection.modify` (`forwardDeletion`, `$extendSelectionForDeletion`, `$modifySelectionAroundDecoratorsAndBlocks`, `$collapseAtStart`, `$removeSegment`), `$insertNodes`, `$selectAll`; `$splitTextPointCaretSlice` and `$isBlockFullySelected`; `ParagraphNode.insertNewAfter`, `canInsertTextBefore`/`After` | `selection_editing`, `selection_insert_text`, `selection_insert_nodes`, `selection_format`, `selection_deletion` | `scripts/selection`, `fuzz/core`; unit tests against JS's output for paths no vector covers | +| Selection editing | `LexicalSelection.ts`: `insertText` (`$insertText`, `$insertTextAtPoint`), `insertRawText` (`tokenizeRawText`, `$generateNodesFromRawText`), `insertNodes` for both selection kinds (`$removeTextAndSplitBlock`, `$splitNodeAtPoint`, `$wrapInlineNodes`), `insertParagraph`, `insertLineBreak`, `removeText`, `formatText` (`$updateTextFormat`, `$setTextFormat`, `$getCharacterOffsets`), `toggleFormat`, `setFormat`, `setStyle`, `deleteCharacter`, `deleteWord`, `deleteLine` and `modify` (`forwardDeletion`, `$extendSelectionForDeletion`, `$modifySelectionAroundDecoratorsAndBlocks`, `$collapseAtStart`, `$removeSegment`, `$updateCaretSelectionForUnicodeCharacter`, `$stopLineDeletionAtLineBreak`, `$shrinkSelectionToRoot`, and the model side of `applyDOMRange`), with an editor's `SelectionMovement` standing in for `Selection.modify`, `$insertNodes`, `$selectAll`; `$splitTextPointCaretSlice` and `$isBlockFullySelected`; `ParagraphNode.insertNewAfter`, `canInsertTextBefore`/`After` | `selection_editing`, `selection_insert_text`, `selection_insert_nodes`, `selection_format`, `selection_deletion` | `scripts/selection`, `fuzz/core`; unit tests against JS's output for paths no vector covers | | Update cycle | `LexicalUpdates.ts` (`$beginUpdate`, nested and queued updates, `$applyAllTransforms`, `$commitPendingUpdates`, `$discardPendingUpdate`, listeners, `triggerCommandListeners`, `updateEditorSync`), `LexicalNormalization.ts` (`$normalizeTextNode`), `LexicalGC.ts` (nodes) | `updates`, `normalization`, `gc` | `scripts/updates`, `scripts/listeners`, `scripts/transforms`, `scripts/commands` | | Editor state | `LexicalEditorState.ts` (`toJSON`, `read`, `isEmpty`, `_parsed`) | `editor_state` | `serialization/core` | | Parsing | `parseEditorState`, `$parseSerializedNode` (`LexicalUpdates.ts`) | `updates` | `serialization/core`, `property/documents` | @@ -37,11 +37,17 @@ touches: - **NodeState** (`"$"`) and **slots** (`"$slots"`): a document that carries either fails to parse with an error naming it, rather than losing them. -- Deletion past the model: JS's `deleteCharacter`, `deleteWord` and - `deleteLine` extend the selection with the DOM's `Selection.modify`. A - headless JS editor has no window and throws (`window object not found`), - and so does this port; the vectors record that. `RangeSelection.modify`, - `extract`, and `$selectAll(selection)`'s scoping aren't ported, nor +- Deletion and `modify` past the model: JS's `deleteCharacter`, + `deleteWord`, `deleteLine` and `modify` measure moves within text with + the DOM's `Selection.modify`. A headless JS editor has no window and + throws (`window object not found`), and so does this port unless the + editor has a `SelectionMovement` (`Editor::set_selection_movement`), + which an app implements from its text layout; the rest follows JS from + where it lands (a unit test checks that against JS with a stubbed + `Selection.modify`). The vectors record the headless behavior. The DOM + details around the measurement (block cursor element, shadow DOM ranges, + Firefox's line-affinity retry) have no counterpart. `extract`, and + `$selectAll(selection)`'s scoping aren't ported, nor `insertNodes`' CASE 3a (blocks with no block ancestor, which needs `$insertNodeToNearestRootAtCaret`). - The caret utilities that split or insert (`$splitTextPointCaret`, diff --git a/rust/lexical-editor/src/editor.rs b/rust/lexical-editor/src/editor.rs index 3f1128654af..8b827a120ad 100644 --- a/rust/lexical-editor/src/editor.rs +++ b/rust/lexical-editor/src/editor.rs @@ -10,6 +10,7 @@ use crate::nodes::{ text::TextNode, }; use crate::ordered::{OrderedMap, OrderedSet}; +use crate::selection_deletion::SelectionMovement; use crate::updates::{ begin_update, commit_pending_updates, parse_editor_state, read_editor_state, trigger_command_listeners, update_editor_sync, @@ -209,6 +210,8 @@ pub(crate) struct EditorInner { text_content_listeners: Listeners, commands: RefCell>, next_id: Cell, + /// Stands in for the DOM's `Selection.modify` (no JS field). + selection_movement: RefCell>>, } /// `createEditor(config)`: registers the core nodes (root, text, line @@ -241,6 +244,7 @@ pub fn create_editor(args: CreateEditorArgs) -> Editor { text_content_listeners: RefCell::new(Vec::new()), commands: RefCell::new(HashMap::new()), next_id: Cell::new(1), + selection_movement: RefCell::new(None), })) } @@ -261,6 +265,21 @@ impl Editor { Rc::downgrade(&self.0) } + /// Sets what stands in for the DOM's `Selection.modify` in this editor: + /// where a caret lands when `modify`, `deleteCharacter`, `deleteWord` or + /// `deleteLine` move it within text, which only text layout knows. + /// Without one (the default) the editor behaves as JS does with no root + /// element: those moves fail with `window object not found`. + pub fn set_selection_movement(&self, movement: Option>) { + *self.0.selection_movement.borrow_mut() = movement; + } + + /// The movement [`set_selection_movement`](Self::set_selection_movement) + /// set. + pub(crate) fn selection_movement(&self) -> Option> { + self.0.selection_movement.borrow().clone() + } + /// `getEditorState()`: the committed state. pub fn get_editor_state(&self) -> EditorState { self.0.editor_state.borrow().clone() diff --git a/rust/lexical-editor/src/lib.rs b/rust/lexical-editor/src/lib.rs index 7035fd852ea..b165657b804 100644 --- a/rust/lexical-editor/src/lib.rs +++ b/rust/lexical-editor/src/lib.rs @@ -101,6 +101,7 @@ pub use selection::{ NodeSelection, Point, PointType, RangeSelection, Selection, SelectionRef, Which, create_node_selection, create_range_selection, get_selection, set_selection, }; +pub use selection_deletion::{SelectionAlter, SelectionGranularity, SelectionMovement}; pub use selection_insert_nodes::{insert_nodes, select_all}; pub use serialized_export::{export_node_json, is_compact_export, with_compact_export}; pub use updates::parse_serialized_node; diff --git a/rust/lexical-editor/src/selection_deletion.rs b/rust/lexical-editor/src/selection_deletion.rs index 4534a1d2c90..32c84d538a3 100644 --- a/rust/lexical-editor/src/selection_deletion.rs +++ b/rust/lexical-editor/src/selection_deletion.rs @@ -1,16 +1,24 @@ -//! `deleteCharacter`, `deleteWord` and `deleteLine` from -//! `LexicalSelection.ts`, with the parts of `modify` they need headless. +//! `deleteCharacter`, `deleteWord`, `deleteLine` and `modify` from +//! `LexicalSelection.ts`. //! -//! JS measures how far a deletion reaches inside text with the DOM's -//! `Selection.modify` (`$extendSelectionForDeletion`), after -//! `$modifySelectionAroundDecoratorsAndBlocks` has resolved the moves that -//! cross blocks in the model. That measurement starts with +//! JS measures how far a deletion or a caret movement reaches inside text +//! with the DOM's `Selection.modify` (`$extendSelectionForDeletion`, +//! `modify`), after `$modifySelectionAroundDecoratorsAndBlocks` has resolved +//! the moves that cross blocks in the model. That measurement starts with //! `getWindow(editor)`, which throws `window object not found` for an editor //! without a root element, so a headless JS editor fails the update whenever -//! a deletion needs it: within text, at a text node edge with a sibling in -//! the deletion direction, and at the edges of the document. Only the model -//! paths (merging blocks, removing a line break or an empty block, removing -//! an expanded selection) succeed, and that is what this port does. +//! a deletion or `modify` needs it: within text, at a text node edge with a +//! sibling in the deletion direction, and at the edges of the document. +//! Only the model paths (merging blocks, removing a line break or an empty +//! block, removing an expanded selection) succeed. +//! +//! An editor with a [`SelectionMovement`] +//! ([`Editor::set_selection_movement`](crate::Editor::set_selection_movement)) +//! stands in for `Selection.modify`: it says where the caret lands, and the +//! rest follows JS from there. The DOM points JS resolves back into the model +//! (`applyDOMRange`) are model points here already, so only the model side +//! of that resolution is ported. Without one, this port throws where +//! headless JS does. //! //! Decorators, slots and shadow roots other than the root are not ported, //! so the branches for them are no-ops (each is noted where JS has it). @@ -32,18 +40,42 @@ use crate::nodes::element::is_root_or_shadow_root; use crate::nodes::line_break::LineBreakNode; use crate::nodes::paragraph::{ParagraphNode, create_paragraph_node}; use crate::selection::{Point, PointType, RangeSelection, SelectionRef, Which}; +use crate::selection_insert_nodes::normalize_selection; +use crate::updates::get_active_editor; use crate::utf16; use crate::utils::{get_root, is_block, is_shadow_root_node, restore_empty_container_paragraph}; +use std::rc::Rc; -/// The `granularity` of a deletion: `'character'`, `'word'` or -/// `'lineboundary'`. +/// `'character'`, `'word'`, `'line'` or `'lineboundary'`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Granularity { +pub enum SelectionGranularity { Character, Word, + Line, LineBoundary, } +/// `'move'` or `'extend'`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SelectionAlter { + Move, + Extend, +} + +/// Where a caret lands when it moves one unit, which only text layout knows +/// (the DOM's `Selection.modify` in JS). Called inside an update or read, so +/// it can read nodes. +pub trait SelectionMovement { + /// The point one `granularity` unit before (`is_backward`) or after + /// `from`, or `None` when it can't move. + fn landed_point( + &self, + from: &Point, + is_backward: bool, + granularity: SelectionGranularity, + ) -> Result, LexicalError>; +} + /// `deleteCharacter`'s scan for a block to merge with. enum MergeState { /// `{type: 'initial'}`. @@ -199,7 +231,7 @@ impl SelectionRef { } // Handle the deletion around decorators. - extend_selection_for_deletion(self, is_backward, Granularity::Character)?; + extend_selection_for_deletion(self, is_backward, SelectionGranularity::Character)?; let range = self.range().expect("a range selection"); if !range.is_collapsed() { @@ -262,12 +294,12 @@ impl SelectionRef { /// `selection.deleteWord(isBackward)`. pub fn delete_word(&self, is_backward: bool) -> Result<(), LexicalError> { - delete_text_by_granularity(self, is_backward, Granularity::Word) + delete_text_by_granularity(self, is_backward, SelectionGranularity::Word) } /// `selection.deleteLine(isBackward)`. pub fn delete_line(&self, is_backward: bool) -> Result<(), LexicalError> { - delete_text_by_granularity(self, is_backward, Granularity::LineBoundary) + delete_text_by_granularity(self, is_backward, SelectionGranularity::LineBoundary) } } @@ -297,7 +329,7 @@ fn forward_deletion(anchor: &Point, anchor_node: &NodeRef, is_backward: bool) -> fn delete_text_by_granularity( selection: &SelectionRef, is_backward: bool, - granularity: Granularity, + granularity: SelectionGranularity, ) -> Result<(), LexicalError> { let Some(range) = selection.range() else { return Ok(()); @@ -306,19 +338,19 @@ fn delete_text_by_granularity( // Set when the deletion stops at a slot boundary, which it never crosses. let mut stopped_at_slot = false; if was_collapsed { - if granularity == Granularity::Word + if granularity == SelectionGranularity::Word && forward_deletion(&range.anchor, &range.anchor.get_node()?, is_backward) { return Ok(()); } stopped_at_slot = extend_selection_for_deletion(selection, is_backward, granularity)?; - if granularity == Granularity::LineBoundary { + if granularity == SelectionGranularity::LineBoundary { stop_line_deletion_at_line_break(selection)?; } } // Line deletion must remain in one block; word deletion may cross blocks. let range = selection.range().expect("a range selection"); - if granularity == Granularity::LineBoundary + if granularity == SelectionGranularity::LineBoundary && !range.is_collapsed() && find_block(&range.anchor.get_node()?) != find_block(&range.focus.get_node()?) { @@ -365,58 +397,549 @@ fn stop_line_deletion_at_line_break(selection: &SelectionRef) -> Result<(), Lexi Ok(()) } +/// The editor's [`SelectionMovement`], what JS reaches through +/// `getDOMSelection(getWindow(editor))`: without one, `getWindow` throws as +/// it does for an editor with no root element. +fn selection_movement() -> Result, LexicalError> { + let editor = + get_active_editor().ok_or_else(|| LexicalError::new("Unable to find an active editor."))?; + editor + .selection_movement() + .ok_or_else(|| LexicalError::new("window object not found")) +} + +/// Where the native `Selection.modify` leaves the caret it moves from +/// `from`: the movement's landed point, or `from` when it can't move. +fn landed_point( + movement: &dyn SelectionMovement, + from: &Point, + is_backward: bool, + granularity: SelectionGranularity, +) -> Result { + Ok(movement + .landed_point(from, is_backward, granularity)? + .unwrap_or_else(|| from.clone())) +} + +impl SelectionRef { + /// `selection.modify(alter, isBackward, granularity)`: moves (collapsed) + /// or extends the selection by one unit, across decorators and blocks + /// in the model and otherwise where the editor's [`SelectionMovement`] + /// lands. Without a movement this fails with `window object not found` + /// unless the model step handled it, as JS does with no root element. + pub fn modify( + &self, + alter: SelectionAlter, + is_backward: bool, + granularity: SelectionGranularity, + ) -> Result<(), LexicalError> { + let Some(range) = self.range() else { + return Ok(()); + }; + if modify_selection_around_decorators_and_blocks( + self, + alter, + is_backward, + granularity, + false, + )? { + return Ok(()); + } + let collapse = alter == SelectionAlter::Move; + let movement = selection_movement()?; + // Removing the DOM block cursor element: there is no DOM. When the + // selection is dirty JS first syncs the DOM selection to it; the + // movement always measures from the model's focus. + let focus = range.focus.clone(); + let focus_node = focus.get_node()?; + let mut from = focus.clone(); + // When focus sits at a TextNode boundary, pre-normalize the caret + // into the adjacent sibling's text so the movement can cross + // inline-grid/flex spans (#7301). + if granularity == SelectionGranularity::Character + && focus_node.is_text() + && focus_node.is_unmergeable() + { + let at_boundary = if is_backward { + focus.offset == 0 + } else { + focus.offset == focus_node.get_text_content_size() + }; + if at_boundary + && let Some(sibling) = get_sibling_caret(&focus_node, direction(is_backward)) + .get_node_at_caret() + .filter(NodeRef::is_text) + { + let sibling_size = sibling.get_text_content_size(); + if collapse { + let offset = if is_backward { sibling_size } else { 0 }; + from = Point::new(sibling.get_key(), offset, PointType::Text); + } else { + // For extend (used by deleteCharacter), set the selection + // directly instead of measuring. + let offset = if is_backward { + sibling_size.saturating_sub(1) + } else { + 1 + }; + self.point_set(Which::Focus, sibling.get_key(), offset, PointType::Text)?; + self.set_dirty(true); + return Ok(()); + } + } + } + let landed = landed_point(&*movement, &from, is_backward, granularity)?; + // The DOM selection after the native move: collapsed at the landed + // point, or from the anchor to it. Its range is in document order. + let (dom_anchor, dom_focus) = if collapse { + (landed.clone(), landed) + } else { + (range.anchor.clone(), landed) + }; + let dom_is_backward = dom_focus.is_before(&dom_anchor)?; + let (start, end) = if dom_is_backward { + (dom_focus, dom_anchor) + } else { + (dom_anchor, dom_focus) + }; + // Apply the DOM selection to our Lexical selection. + let anchor_node = self.range().expect("a range selection").anchor.get_node()?; + let root = if anchor_node.is_root() { + anchor_node + } else { + get_nearest_root_or_shadow_root(&anchor_node)? + }; + apply_range(self, &start, &end)?; + self.set_dirty(true); + if !collapse { + shrink_selection_to_root(self, is_backward, &root)?; + // Because a range works on start and end, we might need to flip + // the anchor and focus points to match what the DOM has. + if dom_is_backward { + swap_points(self)?; + } + } + if granularity == SelectionGranularity::LineBoundary { + modify_selection_around_decorators_and_blocks( + self, + alter, + is_backward, + granularity, + true, + )?; + } + Ok(()) + } +} + /// `$extendSelectionForDeletion(selection, isBackward, granularity)`: /// extends the collapsed selection over what the deletion removes. Returns /// true when the focus stopped at a slot boundary (never, as slots are not /// ported). /// -/// The moves across blocks are resolved in the model. Every other move is -/// measured with the DOM selection of `getWindow(editor)`, which throws for -/// an editor without a root element (`_window` is only set by -/// `setRootElement`), so this returns that error, as headless JS does. +/// The moves across blocks are resolved in the model; every other move is +/// measured by the editor's [`SelectionMovement`] where JS uses the DOM +/// selection of `getWindow(editor)`, and fails like JS without one. fn extend_selection_for_deletion( selection: &SelectionRef, is_backward: bool, - granularity: Granularity, + granularity: SelectionGranularity, ) -> Result { // Decorator/block handling, resolved in the model exactly as modify() // does. - if modify_selection_around_decorators_and_blocks(selection, is_backward, granularity)? { + if modify_selection_around_decorators_and_blocks( + selection, + SelectionAlter::Extend, + is_backward, + granularity, + false, + )? { return Ok(false); } - Err(LexicalError::new("window object not found")) + let movement = selection_movement()?; + // Removing the DOM block cursor element: there is no DOM. + let range = selection.range().expect("a range selection"); + let anchor = range.anchor.clone(); + let anchor_node = anchor.get_node()?; + // Measure from the focus, which the pre-pass above may have hopped past + // an inline decorator; for an untouched collapsed selection it is the + // anchor. + let was_collapsed = range.is_collapsed(); + let landed = landed_point(&*movement, &range.focus, is_backward, granularity)?; + // JS resolves a line boundary that lands in an inline decorator's + // private text at the decorator's edge; decorators are not ported. + + // The movement may not cross inline-grid/flex span boundaries (#7301). + // When at the deletion-side edge of an unmergeable TextNode, extend into + // the adjacent sibling directly instead of relying on the movement. + if was_collapsed + && granularity == SelectionGranularity::Character + && anchor.point_type == PointType::Text + && anchor_node.is_unmergeable() + { + let boundary_offset = if is_backward { + 0 + } else { + anchor_node.get_text_content_size() + }; + if anchor.offset == boundary_offset + && let Some(sibling) = get_sibling_caret(&anchor_node, direction(is_backward)) + .get_node_at_caret() + .filter(NodeRef::is_text) + { + let sibling_offset = if is_backward { + sibling.get_text_content_size().saturating_sub(1) + } else { + 1 + }; + selection.point_set( + Which::Focus, + sibling.get_key(), + sibling_offset, + PointType::Text, + )?; + selection.set_dirty(true); + return Ok(false); + } + } + // In-node character deletion (the common case): keep the focus in the + // anchor's own text node. Landing inside the anchor node uses that + // offset; landing anywhere else is clamped to the anchor's edge. + if was_collapsed + && granularity == SelectionGranularity::Character + && anchor.point_type == PointType::Text + { + // The deletion-side edge of the anchor's text. + let edge_offset = if is_backward { + 0 + } else { + anchor_node.get_text_content_size() + }; + let clamped_offset = if landed.point_type == PointType::Text && landed.key == anchor.key { + Some(landed.offset) + } else if anchor.offset != edge_offset { + Some(edge_offset) + } else { + None + }; + if let Some(clamped_offset) = clamped_offset { + if clamped_offset != anchor.offset { + selection.point_set(Which::Focus, &anchor.key, clamped_offset, PointType::Text)?; + selection.set_dirty(true); + } + return Ok(false); + } + } + // JS stops the focus at a slot boundary here; slots are not ported. + + // General path: the [original .. landed] range a native extend would + // have produced, in document order. + let (start, end) = if is_backward { + (landed, anchor) + } else { + (anchor, landed) + }; + let root = if anchor_node.is_root() { + anchor_node + } else { + get_nearest_root_or_shadow_root(&anchor_node)? + }; + apply_range(selection, &start, &end)?; + selection.set_dirty(true); + if !shrink_selection_to_root(selection, is_backward, &root)? && is_backward { + // applyDOMRange set anchor = range start (the landed point); the + // deletion anchor must stay at the original caret, so restore that + // orientation. + swap_points(selection)?; + } + Ok(false) } -/// `$modifySelectionAroundDecoratorsAndBlocks(selection, 'extend', -/// isBackward, granularity)`: moves the focus to the adjacent block when -/// the caret is at the edge of its own and returns true; false when the -/// native measurement has to resolve the move. -/// -/// Only the `'extend'` alteration in `'decorators-and-blocks'` mode is -/// ported, the one deletion uses; the `'move'` branches belong to `modify`. -/// Decorators are not ported, so the steps over them are no-ops. +/// `selection.applyDOMRange(range)` for a range whose ends are model points +/// already: resolves them as `$internalResolveSelectionPoints` does +/// (`$internalResolveSelectionPoint` and +/// `$normalizeSelectionPointsForBoundaries`), sets them as the anchor and +/// the focus, and normalizes the selection (`$normalizeSelection`). +fn apply_range(selection: &SelectionRef, start: &Point, end: &Point) -> Result<(), LexicalError> { + let last_selection = get_active_editor() + .and_then(|editor| editor.get_editor_state().selection()) + .and_then(|s| s.range()); + let mut anchor = resolve_selection_point(start, last_selection.as_ref().map(|r| &r.anchor))?; + let mut focus = resolve_selection_point(end, last_selection.as_ref().map(|r| &r.focus))?; + // JS ignores a range whose element points are both inside decorators; + // decorators are not ported. + normalize_selection_points_for_boundaries(&mut anchor, &mut focus)?; + selection.point_set(Which::Anchor, &anchor.key, anchor.offset, anchor.point_type)?; + selection.point_set(Which::Focus, &focus.key, focus.offset, focus.point_type)?; + // Firefox will use an element point rather than a text point in some + // cases, so we normalize for that + normalize_selection(selection) +} + +/// `$internalResolveSelectionPoint(dom, offset, lastPoint)` for the DOM +/// position of a model point: a text point keeps its node (its offset +/// clamped), and an element point resolves to the text at that child, as the +/// DOM resolution of an element's child offset does (an element's DOM +/// children are its children here, with no block cursor). +fn resolve_selection_point( + point: &Point, + last_point: Option<&Point>, +) -> Result { + let node = point.get_node()?; + if point.point_type == PointType::Text { + let offset = point.offset.min(node.get_text_content_size()); + return Ok(Point::new(point.key.clone(), offset, PointType::Text)); + } + let size = node.get_children_size(); + let mut resolved_offset = point.offset; + // If the anchor is the same as length, then this means we need to select + // the very last text node. + let move_selection_to_end = resolved_offset == size && size > 0; + if move_selection_to_end { + resolved_offset = size - 1; + } + let text_point = |text: &NodeRef| { + let offset = if move_selection_to_end { + text.get_text_content_size() + } else { + 0 + }; + Point::new(text.get_key(), offset, PointType::Text) + }; + if let Some(text) = node + .get_child_at_index(resolved_offset) + .filter(NodeRef::is_text) + { + return Ok(text_point(&text)); + } + let mut resolved_element = node; + let mut child = resolved_element.get_child_at_index(resolved_offset); + let element_child = child.clone().filter(|c| c.is_element()); + if let Some(element) = element_child + && should_resolve_ancestor(&element, last_point)? + { + let descendant = if move_selection_to_end { + element.get_last_descendant() + } else { + element.get_first_descendant() + }; + match descendant { + None => resolved_element = element, + Some(descendant) => { + resolved_element = if descendant.is_element() { + descendant.clone() + } else { + descendant.get_parent_or_throw()? + }; + child = Some(descendant); + } + } + resolved_offset = 0; + } + if let Some(text) = child.as_ref().filter(|c| c.is_text()) { + return Ok(text_point(text)); + } + if child.as_ref() != Some(&resolved_element) && move_selection_to_end { + resolved_offset = (resolved_offset + 1).min(resolved_element.get_children_size()); + } + Ok(Point::new( + resolved_element.get_key(), + resolved_offset, + PointType::Element, + )) +} + +/// `shouldResolveAncestor(resolvedElement, resolvedOffset, lastPoint)`. +fn should_resolve_ancestor( + resolved_element: &NodeRef, + last_point: Option<&Point>, +) -> Result { + let Some(last_point) = last_point else { + return Ok(true); + }; + Ok(match resolved_element.get_parent() { + None => true, + Some(parent) => !parent.can_be_empty() || parent != last_point.get_node()?, + }) +} + +/// `$normalizeSelectionPointsForBoundaries(anchor, focus, lastSelection)`. +fn normalize_selection_points_for_boundaries( + anchor: &mut Point, + focus: &mut Point, +) -> Result<(), LexicalError> { + if anchor.point_type == PointType::Text && focus.point_type == PointType::Text { + let is_backward = anchor.is_before(focus)?; + let is_collapsed = anchor == focus; + // Attempt to normalize the offset to the previous sibling if we're + // at the start of a text node and the sibling is a text node or + // inline element. + resolve_selection_point_on_boundary(anchor, is_backward, is_collapsed)?; + resolve_selection_point_on_boundary(focus, !is_backward, is_collapsed)?; + if is_collapsed { + *focus = anchor.clone(); + } + } + Ok(()) +} + +/// `$resolveSelectionPointOnBoundary(point, isBackward, isCollapsed)` (a +/// headless editor never has an empty composition target). +fn resolve_selection_point_on_boundary( + point: &mut Point, + is_backward: bool, + is_collapsed: bool, +) -> Result<(), LexicalError> { + let node = point.get_node()?; + let before = point.offset == 0; + if !before && point.offset != node.get_text_content_size() { + return Ok(()); + } + let direction = if before { + CaretDirection::Previous + } else { + CaretDirection::Next + }; + let sibling = get_sibling_caret(&node, direction).get_node_at_caret(); + if before != is_backward + && let Some(sibling) = sibling.as_ref().filter(|s| s.is_element() && s.is_inline()) + && (!before || !is_collapsed) + { + let offset = if before { + sibling.get_children_size() + } else { + 0 + }; + *point = Point::new(sibling.get_key(), offset, PointType::Element); + } else if before && !is_backward { + if let Some(sibling) = sibling.filter(NodeRef::is_text) + && !node.is_unmergeable() + { + *point = Point::new( + sibling.get_key(), + sibling.get_text_content_size(), + PointType::Text, + ); + } + } else if sibling.is_none() + && (is_collapsed || (!before && is_backward)) + && let Some(parent) = node.get_parent() + && parent.is_element() + && parent.is_inline() + && (before + || (!parent.get_latest().can_insert_text_after() && parent.get_text_content_size() > 1)) + && let Some(parent_sibling) = get_sibling_caret(&parent, direction) + .get_node_at_caret() + .filter(NodeRef::is_text) + { + let offset = if before { + parent_sibling.get_text_content_size() + } else { + 0 + }; + *point = Point::new(parent_sibling.get_key(), offset, PointType::Text); + } + Ok(()) +} + +/// `$shrinkSelectionToRoot(selection, isBackward, root)`: when part of the +/// selection lies outside `root`, shrinks it to the edge of the part inside. +/// Returns true if the selection was shrunk. +fn shrink_selection_to_root( + selection: &SelectionRef, + is_backward: bool, + root: &NodeRef, +) -> Result { + let nodes = selection.get_nodes()?; + let valid_nodes: Vec<&NodeRef> = nodes.iter().filter(|n| is_parent_of(root, n)).collect(); + if valid_nodes.is_empty() || valid_nodes.len() == nodes.len() { + return Ok(false); + } + let edge_node = if is_backward { + valid_nodes[0] + } else { + valid_nodes[valid_nodes.len() - 1] + }; + let edge_element = if edge_node.is_element() { + edge_node.clone() + } else { + edge_node.get_parent_or_throw()? + }; + if is_backward { + edge_element.select_start()?; + } else { + edge_element.select_end()?; + } + Ok(true) +} + +/// `$swapPoints(selection)`. +fn swap_points(selection: &SelectionRef) -> Result<(), LexicalError> { + let range = selection.range().expect("a range selection"); + let (anchor, focus) = (range.anchor, range.focus); + selection.point_set(Which::Anchor, &focus.key, focus.offset, focus.point_type)?; + selection.point_set(Which::Focus, &anchor.key, anchor.offset, anchor.point_type) +} + +/// `$getNearestRootOrShadowRoot(node)` (slots are not ported). +fn get_nearest_root_or_shadow_root(node: &NodeRef) -> Result { + let mut current = node.clone(); + loop { + let parent = current.get_parent_or_throw()?; + if is_root_or_shadow_root(&*parent.get_latest()) { + return Ok(parent); + } + current = parent; + } +} + +/// `$modifySelectionAroundDecoratorsAndBlocks(selection, alter, isBackward, +/// granularity, mode)`: resolves in the model a move or extension that +/// leaves the caret's block (`decorators_only` is the `'decorators'` mode, +/// which doesn't look for blocks), and returns true when that handled it; +/// false when the native measurement has to resolve the move. Decorators +/// are not ported, so the steps over them are no-ops. fn modify_selection_around_decorators_and_blocks( selection: &SelectionRef, + alter: SelectionAlter, is_backward: bool, - granularity: Granularity, + granularity: SelectionGranularity, + decorators_only: bool, ) -> Result { let range = selection.range().expect("a range selection"); + if alter == SelectionAlter::Move + && granularity == SelectionGranularity::Character + && !range.is_collapsed() + { + // moving left or right when the selection isn't collapsed will just + // set the anchor to the focus or vice versa depending on direction + let (src, dst) = if is_backward == selection.is_backward()? { + (&range.focus, Which::Anchor) + } else { + (&range.anchor, Which::Focus) + }; + selection.point_set(dst, &src.key, src.offset, src.point_type)?; + return Ok(true); + } let initial_focus = caret_from_point(&range.focus, direction(is_backward))?; - let is_line_boundary = granularity == Granularity::LineBoundary; + let is_line_boundary = granularity == SelectionGranularity::LineBoundary; + let collapse = alter == SelectionAlter::Move; // `focus === initialFocus` in JS: whether a new caret was chosen. let mut focus: Option = None; - let mut check_for_block = true; + let mut check_for_block = !decorators_only; if !is_extendable_text_point_caret(&initial_focus) { if initial_focus.iter().next().is_some() { - // The first sibling in the deletion direction ends the scan; JS - // steps over it (or stops at it when isolated) only when it is a + // The first sibling in the direction ends the scan; JS steps + // over it (or stops at it when isolated) only when it is a // decorator. check_for_block = false; } if check_for_block { - for next_caret in - extend_caret_to_range(&initial_focus)?.iter_node_carets(RootMode::ShadowRoot) - { + let root_mode = match alter { + SelectionAlter::Extend => RootMode::ShadowRoot, + SelectionAlter::Move => RootMode::Root, + }; + for next_caret in extend_caret_to_range(&initial_focus)?.iter_node_carets(root_mode) { if next_caret.is_child_caret() { if !next_caret.origin().is_inline() { focus = Some(next_caret); @@ -435,7 +958,13 @@ fn modify_selection_around_decorators_and_blocks( }; // After this point checkForBlock is true if and only if we moved to a // different block, so we should stop regardless of the granularity. - set_point_from_caret(selection, Which::Focus, &normalize_caret(&focus))?; + // (Moving onto a keyboard-selectable decorator selects it; decorators + // are not ported.) + let focus = normalize_caret(&focus); + if collapse { + set_point_from_caret(selection, Which::Anchor, &focus)?; + } + set_point_from_caret(selection, Which::Focus, &focus)?; Ok(check_for_block || !is_line_boundary) } @@ -1889,4 +2418,526 @@ mod tests { } } } + + /// A [`SelectionMovement`] that never leaves a text node: a character is + /// a code point with the combining marks (U+0300 to U+036F) and variation + /// selector 16 after it, a word is the spaces and then the non-spaces in + /// the direction, and a line boundary is the text node's edge. The JS + /// expectations below come from stubbing `Selection.prototype.modify` + /// with the same rules in jsdom. + struct TestMovement; + + impl SelectionMovement for TestMovement { + fn landed_point( + &self, + from: &Point, + is_backward: bool, + granularity: SelectionGranularity, + ) -> Result, LexicalError> { + if from.point_type != PointType::Text { + return Ok(None); + } + let text: Vec = from.get_node()?.get_text_content().encode_utf16().collect(); + let len = text.len(); + let is_mark = |c: u16| (0x300..=0x36f).contains(&c) || c == 0xfe0f; + let is_high = |c: u16| (0xd800..=0xdbff).contains(&c); + let is_low = |c: u16| (0xdc00..=0xdfff).contains(&c); + let is_space = |c: u16| c == u16::from(b' '); + let mut o = from.offset; + match (granularity, is_backward) { + (SelectionGranularity::Character, true) => { + while o > 0 && is_mark(text[o - 1]) { + o -= 1; + } + if o > 0 { + o -= 1; + if o > 0 && is_low(text[o]) && is_high(text[o - 1]) { + o -= 1; + } + } + } + (SelectionGranularity::Character, false) => { + if o < len { + o += if is_high(text[o]) && o + 1 < len && is_low(text[o + 1]) { + 2 + } else { + 1 + }; + while o < len && is_mark(text[o]) { + o += 1; + } + } + } + (SelectionGranularity::Word, true) => { + while o > 0 && is_space(text[o - 1]) { + o -= 1; + } + while o > 0 && !is_space(text[o - 1]) { + o -= 1; + } + } + (SelectionGranularity::Word, false) => { + while o < len && is_space(text[o]) { + o += 1; + } + while o < len && !is_space(text[o]) { + o += 1; + } + } + (_, true) => o = 0, + (_, false) => o = len, + } + Ok((o != from.offset).then(|| Point::new(from.key.clone(), o, PointType::Text))) + } + } + + /// `key:offset:t|e` as a point. + fn parse_point(spec: &str) -> Point { + let mut parts = spec.split(':'); + let key = parts.next().unwrap().to_owned(); + let offset = parts.next().unwrap().parse().unwrap(); + let point_type = match parts.next().unwrap() { + "t" => PointType::Text, + _ => PointType::Element, + }; + Point::new(key, offset, point_type) + } + + fn show_point(point: &Point) -> String { + let t = match point.point_type { + PointType::Text => "t", + PointType::Element => "e", + }; + format!("{}:{}:{t}", point.key, point.offset) + } + + /// `deleteCharacter`, `deleteWord`, `deleteLine` (`C`, `W`, `L`, `<` + /// backward and `>` forward) and `modify` (`move` or `extend`, the + /// direction, then `c`haracter, `w`ord or `l`ineboundary) with + /// [`TestMovement`], as JS gives them for an editor with a root element in + /// jsdom and `Selection.prototype.modify` stubbed to move the same way. + /// Each line: the operation, the document, anchor and focus | the anchor, + /// focus and format after it | the root's children after it (`=` when + /// unchanged). + #[test] + fn deletion_and_modify_with_a_movement_match_js() { + let docs = [ + r#"[{"type":"paragraph","children":[{"type":"text","text":"hello big world"}]}]"#, + r#"[{"type":"paragraph","children":[{"type":"text","text":"a😀b"}]}]"#, + r#"[{"type":"paragraph","children":[{"type":"text","text":"aéb"}]}]"#, + r#"[{"type":"paragraph","children":[{"type":"text","text":"a❤️b"}]}]"#, + r#"[{"type":"paragraph","children":[{"type":"text","format":1,"text":"ab"},{"type":"text","text":"cd"}],"textFormat":1}]"#, + r#"[{"type":"paragraph","children":[{"type":"text","detail":2,"text":"ab"},{"type":"text","detail":2,"text":"cd"}]}]"#, + r#"[{"type":"paragraph","children":[{"type":"text","text":"ab cd"},{"type":"linebreak"},{"type":"text","text":"ef gh"}]}]"#, + r#"[{"type":"paragraph","children":[{"type":"text","text":"ab cd"}]},{"type":"paragraph","children":[{"type":"text","text":"ef gh"}]}]"#, + r#"[{"type":"paragraph","children":[{"type":"text","text":"x "},{"type":"text","mode":"segmented","text":"foo bar baz"},{"type":"text","text":" y"}]}]"#, + r#"[{"type":"paragraph","children":[{"type":"text","text":"x "},{"type":"text","mode":"token","text":"tok"},{"type":"text","text":" y"}]}]"#, + r#"[{"type":"paragraph","children":[{"type":"text","text":"ab"}]},{"type":"paragraph","children":[]}]"#, + ]; + let cases = r#" +C< 0 2:7:t 2:7:t | 2:6:t 2:6:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hello ig world"}]}] +W< 0 2:7:t 2:7:t | 2:6:t 2:6:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hello ig world"}]}] +L< 0 2:7:t 2:7:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ig world"}]}] +C> 0 2:7:t 2:7:t | 2:7:t 2:7:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hello bg world"}]}] +W> 0 2:7:t 2:7:t | 2:7:t 2:7:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hello b world"}]}] +L> 0 2:7:t 2:7:t | 2:7:t 2:7:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hello b"}]}] +movec 0 2:7:t 2:7:t | 2:8:t 2:8:t 0 | = +move>w 0 2:7:t 2:7:t | 2:9:t 2:9:t 0 | = +move>l 0 2:7:t 2:7:t | 2:15:t 2:15:t 0 | = +extendc 0 2:7:t 2:7:t | 2:7:t 2:8:t 0 | = +extend>w 0 2:7:t 2:7:t | 2:7:t 2:9:t 0 | = +extend>l 0 2:7:t 2:7:t | 2:7:t 2:15:t 0 | = +C< 0 2:6:t 2:6:t | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hellobig world"}]}] +W< 0 2:6:t 2:6:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"big world"}]}] +L< 0 2:6:t 2:6:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"big world"}]}] +C> 0 2:6:t 2:6:t | 2:6:t 2:6:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hello ig world"}]}] +W> 0 2:6:t 2:6:t | 2:6:t 2:6:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hello world"}]}] +L> 0 2:6:t 2:6:t | 2:6:t 2:6:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hello "}]}] +C< 0 2:5:t 2:5:t | 2:4:t 2:4:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hell big world"}]}] +W< 0 2:5:t 2:5:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":" big world"}]}] +L< 0 2:5:t 2:5:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":" big world"}]}] +C> 0 2:5:t 2:5:t | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hellobig world"}]}] +W> 0 2:5:t 2:5:t | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hello world"}]}] +L> 0 2:5:t 2:5:t | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hello"}]}] +C< 0 2:0:t 2:0:t | 2:0:t 2:0:t 0 | = +W< 0 2:0:t 2:0:t | 2:0:t 2:0:t 0 | = +L< 0 2:0:t 2:0:t | 2:0:t 2:0:t 0 | = +C> 0 2:0:t 2:0:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ello big world"}]}] +W> 0 2:0:t 2:0:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":" big world"}]}] +L> 0 2:0:t 2:0:t | 1:0:e 1:0:e 0 | [{"type":"paragraph","children":[]}] +movec 0 2:0:t 2:0:t | 2:1:t 2:1:t 0 | = +move>w 0 2:0:t 2:0:t | 2:5:t 2:5:t 0 | = +move>l 0 2:0:t 2:0:t | 2:15:t 2:15:t 0 | = +extendc 0 2:0:t 2:0:t | 2:0:t 2:1:t 0 | = +extend>w 0 2:0:t 2:0:t | 2:0:t 2:5:t 0 | = +extend>l 0 2:0:t 2:0:t | 2:0:t 2:15:t 0 | = +C< 0 2:15:t 2:15:t | 2:14:t 2:14:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hello big worl"}]}] +W< 0 2:15:t 2:15:t | 2:10:t 2:10:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"hello big "}]}] +L< 0 2:15:t 2:15:t | 1:0:e 1:0:e 0 | [{"type":"paragraph","children":[]}] +C> 0 2:15:t 2:15:t | 2:15:t 2:15:t 0 | = +W> 0 2:15:t 2:15:t | 2:15:t 2:15:t 0 | = +L> 0 2:15:t 2:15:t | 2:15:t 2:15:t 0 | = +movec 0 2:15:t 2:15:t | 2:15:t 2:15:t 0 | = +move>w 0 2:15:t 2:15:t | 2:15:t 2:15:t 0 | = +move>l 0 2:15:t 2:15:t | 2:15:t 2:15:t 0 | = +extendc 0 2:15:t 2:15:t | 2:15:t 2:15:t 0 | = +extend>w 0 2:15:t 2:15:t | 2:15:t 2:15:t 0 | = +extend>l 0 2:15:t 2:15:t | 2:15:t 2:15:t 0 | = +movec 0 2:2:t 2:8:t | 2:8:t 2:8:t 0 | = +move>w 0 2:2:t 2:8:t | 2:9:t 2:9:t 0 | = +move>l 0 2:2:t 2:8:t | 2:15:t 2:15:t 0 | = +extendc 0 2:2:t 2:8:t | 2:2:t 2:9:t 0 | = +extend>w 0 2:2:t 2:8:t | 2:2:t 2:9:t 0 | = +extend>l 0 2:2:t 2:8:t | 2:2:t 2:15:t 0 | = +movec 0 2:8:t 2:2:t | 2:8:t 2:8:t 0 | = +move>w 0 2:8:t 2:2:t | 2:5:t 2:5:t 0 | = +move>l 0 2:8:t 2:2:t | 2:15:t 2:15:t 0 | = +extendc 0 2:8:t 2:2:t | 2:8:t 2:3:t 0 | = +extend>w 0 2:8:t 2:2:t | 2:8:t 2:5:t 0 | = +extend>l 0 2:8:t 2:2:t | 2:8:t 2:15:t 0 | = +C< 1 2:3:t 2:3:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab"}]}] +W< 1 2:3:t 2:3:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"b"}]}] +L< 1 2:3:t 2:3:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"b"}]}] +C> 1 2:1:t 2:1:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab"}]}] +W> 1 2:1:t 2:1:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"a"}]}] +L> 1 2:1:t 2:1:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"a"}]}] +C< 2 2:3:t 2:3:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"aeb"}]}] +W< 2 2:3:t 2:3:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"b"}]}] +L< 2 2:3:t 2:3:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"b"}]}] +C> 2 2:1:t 2:1:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"aeb"}]}] +W> 2 2:1:t 2:1:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"a"}]}] +L> 2 2:1:t 2:1:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"a"}]}] +C< 3 2:3:t 2:3:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab"}]}] +W< 3 2:3:t 2:3:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"b"}]}] +L< 3 2:3:t 2:3:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"b"}]}] +C> 3 2:1:t 2:1:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab"}]}] +W> 3 2:1:t 2:1:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"a"}]}] +L> 3 2:1:t 2:1:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"a"}]}] +C< 4 2:2:t 2:2:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","format":1,"text":"a"},{"type":"text","text":"cd"}],"textFormat":1}] +W< 4 2:2:t 2:2:t | 3:0:t 3:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"cd"}]}] +L< 4 2:2:t 2:2:t | 3:0:t 3:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"cd"}]}] +C> 4 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +W> 4 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +L> 4 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +movec 4 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +move>w 4 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +move>l 4 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +extendc 4 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +extend>w 4 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +extend>l 4 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +C< 4 3:0:t 3:0:t | 3:0:t 3:0:t 0 | = +W< 4 3:0:t 3:0:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","format":1,"text":"a"},{"type":"text","text":"cd"}],"textFormat":1}] +L< 4 3:0:t 3:0:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","format":1,"text":"a"},{"type":"text","text":"cd"}],"textFormat":1}] +C> 4 3:0:t 3:0:t | 3:0:t 3:0:t 0 | [{"type":"paragraph","children":[{"type":"text","format":1,"text":"ab"},{"type":"text","text":"d"}],"textFormat":1}] +W> 4 3:0:t 3:0:t | 2:2:t 2:2:t 1 | [{"type":"paragraph","children":[{"type":"text","format":1,"text":"ab"}],"textFormat":1}] +L> 4 3:0:t 3:0:t | 2:2:t 2:2:t 1 | [{"type":"paragraph","children":[{"type":"text","format":1,"text":"ab"}],"textFormat":1}] +movec 4 3:0:t 3:0:t | 3:1:t 3:1:t 0 | = +move>w 4 3:0:t 3:0:t | 3:2:t 3:2:t 0 | = +move>l 4 3:0:t 3:0:t | 3:2:t 3:2:t 0 | = +extendc 4 3:0:t 3:0:t | 3:0:t 3:1:t 0 | = +extend>w 4 3:0:t 3:0:t | 3:0:t 3:2:t 0 | = +extend>l 4 3:0:t 3:0:t | 3:0:t 3:2:t 0 | = +C< 5 2:2:t 2:2:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","detail":2,"text":"a"},{"type":"text","detail":2,"text":"cd"}]}] +W< 5 2:2:t 2:2:t | 3:0:t 3:0:t 0 | [{"type":"paragraph","children":[{"type":"text","detail":2,"text":"cd"}]}] +L< 5 2:2:t 2:2:t | 3:0:t 3:0:t 0 | [{"type":"paragraph","children":[{"type":"text","detail":2,"text":"cd"}]}] +C> 5 2:2:t 2:2:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","detail":2,"text":"ab"},{"type":"text","detail":2,"text":"d"}]}] +W> 5 2:2:t 2:2:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","detail":2,"text":"ab"},{"type":"text","detail":2,"text":"d"}]}] +L> 5 2:2:t 2:2:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","detail":2,"text":"ab"},{"type":"text","detail":2,"text":"d"}]}] +movec 5 2:2:t 2:2:t | 3:1:t 3:1:t 0 | = +move>w 5 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +move>l 5 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +extendc 5 2:2:t 2:2:t | 2:2:t 3:1:t 0 | = +extend>w 5 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +extend>l 5 2:2:t 2:2:t | 2:2:t 2:2:t 0 | = +C< 5 3:0:t 3:0:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","detail":2,"text":"a"},{"type":"text","detail":2,"text":"cd"}]}] +W< 5 3:0:t 3:0:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","detail":2,"text":"a"},{"type":"text","detail":2,"text":"cd"}]}] +L< 5 3:0:t 3:0:t | 2:1:t 2:1:t 0 | [{"type":"paragraph","children":[{"type":"text","detail":2,"text":"a"},{"type":"text","detail":2,"text":"cd"}]}] +C> 5 3:0:t 3:0:t | 3:0:t 3:0:t 0 | [{"type":"paragraph","children":[{"type":"text","detail":2,"text":"ab"},{"type":"text","detail":2,"text":"d"}]}] +W> 5 3:0:t 3:0:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","detail":2,"text":"ab"}]}] +L> 5 3:0:t 3:0:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","detail":2,"text":"ab"}]}] +movec 5 3:0:t 3:0:t | 3:1:t 3:1:t 0 | = +move>w 5 3:0:t 3:0:t | 3:2:t 3:2:t 0 | = +move>l 5 3:0:t 3:0:t | 3:2:t 3:2:t 0 | = +extendc 5 3:0:t 3:0:t | 3:0:t 3:1:t 0 | = +extend>w 5 3:0:t 3:0:t | 3:0:t 3:2:t 0 | = +extend>l 5 3:0:t 3:0:t | 3:0:t 3:2:t 0 | = +C< 6 4:3:t 4:3:t | 4:2:t 4:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"},{"type":"linebreak"},{"type":"text","text":"efgh"}]}] +W< 6 4:3:t 4:3:t | 4:0:t 4:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"},{"type":"linebreak"},{"type":"text","text":"gh"}]}] +L< 6 4:3:t 4:3:t | 4:0:t 4:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"},{"type":"linebreak"},{"type":"text","text":"gh"}]}] +C> 6 4:3:t 4:3:t | 4:3:t 4:3:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"},{"type":"linebreak"},{"type":"text","text":"ef h"}]}] +W> 6 4:3:t 4:3:t | 4:3:t 4:3:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"},{"type":"linebreak"},{"type":"text","text":"ef "}]}] +L> 6 4:3:t 4:3:t | 4:3:t 4:3:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"},{"type":"linebreak"},{"type":"text","text":"ef "}]}] +C< 6 2:3:t 2:3:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"abcd"},{"type":"linebreak"},{"type":"text","text":"ef gh"}]}] +W< 6 2:3:t 2:3:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"cd"},{"type":"linebreak"},{"type":"text","text":"ef gh"}]}] +L< 6 2:3:t 2:3:t | 2:0:t 2:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"cd"},{"type":"linebreak"},{"type":"text","text":"ef gh"}]}] +C> 6 2:3:t 2:3:t | 2:3:t 2:3:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab d"},{"type":"linebreak"},{"type":"text","text":"ef gh"}]}] +W> 6 2:3:t 2:3:t | 2:3:t 2:3:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab "},{"type":"linebreak"},{"type":"text","text":"ef gh"}]}] +L> 6 2:3:t 2:3:t | 2:3:t 2:3:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab "},{"type":"linebreak"},{"type":"text","text":"ef gh"}]}] +C< 6 1:2:e 1:2:e | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cdef gh"}]}] +W< 6 1:2:e 1:2:e | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cdef gh"}]}] +L< 6 1:2:e 1:2:e | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cdef gh"}]}] +C> 6 1:2:e 1:2:e | 4:0:t 4:0:t 0 | = +W> 6 1:2:e 1:2:e | 4:0:t 4:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"},{"type":"linebreak"},{"type":"text","text":"f gh"}]}] +L> 6 1:2:e 1:2:e | 4:0:t 4:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"},{"type":"linebreak"},{"type":"text","text":"f gh"}]}] +movec 6 1:2:e 1:2:e | 4:0:t 4:0:t 0 | = +move>w 6 1:2:e 1:2:e | 4:0:t 4:0:t 0 | = +move>l 6 1:2:e 1:2:e | 4:0:t 4:0:t 0 | = +extendc 6 1:2:e 1:2:e | 4:0:t 4:0:t 0 | = +extend>w 6 1:2:e 1:2:e | 4:0:t 4:0:t 0 | = +extend>l 6 1:2:e 1:2:e | 4:0:t 4:0:t 0 | = +C< 7 4:0:t 4:0:t | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cdef gh"}]}] +W< 7 4:0:t 4:0:t | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cdef gh"}]}] +L< 7 4:0:t 4:0:t | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cdef gh"}]}] +C> 7 4:0:t 4:0:t | 4:0:t 4:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"}]},{"type":"paragraph","children":[{"type":"text","text":"f gh"}]}] +W> 7 4:0:t 4:0:t | 4:0:t 4:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"}]},{"type":"paragraph","children":[{"type":"text","text":" gh"}]}] +L> 7 4:0:t 4:0:t | 3:0:e 3:0:e 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"}]},{"type":"paragraph","children":[]}] +movec 7 4:0:t 4:0:t | 4:1:t 4:1:t 0 | = +move>w 7 4:0:t 4:0:t | 4:2:t 4:2:t 0 | = +move>l 7 4:0:t 4:0:t | 4:5:t 4:5:t 0 | = +extendc 7 4:0:t 4:0:t | 4:0:t 4:1:t 0 | = +extend>w 7 4:0:t 4:0:t | 4:0:t 4:2:t 0 | = +extend>l 7 4:0:t 4:0:t | 4:0:t 4:5:t 0 | = +C< 7 2:5:t 2:5:t | 2:4:t 2:4:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab c"}]},{"type":"paragraph","children":[{"type":"text","text":"ef gh"}]}] +W< 7 2:5:t 2:5:t | 2:3:t 2:3:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab "}]},{"type":"paragraph","children":[{"type":"text","text":"ef gh"}]}] +L< 7 2:5:t 2:5:t | 1:0:e 1:0:e 0 | [{"type":"paragraph","children":[]},{"type":"paragraph","children":[{"type":"text","text":"ef gh"}]}] +C> 7 2:5:t 2:5:t | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cdef gh"}]}] +W> 7 2:5:t 2:5:t | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cdef gh"}]}] +L> 7 2:5:t 2:5:t | 2:5:t 2:5:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cdef gh"}]}] +movec 7 2:5:t 2:5:t | 4:0:t 4:0:t 0 | = +move>w 7 2:5:t 2:5:t | 4:0:t 4:0:t 0 | = +move>l 7 2:5:t 2:5:t | 4:0:t 4:0:t 0 | = +extendc 7 2:5:t 2:5:t | 2:5:t 4:0:t 0 | = +extend>w 7 2:5:t 2:5:t | 2:5:t 4:0:t 0 | = +extend>l 7 2:5:t 2:5:t | 2:5:t 4:0:t 0 | = +C< 7 4:4:t 4:4:t | 4:3:t 4:3:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"}]},{"type":"paragraph","children":[{"type":"text","text":"ef h"}]}] +W< 7 4:4:t 4:4:t | 4:3:t 4:3:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"}]},{"type":"paragraph","children":[{"type":"text","text":"ef h"}]}] +L< 7 4:4:t 4:4:t | 4:0:t 4:0:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"}]},{"type":"paragraph","children":[{"type":"text","text":"h"}]}] +C> 7 4:4:t 4:4:t | 4:4:t 4:4:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"}]},{"type":"paragraph","children":[{"type":"text","text":"ef g"}]}] +W> 7 4:4:t 4:4:t | 4:4:t 4:4:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"}]},{"type":"paragraph","children":[{"type":"text","text":"ef g"}]}] +L> 7 4:4:t 4:4:t | 4:4:t 4:4:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab cd"}]},{"type":"paragraph","children":[{"type":"text","text":"ef g"}]}] +C< 8 3:5:t 3:5:t | 3:3:t 3:3:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x "},{"type":"text","mode":"segmented","text":"foo baz"},{"type":"text","text":" y"}]}] +W< 8 3:5:t 3:5:t | 2:6:t 2:6:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x foo ar baz y"}]}] +L< 8 3:5:t 3:5:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x ar baz y"}]}] +C> 8 3:5:t 3:5:t | 3:3:t 3:3:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x "},{"type":"text","mode":"segmented","text":"foo baz"},{"type":"text","text":" y"}]}] +W> 8 3:5:t 3:5:t | 2:7:t 2:7:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x foo b baz y"}]}] +L> 8 3:5:t 3:5:t | 2:7:t 2:7:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x foo b y"}]}] +C< 8 3:11:t 3:11:t | 3:7:t 3:7:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x "},{"type":"text","mode":"segmented","text":"foo bar"},{"type":"text","text":" y"}]}] +W< 8 3:11:t 3:11:t | 2:10:t 2:10:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x foo bar y"}]}] +L< 8 3:11:t 3:11:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x y"}]}] +C< 8 4:0:t 4:0:t | 4:0:t 4:0:t 0 | = +W< 8 4:0:t 4:0:t | 3:7:t 3:7:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x "},{"type":"text","mode":"segmented","text":"foo bar"},{"type":"text","text":" y"}]}] +L< 8 4:0:t 4:0:t | 3:7:t 3:7:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x "},{"type":"text","mode":"segmented","text":"foo bar"},{"type":"text","text":" y"}]}] +C< 9 3:3:t 3:3:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x y"}]}] +W< 9 3:3:t 3:3:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x y"}]}] +L< 9 3:3:t 3:3:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x y"}]}] +C> 9 3:1:t 3:1:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x y"}]}] +W> 9 3:1:t 3:1:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x y"}]}] +L> 9 3:1:t 3:1:t | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"x y"}]}] +C< 10 3:0:e 3:0:e | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab"}]}] +W< 10 3:0:e 3:0:e | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab"}]}] +L< 10 3:0:e 3:0:e | 2:2:t 2:2:t 0 | [{"type":"paragraph","children":[{"type":"text","text":"ab"}]}] +C> 10 3:0:e 3:0:e | 3:0:e 3:0:e 0 | = +W> 10 3:0:e 3:0:e | 3:0:e 3:0:e 0 | = +L> 10 3:0:e 3:0:e | 3:0:e 3:0:e 0 | = +movec 10 3:0:e 3:0:e | 3:0:e 3:0:e 0 | = +move>w 10 3:0:e 3:0:e | 3:0:e 3:0:e 0 | = +move>l 10 3:0:e 3:0:e | 3:0:e 3:0:e 0 | = +extendc 10 3:0:e 3:0:e | 3:0:e 3:0:e 0 | = +extend>w 10 3:0:e 3:0:e | 3:0:e 3:0:e 0 | = +extend>l 10 3:0:e 3:0:e | 3:0:e 3:0:e 0 | = +"#; + let discrete = || UpdateOptions { + discrete: true, + ..UpdateOptions::default() + }; + for line in cases.lines().filter(|l| !l.is_empty()) { + let mut parts = line.splitn(3, " | "); + let given: Vec<&str> = parts.next().unwrap().split(' ').collect(); + let want: Vec<&str> = parts.next().unwrap().split(' ').collect(); + let want_children = parts.next().unwrap(); + let (op, doc, anchor, focus) = ( + given[0].to_owned(), + given[1].parse::().unwrap(), + parse_point(given[2]), + parse_point(given[3]), + ); + reset_random_key(); + let editor = create_editor(CreateEditorArgs::default()); + editor.set_selection_movement(Some(Rc::new(TestMovement))); + let children = json::parse(docs[doc]).unwrap(); + editor + .update( + move || { + let nodes: Vec = children + .as_array() + .unwrap() + .iter() + .map(|n| parse_serialized_node(n).unwrap()) + .collect(); + get_root().append(&nodes) + }, + discrete(), + ) + .unwrap(); + let before = json::stringify(&editor.get_editor_state().to_json(true)).unwrap(); + editor + .update( + move || { + let selection = create_range_selection(); + selection.point_set( + Which::Anchor, + &anchor.key, + anchor.offset, + anchor.point_type, + )?; + selection.point_set( + Which::Focus, + &focus.key, + focus.offset, + focus.point_type, + )?; + set_selection(Some(selection.clone())); + let is_backward = op.contains('<'); + let granularity = match op.chars().last() { + Some('w') => SelectionGranularity::Word, + Some('l') => SelectionGranularity::LineBoundary, + _ => SelectionGranularity::Character, + }; + match &op[..1] { + "C" => selection.delete_character(is_backward), + "W" => selection.delete_word(is_backward), + "L" => selection.delete_line(is_backward), + "m" => selection.modify(SelectionAlter::Move, is_backward, granularity), + _ => selection.modify(SelectionAlter::Extend, is_backward, granularity), + } + }, + discrete(), + ) + .unwrap_or_else(|e| panic!("{line}: {e}")); + let state = editor.get_editor_state(); + let compact = json::stringify(&state.to_json(true)).unwrap(); + let want_compact = if want_children == "=" { + before + } else { + format!(r#"{{"root":{{"type":"root","children":{want_children}}}}}"#) + }; + assert_eq!(compact, want_compact, "{line}"); + let Some(Selection::Range(range)) = state.selection().map(|s| s.get()) else { + panic!("{line}: no range selection"); + }; + assert_eq!( + [ + show_point(&range.anchor), + show_point(&range.focus), + range.format.to_string() + ], + [want[0], want[1], want[2]], + "{line}" + ); + } + } + + /// Without a movement, `modify` inside text fails as it does in JS on an + /// editor with no root element. + #[test] + fn headless_modify_matches_js() { + reset_random_key(); + let editor = create_editor(CreateEditorArgs::default()); + let result = editor.update( + || { + let paragraph = create_paragraph_node(); + let text = crate::create_text_node("hello big world"); + paragraph.append(std::slice::from_ref(&text))?; + get_root().append(&[paragraph])?; + let selection = text.select(Some(7), Some(7))?; + selection.modify(SelectionAlter::Move, true, SelectionGranularity::Character) + }, + UpdateOptions { + discrete: true, + ..UpdateOptions::default() + }, + ); + assert_eq!( + result.map_err(|e| e.message().to_owned()), + Err("window object not found".to_owned()) + ); + } } diff --git a/rust/lexical-editor/src/selection_insert_nodes.rs b/rust/lexical-editor/src/selection_insert_nodes.rs index 5a5b3785ec5..43f57e1ed54 100644 --- a/rust/lexical-editor/src/selection_insert_nodes.rs +++ b/rust/lexical-editor/src/selection_insert_nodes.rs @@ -750,7 +750,7 @@ fn get_root_child_ancestor(node: &NodeRef) -> Option { /// `$normalizeSelection(selection)` (`LexicalNormalization.ts`): moves /// element points down to the deepest text or element point. -fn normalize_selection(selection: &SelectionRef) -> Result<(), LexicalError> { +pub(crate) fn normalize_selection(selection: &SelectionRef) -> Result<(), LexicalError> { normalize_point(selection, Which::Anchor)?; normalize_point(selection, Which::Focus) } From c4b3a9ee9c142b0942b405d1272e53ff7749b349 Mon Sep 17 00:00:00 2001 From: Amp Date: Sat, 14 Nov 2026 00:48:25 +0000 Subject: [PATCH 03/10] feat(rust): desktop playground on GPUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- .github/workflows/rust-playground.yml | 72 +- rust/DESIGN.md | 34 +- rust/apps/lexical-playground-core/src/lib.rs | 63 +- .../lexical-playground-core/src/surface.rs | 363 + .../apps/lexical-playground-core/src/tests.rs | 87 + .../.cargo/config.toml | 4 + rust/apps/lexical-playground-gpui/.gitignore | 1 + rust/apps/lexical-playground-gpui/Cargo.lock | 8445 +++++++++++++++++ rust/apps/lexical-playground-gpui/Cargo.toml | 28 + rust/apps/lexical-playground-gpui/README.md | 75 + rust/apps/lexical-playground-gpui/bundle.sh | 38 + .../rust-toolchain.toml | 5 + rust/apps/lexical-playground-gpui/src/app.rs | 292 + rust/apps/lexical-playground-gpui/src/e2e.rs | 166 + .../lexical-playground-gpui/src/editor.rs | 691 ++ rust/apps/lexical-playground-gpui/src/main.rs | 62 + rust/apps/lexical-playground-rune/README.md | 2 +- rust/apps/lexical-playground-rune/src/main.rs | 4 + rust/apps/lexical-playground-rune/src/view.rs | 21 +- 19 files changed, 10435 insertions(+), 18 deletions(-) create mode 100644 rust/apps/lexical-playground-core/src/surface.rs create mode 100644 rust/apps/lexical-playground-gpui/.cargo/config.toml create mode 100644 rust/apps/lexical-playground-gpui/.gitignore create mode 100644 rust/apps/lexical-playground-gpui/Cargo.lock create mode 100644 rust/apps/lexical-playground-gpui/Cargo.toml create mode 100644 rust/apps/lexical-playground-gpui/README.md create mode 100755 rust/apps/lexical-playground-gpui/bundle.sh create mode 100644 rust/apps/lexical-playground-gpui/rust-toolchain.toml create mode 100644 rust/apps/lexical-playground-gpui/src/app.rs create mode 100644 rust/apps/lexical-playground-gpui/src/e2e.rs create mode 100644 rust/apps/lexical-playground-gpui/src/editor.rs create mode 100644 rust/apps/lexical-playground-gpui/src/main.rs diff --git a/.github/workflows/rust-playground.yml b/.github/workflows/rust-playground.yml index 4b8303c9262..95e71c23b45 100644 --- a/.github/workflows/rust-playground.yml +++ b/.github/workflows/rust-playground.yml @@ -4,7 +4,8 @@ name: Rust playgrounds # workspace of its own (Rune needs a newer Rust than lexical-editor's 1.88). # Path-filtered because macOS runners are slow to start. Linux checks that # the Rune app builds, passes its tests, and renders headless; macOS builds -# and renders it as it runs on tmm's Mac. +# and renders it as it runs on tmm's Mac. The GPUI app is macOS only here: it +# runs an end-to-end session in a real window, and JS replays the session. on: push: @@ -69,11 +70,18 @@ jobs: run: | cargo run --locked -- --snap "$RUNNER_TEMP/lexical-playground-rune-linux.png" --size 1100x720 --scene typed test -s "$RUNNER_TEMP/lexical-playground-rune-linux.png" + # The Parity panel after Replay vectors, run in the app. + - name: Render the parity panel + run: | + cargo run --locked -- --snap "$RUNNER_TEMP/lexical-playground-rune-parity.png" --size 1100x720 --scene parity + test -s "$RUNNER_TEMP/lexical-playground-rune-parity.png" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: always() with: name: lexical-playground-rune-linux-snapshot - path: ${{ runner.temp }}/lexical-playground-rune-linux.png + path: | + ${{ runner.temp }}/lexical-playground-rune-linux.png + ${{ runner.temp }}/lexical-playground-rune-parity.png if-no-files-found: ignore rune-macos: @@ -112,3 +120,63 @@ jobs: name: lexical-playground-rune-macos-snapshot path: ${{ runner.temp }}/lexical-playground-rune-macos.png if-no-files-found: ignore + + gpui-macos: + name: GPUI playground (macOS) + runs-on: macos-26 + timeout-minutes: 45 + defaults: + run: + working-directory: rust/apps/lexical-playground-gpui + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Install the toolchain + run: rustup show active-toolchain || rustup toolchain install + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/apps/lexical-playground-gpui/target + key: lexical-playground-gpui-${{ runner.os }}-${{ hashFiles('rust/apps/lexical-playground-gpui/Cargo.lock', 'rust/apps/lexical-playground-gpui/rust-toolchain.toml') }} + restore-keys: lexical-playground-gpui-${{ runner.os }}- + - name: Format + run: cargo fmt --check + - name: Lint + run: | + cargo clippy --all-targets --locked -- -D warnings + cargo clippy --all-targets --locked --features e2e -- -D warnings + - name: Test + run: cargo test --locked + # The end-to-end run (src/e2e.rs): the app types, composes and deletes + # in its own window through the input handler and key bindings, and + # exports the session... + - name: Run end to end + run: | + cargo build --locked --features e2e + LEXICAL_PLAYGROUND_E2E="$RUNNER_TEMP/gpui-session.json" \ + LEXICAL_PLAYGROUND_E2E_SHOT="$RUNNER_TEMP/lexical-playground-gpui-e2e.png" \ + ./target/debug/lexical-playground-gpui + # ...which JS Lexical replays, comparing every step with its own. + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + version: 11.24.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + package-manager-cache: false + - run: pnpm install --frozen-lockfile + working-directory: . + - name: Replay the session in JS + working-directory: packages/lexical-conformance + run: LEXICAL_REPLAY="$RUNNER_TEMP/gpui-session.json" pnpm exec vitest run src/replay.test.ts + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: always() + with: + name: lexical-playground-gpui-e2e + path: | + ${{ runner.temp }}/lexical-playground-gpui-e2e.png + ${{ runner.temp }}/gpui-session.json + if-no-files-found: ignore diff --git a/rust/DESIGN.md b/rust/DESIGN.md index e68ef167eda..bd2451ffdfd 100644 --- a/rust/DESIGN.md +++ b/rust/DESIGN.md @@ -24,6 +24,9 @@ pull request. | --------------------- | --------------------- | --------- | --------------------------------------------------------- | | `rust/lexical-editor` | `lexical-editor` | not yet | The port: core, and one feature per upstream package | | `rust/conformance` | `lexical-conformance` | never | The conformance runner: replays every vector file | +| `rust/apps/lexical-playground-core` | `lexical-playground-core` | never | The playgrounds' binding to `lexical-editor` | +| `rust/apps/lexical-playground-rune` | `lexical-playground-rune` | never | The playground on Rune (a workspace of its own) | +| `rust/apps/lexical-playground-gpui` | `lexical-playground-gpui` | never | The playground on GPUI (a workspace of its own) | `rust/` is developed in wevm/lexical, with its own CI (`.github/workflows/rust.yml`), so it can later be mirrored to @@ -170,8 +173,8 @@ Each milestone ends with green CI and a report to the Space. | Milestone | Scope | Vectors that pass when done | Status | | --- | --- | --- | --- | | M0 | The `rust/` workspace, JSON as JS writes it, the core nodes (root, paragraph, text, tab, line break), `parseEditorState` and `toJSON` in both forms, the conformance runner, CI | `meta`, `serialization/core`, the core-only cases of `property/documents` (JSON and text) | done | -| M1 | The core model: `editor.update` and the commit (dirty nodes, normalization, transforms, listeners, commands, tags), node creation and methods (`splitText`, `mergeWithSibling`, `spliceText`, `insertAfter`, `remove`, `replace`...) with UTF-16 offsets, `RangeSelection` and `NodeSelection` editing, carets, NodeState, slots, the schema facility, `DecoratorNode`, the extension API (`buildEditorFromExtensions`), the parity ledger | `scripts/*`, `fuzz/core`, `file/*` | in progress: the update cycle, `setEditorState`, commands, transforms, listeners, the node methods, carets, selection reading and `RangeSelection`/`NodeSelection` editing are done (`scripts/*` and `fuzz/core` pass); next, the platform's `Selection.modify` as an editor hook, NodeState, `DecoratorNode`, the extension API | -| P | Desktop playgrounds (`apps/`): `lexical-playground-rune` on Rune, then `lexical-playground-gpui` on GPUI, sharing `lexical-playground-core`; they grow with each milestone below | the vectors, replayed in the app; a session exported from the app replays in JS (`replay.test.ts`) | in progress | +| M1 | The core model: `editor.update` and the commit (dirty nodes, normalization, transforms, listeners, commands, tags), node creation and methods (`splitText`, `mergeWithSibling`, `spliceText`, `insertAfter`, `remove`, `replace`...) with UTF-16 offsets, `RangeSelection` and `NodeSelection` editing, carets, NodeState, slots, the schema facility, `DecoratorNode`, the extension API (`buildEditorFromExtensions`), the parity ledger | `scripts/*`, `fuzz/core`, `file/*` | in progress: the update cycle, `setEditorState`, commands, transforms, listeners, the node methods, carets, selection reading and `RangeSelection`/`NodeSelection` editing are done (`scripts/*` and `fuzz/core` pass), and the platform's `Selection.modify` is an editor hook (`SelectionMovement`); next, NodeState, `DecoratorNode`, the extension API | +| P | Desktop playgrounds (`apps/`): `lexical-playground-rune` on Rune, then `lexical-playground-gpui` on GPUI, sharing `lexical-playground-core`; they grow with each milestone below | the vectors, replayed in the app; a session exported from the app replays in JS (`replay.test.ts`); in CI, a GPUI session typed in a real window replays in JS | both apps run on macOS with typing, Return, deletion, formatting, input methods and the Parity panel; they grow with each milestone | | M2 | `rich-text` (headings, quotes, and the command handlers a text surface dispatches: typing, Enter, Backspace, formatting) and `history` (undo), which the playgrounds need first; then the node packages `list`, `link`, `code`, `overflow`, `hashtag`, with `selection` and `utils` | `serialization/*`, `property/documents` (JSON and text), `entities/hashtag-*` | | | M3 | The commands `property/sequences` dispatches | `property/sequences` | | | M4 | `html`: a DOM abstraction over an HTML parser, CSS declarations, export and import | `html/*`, `css/*`, `property/documents` (HTML) | | @@ -192,17 +195,32 @@ app is its own Cargo workspace under `apps/` (Rune needs Rust 1.90 or later; the crate stays on 1.88), and both depend on `apps/lexical-playground-core`, which is in this workspace: -- Lexical owns the document and the selection; the framework's text field - draws a mirror of them, lays it out, hit-tests and moves the caret. Keys - that edit run as Lexical updates before the field sees them, and a change - the field makes itself (an input method's commit, a cut) is replayed into - Lexical as the text it replaced; then the mirror is rebuilt, as Lexical's - reconciler rebuilds the DOM. This is the Swift port's split with TextKit. +- Lexical owns the document and the selection; the app draws them, lays + them out, hit-tests and moves the caret, as a browser does for + `contenteditable` and TextKit does for the Swift port. + - **Rune:** its `text_input` draws a mirror of the document. Keys that + edit run as Lexical updates before the field sees them, and a change + the field makes itself (an input method's commit, a cut) is replayed + into Lexical as the text it replaced; then the mirror is rebuilt, as + Lexical's reconciler rebuilds the DOM. + - **GPUI:** an element of its own draws the document, and is the window's + text input client (`EntityInputHandler`) through `Surface` in the core + crate, which maps UTF-16 ranges to Lexical points and keeps an input + method's composition outside Lexical until it commits, as a browser + does. Keys are GPUI actions bound as macOS binds them. + - Deleting by character, word or line extends the selection in the core + crate (graphemes, words, the paragraph) before the step runs, so the + step is what JS runs once the browser's `Selection.modify` has moved. + `SelectionMovement` lets an app answer that from its own layout instead. - Every edit is a step of the conformance script language, run by the same interpreter that replays `scripts/*`. Export session writes the steps and their results as a vector file that JS replays and compares (`packages/lexical-conformance/src/replay.test.ts`); Replay vectors runs the conformance report in the app. +- The GPUI app has an end-to-end mode for CI (`--features e2e`), as + zbugs-gpui does: it types, composes and deletes in its own window through + the input handler and key bindings, then exports the session, which JS + replays in the same job. - To try one: `cargo run --release` in its directory. On macOS a `bundle.sh` runner makes a `.app` that stays in the Dock, as zbugs' do. diff --git a/rust/apps/lexical-playground-core/src/lib.rs b/rust/apps/lexical-playground-core/src/lib.rs index 639a98bc319..e0924ead136 100644 --- a/rust/apps/lexical-playground-core/src/lib.rs +++ b/rust/apps/lexical-playground-core/src/lib.rs @@ -16,12 +16,18 @@ //! `scripts/*` vector case that JS Lexical replays //! (`packages/lexical-conformance/src/replay.test.ts`): parity for a //! session typed by hand. +//! - [`Surface`]: the text input client for frameworks that hand the app +//! raw text input and input-method compositions (GPUI), over a +//! [`Playground`]. //! - [`replay_vectors`]: the conformance report, run in the app. use lexical::json::{self, Object, Value}; use lexical::{NodeRef, get_root}; use lexical_conformance::scripts::Session; +mod surface; +pub use surface::{DisplayRun, Surface, byte_of_utf16, utf16_of_byte}; + /// A run of a block: a text node, a tab, or a line break. #[derive(Clone, Debug, PartialEq)] pub struct Run { @@ -899,11 +905,62 @@ impl Playground { } /// Replays the conformance vectors (`packages/lexical-conformance/vectors`, -/// or `$LEXICAL_CONFORMANCE_VECTORS`) and returns the report as Markdown, -/// and whether nothing failed. +/// or `$LEXICAL_CONFORMANCE_VECTORS`) and returns a plain-text summary sized +/// for an app's side panel, and whether nothing failed. pub fn replay_vectors() -> Result<(String, bool), String> { + use lexical_conformance::Status; + use std::collections::BTreeMap; + use std::fmt::Write; let report = lexical_conformance::run(None)?; - Ok((report.markdown(), report.ok())) + let replayed: Vec<_> = report + .files + .iter() + .filter(|f| f.status != Status::NotImplemented) + .collect(); + let sum = |n: fn(&lexical_conformance::FileReport) -> usize| -> usize { + replayed.iter().map(|f| n(f)).sum() + }; + let waiting = |f: &lexical_conformance::FileReport| f.not_ported.values().sum::(); + let mut out = format!( + "{} cases · {} match JS · {} wait on a package · {} fail\n\n", + sum(|f| f.cases), + sum(|f| f.passed + f.exceptions.values().sum::()), + sum(waiting), + sum(|f| f.failed), + ); + let width = replayed.iter().map(|f| f.file.len()).max().unwrap_or(0); + for f in &replayed { + let checked = f.cases - waiting(f); + let _ = write!(out, "{:width$} {}/{}", f.file, checked - f.failed, checked); + if let Status::Unreadable(e) = &f.status { + let _ = write!(out, " unreadable: {e}"); + } + if waiting(f) > 0 { + let _ = write!(out, " (+{} waiting)", waiting(f)); + } + out.push('\n'); + } + let mut by_package: BTreeMap<&str, usize> = BTreeMap::new(); + for f in &replayed { + for (package, n) in &f.not_ported { + *by_package.entry(package).or_default() += n; + } + } + if !by_package.is_empty() { + let list: Vec = by_package.iter().map(|(p, n)| format!("{p} {n}")).collect(); + let _ = write!(out, "\nWaiting on: {}\n", list.join(", ")); + } + let _ = writeln!( + out, + "Not replayed yet: {} vector files", + report.files.len() - replayed.len() + ); + for f in &replayed { + for message in &f.failures { + let _ = write!(out, "\n{}: {message}\n", f.file); + } + } + Ok((out, report.ok())) } /// The document the playgrounds open with. diff --git a/rust/apps/lexical-playground-core/src/surface.rs b/rust/apps/lexical-playground-core/src/surface.rs new file mode 100644 index 00000000000..016b5e70ced --- /dev/null +++ b/rust/apps/lexical-playground-core/src/surface.rs @@ -0,0 +1,363 @@ +//! An editor surface for UI frameworks that hand the app raw text input, +//! as GPUI does with macOS's `NSTextInputClient` (`EntityInputHandler`): +//! the app draws the text, and the framework asks it for ranges in UTF-16 +//! code units, replaces text, and marks an input method's composition. +//! +//! Offsets are bytes in [`Surface::display_text`]: the document's text +//! ([`Document::text`]) with the composition, which Lexical doesn't see +//! until the input method commits it, in place of what it replaces. + +use crate::{Outcome, Playground, Sel, Unit}; +use std::ops::Range; + +/// An input method's composition: `text` shown in place of `replaced` +/// (bytes of the document's text), with its own selection. +#[derive(Clone, Debug, PartialEq, Eq)] +struct Composition { + replaced: Range, + text: String, + selected: Range, +} + +/// A run of [`Surface::display_text`] to draw: a block's run with its +/// format, the newline between lines, or the composition. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DisplayRun { + pub range: Range, + pub format: u32, + pub composing: bool, +} + +pub struct Surface { + pub playground: Playground, + /// The selection in the document's text, `anchor` where it started. + anchor: usize, + focus: usize, + composition: Option, +} + +impl Surface { + pub fn new(playground: Playground) -> Surface { + let mut surface = Surface { + playground, + anchor: 0, + focus: 0, + composition: None, + }; + surface.sync(); + surface + } + + /// The text to draw. + pub fn display_text(&self) -> String { + let text = self.playground.document().text(); + match &self.composition { + None => text, + Some(c) => format!( + "{}{}{}", + &text[..c.replaced.start], + c.text, + &text[c.replaced.end..] + ), + } + } + + /// The selection in the display text, in document order, and whether + /// the focus is at its start. + pub fn selection(&self) -> (Range, bool) { + if let Some(c) = &self.composition { + let start = c.replaced.start; + return (start + c.selected.start..start + c.selected.end, false); + } + let (a, f) = (self.anchor, self.focus); + (a.min(f)..a.max(f), f < a) + } + + /// Where the caret is drawn: the selection's focus. + pub fn caret(&self) -> usize { + let (range, reversed) = self.selection(); + if reversed { range.start } else { range.end } + } + + /// The composition's range in the display text. + pub fn marked_range(&self) -> Option> { + let c = self.composition.as_ref()?; + Some(c.replaced.start..c.replaced.start + c.text.len()) + } + + /// The display text in runs to draw: each text run's format, the + /// newlines between lines, and the composition, in order. + pub fn runs(&self) -> Vec { + let mut runs = Vec::new(); + let mut offset = 0; + let blocks = &self.playground.document().blocks; + for (i, block) in blocks.iter().enumerate() { + for run in &block.runs { + runs.push(DisplayRun { + range: offset + run.start..offset + run.end(), + format: run.format, + composing: false, + }); + } + offset += block.text.len(); + if i + 1 < blocks.len() { + runs.push(DisplayRun { + range: offset..offset + 1, + format: 0, + composing: false, + }); + offset += 1; + } + } + let Some(c) = &self.composition else { + return runs; + }; + // Cut the composition's text in where it replaces the document's. + let delta = c.text.len() as isize - c.replaced.len() as isize; + let shift = |x: usize| (x as isize + delta) as usize; + let mut out = Vec::new(); + for run in runs { + let before = run.range.start..run.range.end.min(c.replaced.start); + let after = run.range.start.max(c.replaced.end)..run.range.end; + if before.start < before.end { + out.push(DisplayRun { + range: before, + ..run.clone() + }); + } + if run.range.end >= c.replaced.start + && !out.iter().any(|r: &DisplayRun| r.composing) + && run.range.start <= c.replaced.start + { + out.push(DisplayRun { + range: c.replaced.start..c.replaced.start + c.text.len(), + format: run.format, + composing: true, + }); + } + if after.start < after.end { + out.push(DisplayRun { + range: shift(after.start)..shift(after.end), + ..run + }); + } + } + if !out.iter().any(|r| r.composing) { + out.push(DisplayRun { + range: c.replaced.start..c.replaced.start + c.text.len(), + format: 0, + composing: true, + }); + } + out.retain(|r| r.range.start < r.range.end); + out + } + + /// Takes the selection from the editor, after an edit. + fn sync(&mut self) { + let doc = self.playground.document(); + match self.playground.selection() { + Some(sel) => { + self.anchor = doc.offset_of(sel.anchor); + self.focus = doc.offset_of(sel.focus); + } + None => { + let end = doc.text().len(); + self.anchor = self.anchor.min(end); + self.focus = self.focus.min(end); + } + } + } + + /// Runs an edit at the selection. + pub fn edit(&mut self, f: impl FnOnce(&mut Playground) -> Outcome) -> Outcome { + self.commit_composition(); + self.push_selection(); + let outcome = f(&mut self.playground); + self.sync(); + outcome + } + + fn push_selection(&mut self) { + let doc = self.playground.document(); + let sel = Sel { + anchor: doc.pos_at(self.anchor), + focus: doc.pos_at(self.focus), + }; + self.playground.set_selection(Some(sel)); + } + + /// Moves the caret to `offset` (in the display text, which is the + /// document's text outside a composition), or with `extend` the focus. + pub fn move_to(&mut self, offset: usize, extend: bool) { + self.commit_composition(); + let text = self.playground.document().text(); + let offset = floor_char_boundary(&text, offset); + self.focus = offset; + if !extend { + self.anchor = offset; + } + self.push_selection(); + } + + pub fn select_all(&mut self) { + self.commit_composition(); + self.anchor = 0; + self.focus = self.playground.document().text().len(); + self.push_selection(); + } + + /// The selected text. + pub fn selected_text(&self) -> String { + let (range, _) = self.selection(); + self.display_text()[range].to_owned() + } + + /// Backspace or Delete, by `unit`. + pub fn delete(&mut self, unit: Unit, backward: bool) -> Outcome { + self.edit(|p| p.delete(unit, backward)) + } + + // The text input client, in UTF-16 code units of the display text. + + pub fn text_length_utf16(&self) -> usize { + self.display_text().encode_utf16().count() + } + + /// `text_for_range`: the text, and the range it really covers. + pub fn text_for_range_utf16(&self, range: Range) -> (String, Range) { + let text = self.display_text(); + let bytes = from_utf16_range(&text, &range); + let actual = to_utf16_range(&text, &bytes); + (text[bytes].to_owned(), actual) + } + + /// `selected_text_range`: the selection and whether it's reversed. + pub fn selected_range_utf16(&self) -> (Range, bool) { + let (range, reversed) = self.selection(); + (to_utf16_range(&self.display_text(), &range), reversed) + } + + pub fn set_selected_range_utf16(&mut self, range: Range) { + if self.composition.is_some() { + let text = self.display_text(); + let bytes = from_utf16_range(&text, &range); + if let Some(c) = &mut self.composition { + let start = c.replaced.start; + let end = start + c.text.len(); + if bytes.start >= start && bytes.end <= end { + c.selected = bytes.start - start..bytes.end - start; + return; + } + } + } + let text = self.playground.document().text(); + let bytes = from_utf16_range(&text, &range); + self.move_to(bytes.start, false); + self.move_to(bytes.end, true); + } + + pub fn marked_range_utf16(&self) -> Option> { + let range = self.marked_range()?; + Some(to_utf16_range(&self.display_text(), &range)) + } + + /// `unmark_text`: the input method keeps its composition as it is. + pub fn unmark(&mut self) -> Option { + self.commit_composition() + } + + /// `replace_text_in_range`: typed or committed text. `range` (else the + /// composition, else the selection) is replaced by `text` in Lexical: + /// `insertText`, `insertRawText` for several lines, `removeText` for + /// none. + pub fn replace_text_utf16(&mut self, range: Option>, text: &str) -> Outcome { + let replaced = match (self.composition.take(), range) { + (Some(c), _) => c.replaced, + (None, Some(range)) => from_utf16_range(&self.playground.document().text(), &range), + (None, None) => self.selection().0, + }; + self.anchor = replaced.start; + self.focus = replaced.end; + self.edit(|p| { + if text.is_empty() { + p.remove_text() + } else if text.contains('\n') { + p.insert_raw_text(text) + } else { + p.insert_text(text) + } + }) + } + + /// `replace_and_mark_text_in_range`: an input method's composition. + /// Lexical doesn't see it until it's committed; an empty one ends it. + pub fn replace_and_mark_utf16( + &mut self, + range: Option>, + text: &str, + selected: Option>, + ) { + let replaced = match (&self.composition, range) { + (Some(c), _) => c.replaced.clone(), + (None, Some(range)) => from_utf16_range(&self.playground.document().text(), &range), + (None, None) => self.selection().0, + }; + if text.is_empty() { + self.composition = None; + self.anchor = replaced.start; + self.focus = replaced.start; + return; + } + let selected = selected.map_or(text.len()..text.len(), |r| from_utf16_range(text, &r)); + self.composition = Some(Composition { + replaced, + text: text.to_owned(), + selected, + }); + } + + fn commit_composition(&mut self) -> Option { + let c = self.composition.take()?; + self.anchor = c.replaced.start; + self.focus = c.replaced.end; + self.push_selection(); + let outcome = self.playground.insert_text(&c.text); + self.sync(); + Some(outcome) + } +} + +fn floor_char_boundary(s: &str, byte: usize) -> usize { + let mut byte = byte.min(s.len()); + while !s.is_char_boundary(byte) { + byte -= 1; + } + byte +} + +/// The byte offset of the UTF-16 offset `units` (clamped, rounded down to +/// a character). +pub fn byte_of_utf16(s: &str, units: usize) -> usize { + let mut seen = 0; + for (i, c) in s.char_indices() { + if seen + c.len_utf16() > units { + return i; + } + seen += c.len_utf16(); + } + s.len() +} + +/// The UTF-16 offset of the byte offset `byte`. +pub fn utf16_of_byte(s: &str, byte: usize) -> usize { + s[..floor_char_boundary(s, byte)].encode_utf16().count() +} + +fn from_utf16_range(s: &str, range: &Range) -> Range { + byte_of_utf16(s, range.start)..byte_of_utf16(s, range.end.max(range.start)) +} + +fn to_utf16_range(s: &str, range: &Range) -> Range { + utf16_of_byte(s, range.start)..utf16_of_byte(s, range.end) +} diff --git a/rust/apps/lexical-playground-core/src/tests.rs b/rust/apps/lexical-playground-core/src/tests.rs index 2487af8bc90..f0b4d2da7ba 100644 --- a/rust/apps/lexical-playground-core/src/tests.rs +++ b/rust/apps/lexical-playground-core/src/tests.rs @@ -136,3 +136,90 @@ fn deleting_past_the_documents_ends_does_nothing() { assert_eq!(p.delete_word(false), Outcome::default()); assert_eq!(p.step_count(), 1, "no step ran"); } + +fn surface() -> Surface { + Surface::new( + Playground::new(vec![ + json::parse(r#"{"type":"paragraph","children":[{"type":"text","text":"a😀b"}]}"#) + .unwrap(), + json::parse( + r#"{"type":"paragraph","children":[{"type":"text","text":"bold","format":1}]}"#, + ) + .unwrap(), + ]) + .unwrap(), + ) +} + +#[test] +fn the_surface_takes_typing_and_ranges_in_utf16() { + let mut s = surface(); + assert_eq!(s.display_text(), "a😀b\nbold"); + assert_eq!(s.text_length_utf16(), 9); + // After the emoji: UTF-16 offset 3, byte 5. + s.set_selected_range_utf16(3..3); + assert_eq!(s.selection(), (5..5, false)); + assert_eq!(s.replace_text_utf16(None, "x").error, None); + assert_eq!(s.display_text(), "a😀xb\nbold"); + assert_eq!(s.caret(), 6); + // Replacing a given range: the emoji. + assert_eq!(s.replace_text_utf16(Some(1..3), "é").error, None); + assert_eq!(s.display_text(), "aéxb\nbold"); + assert_eq!(s.text_for_range_utf16(0..2), ("aé".to_owned(), 0..2)); + // A backward selection. + s.move_to(4, false); + s.move_to(1, true); + assert_eq!(s.selection(), (1..4, true)); + assert_eq!(s.selected_text(), "éx"); +} + +#[test] +fn a_composition_shows_without_reaching_lexical_until_committed() { + let mut s = surface(); + s.move_to(1, false); + s.replace_and_mark_utf16(None, "か", Some(1..1)); + assert_eq!(s.display_text(), "aか😀b\nbold"); + assert_eq!(s.marked_range(), Some(1..4)); + assert_eq!(s.selection(), (4..4, false)); + assert_eq!( + s.playground.document().text(), + "a😀b\nbold", + "Lexical hasn't seen it" + ); + let runs = s.runs(); + assert!(runs.iter().any(|r| r.composing && r.range == (1..4))); + let covered: usize = runs.iter().map(|r| r.range.len()).sum(); + assert_eq!(covered, s.display_text().len(), "the runs cover the text"); + // The input method replaces its composition, then commits. + s.replace_and_mark_utf16(None, "かな", None); + assert_eq!(s.display_text(), "aかな😀b\nbold"); + assert_eq!(s.replace_text_utf16(None, "仮名").error, None); + assert_eq!(s.display_text(), "a仮名😀b\nbold"); + assert_eq!(s.marked_range(), None); + assert_eq!(s.caret(), "a仮名".len()); + // An empty composition ends it without an edit. + let steps = s.playground.step_count(); + s.replace_and_mark_utf16(None, "x", None); + s.replace_and_mark_utf16(None, "", None); + assert_eq!(s.display_text(), "a仮名😀b\nbold"); + assert_eq!(s.playground.step_count(), steps); +} + +#[test] +fn the_surface_draws_formats_and_edits_through_lexical() { + let mut s = surface(); + let bold = s + .runs() + .into_iter() + .find(|r| r.format == format_flag("bold")); + assert_eq!(bold.map(|r| r.range), Some(7..11)); + s.move_to(s.display_text().len(), false); + assert_eq!(s.edit(Playground::insert_paragraph).error, None); + assert_eq!(s.replace_text_utf16(None, "new").error, None); + assert_eq!(s.display_text(), "a😀b\nbold\nnew"); + assert_eq!(s.delete(Unit::Character, true).error, None); + assert_eq!(s.display_text(), "a😀b\nbold\nne"); + s.select_all(); + assert_eq!(s.edit(|p| p.format_text("italic")).error, None); + assert!(s.playground.has_format("italic")); +} diff --git a/rust/apps/lexical-playground-gpui/.cargo/config.toml b/rust/apps/lexical-playground-gpui/.cargo/config.toml new file mode 100644 index 00000000000..395ed6c813a --- /dev/null +++ b/rust/apps/lexical-playground-gpui/.cargo/config.toml @@ -0,0 +1,4 @@ +# `cargo run` on macOS runs the app from inside an app bundle (bundle.sh), +# rather than as a bare executable, as wevm/zero's zbugs-gpui does. +[target.'cfg(target_os = "macos")'] +runner = "./bundle.sh" diff --git a/rust/apps/lexical-playground-gpui/.gitignore b/rust/apps/lexical-playground-gpui/.gitignore new file mode 100644 index 00000000000..b83d22266ac --- /dev/null +++ b/rust/apps/lexical-playground-gpui/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/rust/apps/lexical-playground-gpui/Cargo.lock b/rust/apps/lexical-playground-gpui/Cargo.lock new file mode 100644 index 00000000000..14b857ed2e8 --- /dev/null +++ b/rust/apps/lexical-playground-gpui/Cargo.lock @@ -0,0 +1,8445 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "enumn", + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023da0e5097f46df7092d5280b02efb9bbf8d93298daeced42652463e357d636" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "phf 0.13.1", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e156ed3802e35eefe894ef2671bc6c889303d8a7e110b5e1b48f504b91362f" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106c2b961215864d1c2e703ee63269c25c4e80a577ffb2c1017b9c17dcdf83a1" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "static_assertions", + "windows 0.62.2", + "windows-core 0.62.2", +] + +[[package]] +name = "addr2line" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e567177890eb1617b1f774005b66b26b2377afd138a2ca37aae7d8f0c81429d4" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a5b0a5ab8561c603f16c3b78c24a386b1f70e7fc1b1b1e8c15738828e4a176" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" + +[[package]] +name = "arc-swap" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b5d51209f6de83d37a0b33d320acb8da88823e42a94c075140708f032a5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ashpd" +version = "0.13.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "getrandom 0.4.3", + "serde", + "serde_repr", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f426a31126c735126297bc5ea7a521263c9faff4f63ca415267ee625651c4f37" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0876700b0d007ad25d5508865a4259c3af9af6d296bf663a0d586d0bc3369f6f" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.21", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "backtrace" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f4220217f3288537b83684130f42fad43320b1c3b805f1910d683d52ded24f" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide 0.9.1", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base62" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a647dce9631f152a99ec16cfc09267ae55fe280d5dc288964027d458076323" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bit_field" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb2907cc03d08cdb1c6724ad0670ce67c4a71e1c45a6d7635224011714cfc4b1" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.3", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.5", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" +dependencies = [ + "bitflags 2.13.2", + "polling", + "rustix", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", + "tempfile", + "toml 0.8.23", +] + +[[package]] +name = "cc" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ff2a8126fff0f8c839080d619675a585e61720bb12d2baceae45769fd66f155" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be95d4a884358a21a199689323c29e9256b4a1b315e22f183d155422041d3a9a" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-helmer-fork" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cosmic-text" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" +dependencies = [ + "bitflags 2.13.2", + "fontdb", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 2.1.3", + "self_cell", + "skrifa 0.40.0", + "smol_str", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b929dd2a135771f40afabd1073f85eed59c800b7c61901e5c254270497dad64" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dwrote" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "embed-resource" +version = "3.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.2.1+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "encoding_rs" +version = "0.8.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89253175e704ab869b04f9db224d38630e0211a53841a79181deae0889e169f5" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb29d0be702adf92c645d9b2f93472906ba83d400e59f98adb3770ce5c18c2f6" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.9", + "multiversion", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "flate2" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626d4928326b646e70f65ed36fc7cf9f206bdb68a0019ee7c6b4709da4e084c4" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.9", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1033caf0b349c518623b5396bfb2cf0bddf44f0306d543a250e5743297aafd10" + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "globset" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98b0d85522dc0a824cfebbc9594ff4a9b289c568bea0370319dfbdf20ccdd2d" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.21", + "windows 0.62.2", +] + +[[package]] +name = "gpui-base" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e9ae5237737cfd110d0ff513fcd9c20f97d98ebabd0a73164068177f0aca4f" +dependencies = [ + "aho-corasick", + "anyhow", + "ashpd", + "async-channel", + "chrono", + "data-url", + "futures", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", + "html5ever", + "instant", + "itertools 0.13.0", + "lsp-types", + "markdown", + "markup5ever_rcdom", + "num-traits", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "raw-window-handle", + "regex", + "ropey", + "schemars", + "serde", + "serde_json", + "smallvec", + "smol", + "tracing", + "unicode-linebreak", + "unicode-segmentation", + "web-time", + "windows 0.58.0", +] + +[[package]] +name = "gpui-component" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e155c4914bd809d966660610d596f7b57c5cd38979ca2c132215057a91f23f03" +dependencies = [ + "anyhow", + "chrono", + "core-text", + "enum-iterator", + "gpui-base", + "gpui-component-macros", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", + "instant", + "itertools 0.13.0", + "log", + "lsp-types", + "markdown", + "notify", + "num-traits", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "once_cell", + "paste", + "raw-window-handle", + "regex", + "resvg 0.45.1", + "ropey", + "rust-i18n", + "schemars", + "serde", + "serde_json", + "serde_repr", + "similar", + "smallvec", + "smol", + "tracing", + "unicode-segmentation", + "unicode-width", + "uuid", + "windows 0.58.0", +] + +[[package]] +name = "gpui-component-macros" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676a5d12c59ce295b5c82cc18365ad8b96cb683682e2e74668a56fb92544300" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-kit" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aca1dedfea73645b60b5893c9c16de0b1396f2cf4251d19e5f03a295c1b61879" +dependencies = [ + "gpui-base", + "gpui-component", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-platform", + "gpui-pre-web", +] + +[[package]] +name = "gpui-kit-assets" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5abe0bbd8f9226170c8726e529f6574519e297e9ed1bde86d5fd4d3536079a" +dependencies = [ + "anyhow", + "gpui-pre", + "gpui-pre-reqwest", + "log", + "rust-embed", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "gpui-pre" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29db89613527079737f53752eab27fb65c9a7dd94979256c65a0e590c9fd34d8" +dependencies = [ + "accesskit", + "anyhow", + "async-channel", + "async-task", + "backtrace", + "bitflags 2.13.2", + "chrono", + "derive_more", + "embed-resource", + "etagere", + "futures", + "getrandom 0.3.4", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-macros", + "gpui-pre-refineable", + "gpui-pre-scheduler", + "gpui-pre-shared-string", + "gpui-pre-sum-tree", + "gpui-pre-util", + "heapless", + "icu_segmenter", + "image", + "inventory", + "itertools 0.14.0", + "libc", + "log", + "lyon", + "num_cpus", + "objc2-core-foundation", + "objc2-core-video", + "parking", + "parking_lot", + "pin-project", + "pollster", + "profiling", + "rand 0.9.5", + "raw-window-handle", + "regex", + "resvg 0.46.0", + "schemars", + "seahash", + "serde", + "serde_json", + "slotmap", + "smallvec", + "spin 0.10.1", + "strum", + "taffy", + "thiserror 2.0.21", + "ttf-parser", + "unicode-segmentation", + "url", + "usvg 0.46.0", + "uuid", + "waker-fn", + "windows 0.62.2", + "zed-scap", +] + +[[package]] +name = "gpui-pre-apple" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a460605ec4e8cbcc9b213462a84b1d6cc3efe4ed8f0e18669659f07b2d5c6aec" +dependencies = [ + "anyhow", + "async-task", + "block2 0.6.2", + "cbindgen", + "core-foundation 0.10.1", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text", + "dispatch2", + "etagere", + "futures", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "libc", + "log", + "mach2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-security", + "parking_lot", + "pathfinder_geometry", + "smallvec", + "thiserror 2.0.21", + "zed-font-kit", +] + +[[package]] +name = "gpui-pre-collections" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb60ece401b399fd5ec13911915b407b0c6efa1f6c9a9f66201aaac56c7ec39d" +dependencies = [ + "gpui-pre-util", + "indexmap", + "rustc-hash 2.1.3", +] + +[[package]] +name = "gpui-pre-derive-refineable" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb2a3f5e6c9f300f432ae7989a81b262953ccfb3f9385be51cdc3303aef8858" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-http-client" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55245de7e3b6e219596fd98bb29555d6e3fe44c055d025960affeb11b894c048" +dependencies = [ + "anyhow", + "bytes", + "futures", + "http", + "http-body", + "parking_lot", + "serde", + "serde_json", + "serde_urlencoded", + "url", +] + +[[package]] +name = "gpui-pre-ios" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10656f6db4bbedb60bd7fcbb8a70f7c7eeee45bc4582fb8933ca807d2ddf52ad" +dependencies = [ + "anyhow", + "block2 0.6.2", + "futures", + "gpui-pre", + "gpui-pre-apple", + "log", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-ui-kit", + "parking_lot", + "raw-window-handle", + "uuid", +] + +[[package]] +name = "gpui-pre-linux" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54bab418d0b7c027af9495f278619b46887362b3bd6446e246c87c5b0fa167df" +dependencies = [ + "accesskit", + "accesskit_unix", + "anyhow", + "as-raw-xcb-connection", + "ashpd", + "bytemuck", + "calloop", + "calloop-wayland-source", + "filedescriptor", + "futures", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-util", + "gpui-pre-wgpu", + "libc", + "log", + "notify-rust", + "oo7", + "open", + "parking_lot", + "raw-window-handle", + "smallvec", + "smol", + "strum", + "url", + "uuid", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "wayland-protocols-wlr", + "x11rb", + "xkbcommon", + "zed-scap", + "zed-xim", +] + +[[package]] +name = "gpui-pre-macos" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de7d3f4ad51ac387e822495db363fcc1188d349793365731d054444c4cb06ee" +dependencies = [ + "accesskit", + "accesskit_macos", + "anyhow", + "block2 0.6.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "core-graphics 0.24.0", + "ctor", + "dispatch2", + "foreign-types", + "futures", + "gpui-pre", + "gpui-pre-apple", + "gpui-pre-collections", + "gpui-pre-scheduler", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "objc2 0.6.5", + "objc2-app-kit 0.3.2", + "objc2-core-graphics", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", + "parking_lot", + "raw-window-handle", + "semver", + "smallvec", + "strum", + "uuid", +] + +[[package]] +name = "gpui-pre-macros" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7f67904eced61ed42ae3320a9b4b388aed893f8547fffe9161ec425ad162eb" +dependencies = [ + "heck 0.5.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-platform" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b785bdcbd229f762096aebf4f9ae913bdaa9db49794907cabaf516eb55227b" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "gpui-pre", + "gpui-pre-ios", + "gpui-pre-linux", + "gpui-pre-macos", + "gpui-pre-web", + "gpui-pre-windows", +] + +[[package]] +name = "gpui-pre-refineable" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d197569e3b1fd84170f574bb866e95ecbc2625e7ae9443c877150eaf7d53374" +dependencies = [ + "gpui-pre-derive-refineable", +] + +[[package]] +name = "gpui-pre-reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05be23908e707966824f8c51a609904b7f30739e8d915e120a53c1b995ba964c" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry 0.4.0", +] + +[[package]] +name = "gpui-pre-scheduler" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d3fa97eeb4ea233635b756684affe96db9e29fc2af8cdc269268bd7ff649fc" +dependencies = [ + "async-task", + "backtrace", + "chrono", + "flume", + "futures", + "parking_lot", + "rand 0.9.5", + "wasm_thread", + "web-time", +] + +[[package]] +name = "gpui-pre-shared-string" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63c2ed1e44195079d4461b83a99845aa9a52ce8fcf1516410816670a03d32d94" +dependencies = [ + "schemars", + "serde", + "smol_str", +] + +[[package]] +name = "gpui-pre-sum-tree" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6085e61458366decd21491396b0b90b3adf928f2ec27e170c4d3c9fba455d536" +dependencies = [ + "gpui-pre-ztracing", + "heapless", + "rayon", + "tracing", +] + +[[package]] +name = "gpui-pre-util" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ead57ff96b5a34e71d64758080347066d667e0c4bd4ff71e85f3a50643fa3e" +dependencies = [ + "anyhow", + "log", + "which", +] + +[[package]] +name = "gpui-pre-web" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a85e775c6874257c63f9cf51d99f0aba13b3bc615ed8d47e7d5338e3b5941f6a" +dependencies = [ + "anyhow", + "async-channel", + "console_error_panic_hook", + "futures", + "gpui-pre", + "gpui-pre-http-client", + "gpui-pre-scheduler", + "gpui-pre-wgpu", + "js-sys", + "log", + "parking_lot", + "raw-window-handle", + "smallvec", + "unicode-properties", + "unicode-script", + "unicode-segmentation", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm_thread", + "web-sys", + "web-time", +] + +[[package]] +name = "gpui-pre-wgpu" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc76608d385f6d0dcbad44504b1bf669dac3b7d7ffa2885acdb7c1c358caf852" +dependencies = [ + "anyhow", + "bytemuck", + "cosmic-text", + "etagere", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", + "itertools 0.14.0", + "log", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "swash", + "unicode-bidi", + "unicode-segmentation", + "web-sys", + "wgpu", + "zed-font-kit", +] + +[[package]] +name = "gpui-pre-windows" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7201762420222d5b5ce5bdf0056996199212a3349bfb2457a3a0238c688681f3" +dependencies = [ + "accesskit", + "accesskit_windows", + "anyhow", + "dunce", + "etagere", + "futures", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-scheduler", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "parking_lot", + "rand 0.9.5", + "raw-window-handle", + "smallvec", + "uuid", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-numerics 0.3.1", + "windows-registry 0.6.1", +] + +[[package]] +name = "gpui-pre-ztracing" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1043bbe8d711baf03e774b03bae6521155f017450b866dcaa987a07d978d660a" +dependencies = [ + "gpui-pre-ztracing-macro", + "tracing", +] + +[[package]] +name = "gpui-pre-ztracing-macro" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85701631a9bbecf47e6d19f145e85e57767e7c50f7192d5b1bf65b5ef43cb373" + +[[package]] +name = "granit-parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0" +dependencies = [ + "arraydeque", + "smallvec", +] + +[[package]] +name = "h2" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "read-fonts 0.37.0", + "smallvec", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406e9ae5b95d8aa37db6293405931b766a6172d1848daee22d70eb44aa78a43c" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3632ebbd0c5c3637ba1a40bdf8288f2cb9d2268e73441a69d5fc73d7cfbaa87c" + +[[package]] +name = "icu_normalizer" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" + +[[package]] +name = "icu_properties" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b195dc605f81aa181bec9b3bd14e26cc71b903e4644f6af578ca9772c7a710" +dependencies = [ + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "smallvec", + "utf8_iter", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913a7d294ad056474749152107309fdee3ec17d2482d1ab70fdc61a81c64b9a3" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484be5657551d7868d67587df967fb4fcbe9d266a761cc855aaa148fda0b3cc7" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif 0.14.2", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "tiff", + "zune-core 0.5.3", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "imgref" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" +dependencies = [ + "bitflags 2.13.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "inventory" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6928282826c822ad91bf1c9a1cb90a30ba1c26770749929b4656cd6be829cd7c" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd1574e617a9a6c1f8f8181bd7e841e1f20e4277607ef28945765188d6284d4" + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kqueue" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.2", + "libc", +] + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "lexical-conformance" +version = "0.0.0" +dependencies = [ + "lexical-editor", +] + +[[package]] +name = "lexical-editor" +version = "0.0.0" + +[[package]] +name = "lexical-playground-core" +version = "0.0.0" +dependencies = [ + "lexical-conformance", + "lexical-editor", + "unicode-segmentation", +] + +[[package]] +name = "lexical-playground-gpui" +version = "0.0.0" +dependencies = [ + "gpui-kit", + "lexical-playground-core", + "unicode-segmentation", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "link-section" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c29a617ce3df32c08497bdc1ab6e2376e0b17948ac166a2fbe5977c5954cd9" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e57c38c1e860fd37c604281cdfb1dd2216977fd76a50f85ba2f388ef3219616" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "litrs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "lsp-types" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" +dependencies = [ + "bitflags 1.3.2", + "fluent-uri", + "serde", + "serde_json", + "serde_repr", +] + +[[package]] +name = "lyon" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a6b9d4e8b48acd531fbf1241ad9b270df6e60b6b92bdea1469f4f40899d805" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82dd26aa4dc0b5b70b9115de59824842bcb43cf4a28e1b1e6d0ad148c93f93e" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd4872650e70dc907dd4f7ebc449d7d657a0d2658e423b2e724970870bbbf75b" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mac-notification-sys" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" +dependencies = [ + "cc", + "log", + "objc2 0.6.5", + "objc2-foundation 0.3.2", + "time", + "uuid", +] + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "serde", + "unicode-id", +] + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0ca1155a9ecc25aa12e8313a119f32edf0255fea2f44839c4ac0a12b2ac157" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5848f5a9e8117cf94f2f90faac24a87ff81600ff8628c1e3deaab572e76bfa1" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multiversion" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb8a68be21d0755f83bfe47e79b896cc732687cf1903f0e50585e8d6ecc53076" +dependencies = [ + "multiversion-macros", + "target-features", +] + +[[package]] +name = "multiversion-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a05e0c3c3e4edaa21d87c645852ed0b0c3663752bcc6ec43353bd6189dce35e1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "target-features", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "naga" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.17.1", + "indexmap", + "libm", + "log", + "naga-types", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.21", + "unicode-ident", +] + +[[package]] +name = "naga-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" +dependencies = [ + "hashbrown 0.17.1", + "indexmap", + "rustc-hash 1.1.0", + "thiserror 2.0.21", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "normpath" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11ce00d2594068e8a27c9146fdc5cf9f3ac38eb42c7cd34d05fea618873ac9f" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "notify" +version = "8.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41a04a5f59df9322cc657286dd2d79f90816119b22c5f847607c850110205ab" +dependencies = [ + "bitflags 2.13.2", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "objc2-core-foundation", + "objc2-core-services", + "walkdir", + "windows-sys 0.61.2", +] + +[[package]] +name = "notify-rust" +version = "4.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ff2e74231b72c832d82982193b417f230945be6bdb5575b251d941d31adb00" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "notify-types" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a437a9308232ce794b1e7fdf540d38ea37efc3c73492640c13b4c8568b8f108" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f9a86e097b0d187ad0e65667c2f58b9254671e86e7dbb78036b16692eae099" +dependencies = [ + "libm", + "num-integer", + "num-iter", + "num-traits", + "once_cell", + "rand 0.9.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d5490aaf8f1d7cf7688dfa9b0ce07900e168852c45cd2c03f534dfd27cfd0b" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.3", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.5", + "objc2-cloud-kit", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-uniform-type-identifiers", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "dispatch2", + "libc", + "objc2 0.6.5", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-services" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583300ad934cba24ff5292aee751ecc070f7ca6b39a574cc21b7b5e588e06a0b" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.3", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.5", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "dispatch2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7902ac02859fc1f7045f8b598c63f1ae0cc7efeaa06a9bc9f3d9a3c955974fa4" +dependencies = [ + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oo7" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f2bfed90f1618b4b48dcad9307f25e14ae894e2949642c87c351601d62cebd" +dependencies = [ + "aes", + "ashpd", + "async-fs", + "async-io", + "async-lock", + "blocking", + "cbc", + "cipher", + "digest 0.10.7", + "endi", + "futures-lite", + "futures-util", + "getrandom 0.4.3", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "serde", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zbus", + "zbus_macros", + "zeroize", + "zvariant", +] + +[[package]] +name = "open" +version = "5.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95" +dependencies = [ + "is-wsl", + "libc", +] + +[[package]] +name = "openssl-probe" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e036b411754132fd6b74e94b74e9a13c5cf606fa93d1885c35860d3060820b1" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c860fd3227ca4ac3cc032e2cd20cba3f02ccdf4b610538f8ee6584d56bb62e96" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.8", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.19+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.3", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash 2.1.3", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.5", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.21", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.11.3", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.7", + "once_cell", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif 0.13.3", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", + "tiny-skia", + "usvg 0.45.1", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "resvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" +dependencies = [ + "gif 0.14.2", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.16.2", + "tiny-skia", + "usvg 0.46.0", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ropey" +version = "2.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4045a00dc327d084a2bbf126976e14125b54f23bd30511d45b842eba76c52d74" +dependencies = [ + "str_indices", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "roxmltree" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da7f59328b09405b181fee1018925fad50f157afb43c0c0f5c522c256dd5907" +dependencies = [ + "memchr", +] + +[[package]] +name = "rust-embed" +version = "8.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19afa5b4b6a611de00bd1bdae6ae6f39084c9399f0679c3f52d8469cf335cc23" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d8afda6374eac59e066abee06d265247ebbaf3006cf878e2879e8356e34053" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 2.0.119", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d84e8ba78bd384263e5922f084cbe1b081c3b7e69add59c8fb097b879ba968a" +dependencies = [ + "globset", + "sha2 0.11.1", + "walkdir", +] + +[[package]] +name = "rust-i18n" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8502e73e6461c738659455c5d2cf95bc132760a6f203c717e66ee72b78906d7f" +dependencies = [ + "globwalk", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54a28dd9d46108d56b9f0c85b220908844233e561c03087890325104f90f2c6" +dependencies = [ + "glob", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "syn 2.0.119", +] + +[[package]] +name = "rust-i18n-support" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e806c2a9701cbd6043992cd935c30cb1fdb128fa15e7b506f3b46f97045737c0" +dependencies = [ + "arc-swap", + "base62", + "foldhash", + "globwalk", + "itertools 0.11.0", + "normpath", + "serde", + "serde-saphyr", + "serde_json", + "siphasher", + "toml 0.8.23", + "triomphe", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102f35f7de7b06d8c61f1cff6592f5253e0dea431c74867aad5dfd70f44663fb" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c3a92094fa7d61aa124645844facb6b554dfc797136d0f5fd1f890e2bffc69" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.10", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "screencapturekit" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" +dependencies = [ + "screencapturekit-sys", +] + +[[package]] +name = "screencapturekit-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" +dependencies = [ + "block", + "dispatch", + "objc", + "objc-foundation", + "objc_id", + "once_cell", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-saphyr" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8" +dependencies = [ + "annotate-snippets", + "base64 0.23.1", + "encoding_rs_io", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "foldhash", + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c187e0a369471c550af028cba56d4f4fa2a489ad28151ae95a4d340a2a8b782" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs 6.0.0", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +dependencies = [ + "bstr", + "unicode-segmentation", +] + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", +] + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "149309498a5f7334b12ddae0cad8aba81ce785fe91755068867e017cf98fee46" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "smol_str" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb7109d23f319619d4cc0de5464613c8b7b1a09db2774a58101459cf6be2f10" +dependencies = [ + "serde_core", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str_indices" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher", +] + +[[package]] +name = "svgtypes" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f12b12995e3638c70618c1facb1ce65302f6a6f706b675f07c58a5d7391120e" +dependencies = [ + "kurbo 0.13.1", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" +dependencies = [ + "skrifa 0.44.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae9ccbea3a7b0b09443036490c8f34a8b1b760fd345e5c0dae0aac718faea79" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "taffy" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f7bd2d97198df05186f93a3a3c306845f40e8b3c22bf9279cdd7e5cf5fea7d" +dependencies = [ + "serde", + "slotmap", + "smallvec", +] + +[[package]] +name = "tao-core-video-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "objc", +] + +[[package]] +name = "target-features" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1bbb9f3c5c463a01705937a24fdabc5047929ac764b2d5b9cf681c1f5041ed5" + +[[package]] +name = "tauri-winrt-notification" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade" +dependencies = [ + "thiserror 2.0.21", + "windows 0.61.3", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.2.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5755c51863cdac711f1956baf8eb9588e060755f7d3c4c979e70ea1e1f32cd7" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.3", + "toml_datetime 1.1.3+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.5", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173f25c883cd2176a2d8212155638c0a60d484a9d84546d270866f40f56b6e49" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.19+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd43688c4858d7264ae1cf9a9f874a67c3feb74a883b1cd7bebd6a4ea8307218" +dependencies = [ + "indexmap", + "toml_datetime 1.1.3+spec-1.1.0", + "toml_parser", + "winnow 1.0.5", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow 1.0.5", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.2.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009dbb7700f11c55ef83aebbe4c0e9cf1f1ac0844bd8d6f00dc38f302026e13a" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "triomphe" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d15f4f5898bcfc5d8a8e45ed30b2f108d710c1a2e5489dbf80e078f707181bc" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-id" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993ad287eb20e8ba755312801cfe1413e0f25d604075863acaddca10b04c1833" + +[[package]] +name = "unicode-script" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa04c955db05429d464c74d495c1ee677e1866e783a4c3747f9c0cef7756cabf" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.13.0", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "usvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.14.0", + "kurbo 0.13.1", + "log", + "pico-args", + "roxmltree 0.21.2", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes 0.16.2", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3bd9d3e571c26fcce9d033b87ea5ab061b8d2ffe9b7a5675d810d68f03edf0" +dependencies = [ + "utf_types", +] + +[[package]] +name = "utf_types" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" +dependencies = [ + "cfg-if", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e14e0327cb5368ad60ccb22d04a9827a693c95a926d8d125e9fa39e0baec501" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.10", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm_thread" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7516db7f32decdadb1c3b8deb1b7d78b9df7606c5cc2f6241737c2ab3a0258e" +dependencies = [ + "futures", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "log", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d179596ff5ec0f6de4db64d54011bde6289cc49603d3a6e4ce3f329207ab88" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.13.2", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "indexmap", + "log", + "naga", + "naga-types", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.21", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d260ca2ca8adc654f513a869bf3cac0b47f5b8d3f87803d6942015f8de597aae" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.13.2", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "hashbrown 0.17.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "naga-types", + "ndk-sys", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "static_assertions", + "thiserror 2.0.21", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-result 0.4.1", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "js-sys", + "log", + "naga-types", + "raw-window-handle", + "static_assertions", + "web-sys", +] + +[[package]] +name = "which" +version = "8.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-capture" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" +dependencies = [ + "parking_lot", + "rayon", + "thiserror 2.0.21", + "windows 0.61.3", + "windows-future 0.2.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3b93199df0ab874cdf0c6a94f0a0dc3423fe41f1cafb62b8bd7b1deb277aef" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "rustix", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcb" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c2ad15e0e922856ee89afe862b8992334bbe7953adad56cd1199358cb30566" +dependencies = [ + "bitflags 2.13.2", + "libc", + "quick-xml", + "x11", +] + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xim-ctext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac61a7062c40f3c37b6e82eeeef835d5cc7824b632a72784a89b3963c33284c" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "xim-parser" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcee45f89572d5a65180af3a84e7ddb24f5ea690a6d3aa9de231281544dd7b7" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "as-raw-xcb-connection", + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever", +] + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yazi" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac366ea532fa8ee6a992cec9dea525b1dc284e69702f36c2ab5fbebadc81ec86" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.5", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.10", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812a12be4ead7a59e18ea8f047d08add0066b73bc62fc1467066a2472ae088dd" +dependencies = [ + "serde", + "winnow 1.0.5", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow 1.0.5", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "939f186d9ecebe044315a5cb6d6f78568bbc21152adc954d5a0c2f58bdbc9920" +dependencies = [ + "serde", +] + +[[package]] +name = "zed-font-kit" +version = "0.14.1-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" +dependencies = [ + "bitflags 2.13.2", + "byteorder", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "core-text", + "dirs 5.0.1", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "zed-scap" +version = "0.0.8-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" +dependencies = [ + "anyhow", + "cocoa", + "core-graphics-helmer-fork", + "log", + "objc", + "rand 0.8.8", + "screencapturekit", + "screencapturekit-sys", + "sysinfo", + "tao-core-video-sys", + "windows 0.61.3", + "windows-capture", + "x11", + "xcb", +] + +[[package]] +name = "zed-xim" +version = "0.4.0-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0b46ed118eba34d9ba53d94ddc0b665e0e06a2cf874cfa2dd5dec278148642" +dependencies = [ + "ahash", + "hashbrown 0.14.5", + "log", + "x11rb", + "xim-ctext", + "xim-parser", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.10", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core 0.5.3", +] + +[[package]] +name = "zvariant" +version = "5.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bf6d6cd6a4b601acb64cfb8284cdc643fcfa75ef26ae3c46ec8aa4e8a1fbb7" +dependencies = [ + "endi", + "enumflags2", + "serde", + "serde_bytes", + "winnow 1.0.5", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2672456ba1cb54b5eaec136e63135b3c7495fb938e53b28a62e633496d486d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.10", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.10", + "winnow 1.0.5", +] diff --git a/rust/apps/lexical-playground-gpui/Cargo.toml b/rust/apps/lexical-playground-gpui/Cargo.toml new file mode 100644 index 00000000000..fe22e173ead --- /dev/null +++ b/rust/apps/lexical-playground-gpui/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "lexical-playground-gpui" +version = "0.0.0" +description = "The Lexical playground as a native desktop app: lexical-editor drawn with GPUI." +edition = "2024" +license = "MIT" +publish = false + +# Its own workspace, beside rust/ and the Rune app: GPUI needs a newer Rust +# than lexical-editor's 1.88 (rust-toolchain.toml). +[workspace] + +[dependencies] +lexical-playground-core = { path = "../lexical-playground-core" } +# GPUI through GPUI Kit, the version wevm/zero's zbugs-gpui uses. +gpui-kit = "0.9.1" +# Graphemes and words, for moving the caret and double-click selection. +unicode-segmentation = "1" + +[features] +# The end-to-end run CI does: see src/e2e.rs. +e2e = [] + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +all = { level = "warn", priority = -1 } diff --git a/rust/apps/lexical-playground-gpui/README.md b/rust/apps/lexical-playground-gpui/README.md new file mode 100644 index 00000000000..9c6553c6645 --- /dev/null +++ b/rust/apps/lexical-playground-gpui/README.md @@ -0,0 +1,75 @@ +# Lexical playground on GPUI + +The Lexical playground as a native desktop app: a document in +[`lexical-editor`](../../lexical-editor), the Rust port of Lexical, drawn and +edited with [GPUI](https://www.gpui.rs) through GPUI Kit, as wevm/zero's +zbugs-gpui is. macOS. + +- **The editor.** Lexical owns the document and the selection. The editor + is a GPUI element of its own (`src/editor.rs`) that draws them, shapes + and wraps the text, hit-tests the pointer and moves the caret, as a + browser does for `contenteditable`. It is the window's text input client + (`EntityInputHandler`), so typing, the character palette and input + methods reach it as macOS sends them. Every change goes through Lexical: + - Typed and committed text runs `insertText`; Return `insertParagraph`; + Shift-Return `insertLineBreak`; Backspace and Delete delete by grapheme, + Option by word and Command by line; paste runs `insertRawText`; ⌘A, ⌘B, + ⌘I and ⌘U select all and format. + - An input method's composition is drawn underlined and kept out of + Lexical until it commits, as a browser keeps it out of the editor + state. + - After each edit the element draws again from Lexical's state. + - The binding, `Surface` in + [`lexical-playground-core`](../lexical-playground-core), maps the + window's UTF-16 ranges to Lexical points. It also backs the Rune + playground. +- **The toolbar.** Bold, italic, underline, strikethrough, code, highlight, + subscript and superscript follow the selection's format. Select all, and + Reset. +- **The side panel.** + - **Tree:** the node tree with keys. + - **JSON:** the editor state's JSON. + - **Parity:** **Replay vectors** runs the conformance vectors JS recorded + (`packages/lexical-conformance/vectors`) in the app. **Export session** + writes this session as a vector file (`$TMPDIR/lexical-playground-session.json`, + or `$LEXICAL_PLAYGROUND_EXPORT`), which JS Lexical replays and compares + at every step: + + ```sh + cd packages/lexical-conformance + LEXICAL_REPLAY=/path/to/lexical-playground-session.json pnpm exec vitest run src/replay.test.ts + ``` + +Undo, headings, lists, links, code blocks, HTML and Markdown arrive as the +port gets the packages ([DESIGN.md](../../DESIGN.md), milestones). + +## Running it + +```sh +cd rust/apps/lexical-playground-gpui +cargo run --release +``` + +The app is a Cargo workspace of its own, on the toolchain zbugs-gpui uses +(`rust-toolchain.toml`), apart from `rust/`, which builds on 1.88. rustup +installs that toolchain on the first build. + +`cargo run` runs the app as an app: `bundle.sh`, Cargo's runner on macOS +(`.cargo/config.toml`), makes `Lexical Playground (GPUI).app` beside the +executable and runs the app from inside it, so it shows in the Dock. The +bundle stays, to open from Finder. + +## End to end + +Built with the `e2e` feature, the app drives its own window once it's up: +it types, formats, composes with an input method, types an emoji and +deletes it, and adds a paragraph and a line break, through the input +handler and the key bindings, checking the document after each step. It +writes the session to `$LEXICAL_PLAYGROUND_E2E` and exits 0, or 1 with the +step that failed. CI (`.github/workflows/rust-playground.yml`) then replays +the session in JS. + +```sh +cargo build --features e2e +LEXICAL_PLAYGROUND_E2E=/tmp/session.json ./target/debug/lexical-playground-gpui +``` diff --git a/rust/apps/lexical-playground-gpui/bundle.sh b/rust/apps/lexical-playground-gpui/bundle.sh new file mode 100755 index 00000000000..cdfc37bf1f2 --- /dev/null +++ b/rust/apps/lexical-playground-gpui/bundle.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# Cargo's runner on macOS (.cargo/config.toml): runs the playground from +# inside an app bundle, `Lexical Playground (GPUI).app` beside the +# executable, so it launches as an app in the Dock and the app switcher. The +# bundle stays, to open from Finder or keep in the Dock. Tests and other +# executables run as they are. +set -eu +exe=$1 +shift +[ "$(basename "$exe")" = lexical-playground-gpui ] || exec "$exe" "$@" + +app="$(cd "$(dirname "$exe")" && pwd)/Lexical Playground (GPUI).app" +mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" +cat > "$app/Contents/Info.plist" <<'PLIST' + + + + + CFBundleExecutable + lexical-playground-gpui + CFBundleIdentifier + dev.lexical.playground-gpui + CFBundleName + Lexical Playground (GPUI) + CFBundlePackageType + APPL + LSApplicationCategoryType + public.app-category.developer-tools + NSHighResolutionCapable + + + +PLIST +# A hard link, not a copy: the executable can be large in a debug build. +ln -f "$exe" "$app/Contents/MacOS/lexical-playground-gpui" +/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \ + -f "$app" +exec "$app/Contents/MacOS/lexical-playground-gpui" "$@" diff --git a/rust/apps/lexical-playground-gpui/rust-toolchain.toml b/rust/apps/lexical-playground-gpui/rust-toolchain.toml new file mode 100644 index 00000000000..7ffd41dc286 --- /dev/null +++ b/rust/apps/lexical-playground-gpui/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +# The toolchain wevm/zero's zbugs-gpui uses. +channel = "1.99.1" +components = ["rustfmt", "clippy"] +profile = "minimal" diff --git a/rust/apps/lexical-playground-gpui/src/app.rs b/rust/apps/lexical-playground-gpui/src/app.rs new file mode 100644 index 00000000000..d38986e6ab9 --- /dev/null +++ b/rust/apps/lexical-playground-gpui/src/app.rs @@ -0,0 +1,292 @@ +//! The window: a toolbar, the editor, and a side panel with the editor's +//! tree, its JSON, and parity with JS Lexical. + +use crate::editor::{EditorEvent, LexicalEditor}; +use gpui_kit::component::button::*; +use gpui_kit::component::*; +use gpui_kit::*; +use lexical_playground_core::{Playground, example, replay_vectors}; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Panel { + Tree, + Json, + Parity, +} + +const FORMATS: [(&str, &str); 8] = [ + ("bold", "B"), + ("italic", "I"), + ("underline", "U"), + ("strikethrough", "S"), + ("code", ""), + ("highlight", "Mark"), + ("subscript", "x₂"), + ("superscript", "x²"), +]; + +pub struct PlaygroundApp { + editor: Entity, + panel: Panel, + report: Option<(String, bool)>, + status: Option, + _subscription: Subscription, +} + +impl PlaygroundApp { + #[cfg(feature = "e2e")] + pub fn editor(&self) -> &Entity { + &self.editor + } + + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let editor = cx.new(|cx| { + LexicalEditor::new(Playground::new(example()).expect("the example loads"), cx) + }); + let subscription = cx.subscribe(&editor, |this, _, _: &EditorEvent, cx| { + this.status = None; + cx.notify(); + }); + window.focus(&editor.focus_handle(cx), cx); + PlaygroundApp { + editor, + panel: Panel::Tree, + report: None, + status: None, + _subscription: subscription, + } + } + + /// Runs a toolbar button's edit, then gives the keyboard back to the + /// editor, as a toolbar in a web editor keeps the selection. + fn toolbar_edit( + &mut self, + window: &mut Window, + cx: &mut Context, + f: impl FnOnce(&mut Playground) -> lexical_playground_core::Outcome, + ) { + self.editor.update(cx, |editor, cx| editor.edit(cx, f)); + window.focus(&self.editor.focus_handle(cx), cx); + } + + fn toolbar(&self, cx: &mut Context) -> impl IntoElement { + let playground = &self.editor.read(cx).surface.playground; + let mut bar = h_flex() + .w_full() + .gap_1() + .px_3() + .py_2() + .border_b_1() + .border_color(cx.theme().border) + .child(div().font_weight(FontWeight::SEMIBOLD).mr_2().child("Lexical")); + for (name, label) in FORMATS { + let on = playground.has_format(name); + bar = bar.child( + Button::new(SharedString::from(format!("format-{name}"))) + .small() + .label(label) + .selected(on) + .tooltip(format!("Format {name}")) + .on_click(cx.listener(move |this, _, window, cx| { + this.toolbar_edit(window, cx, |p| p.format_text(name)) + })), + ); + } + bar.child( + Button::new("select-all") + .small() + .ghost() + .label("Select all") + .on_click(cx.listener(|this, _, window, cx| { + this.toolbar_edit(window, cx, Playground::select_all) + })), + ) + .child(div().flex_1()) + .child( + Button::new("reset") + .small() + .ghost() + .label("Reset") + .on_click(cx.listener(|this, _, window, cx| { + let editor = + cx.new(|cx| LexicalEditor::new(Playground::new(example()).unwrap(), cx)); + this._subscription = cx.subscribe(&editor, |this, _, _: &EditorEvent, cx| { + this.status = None; + cx.notify(); + }); + window.focus(&editor.focus_handle(cx), cx); + this.editor = editor; + this.status = None; + cx.notify(); + })), + ) + } + + fn panel(&self, cx: &mut Context) -> impl IntoElement { + let tab = |id: &'static str, label: &'static str, panel: Panel, cx: &mut Context| { + Button::new(id) + .small() + .label(label) + .selected(self.panel == panel) + .on_click(cx.listener(move |this, _, _, cx| { + this.panel = panel; + cx.notify(); + })) + }; + let playground = &self.editor.read(cx).surface.playground; + let mono = cx.theme().mono_font_family.clone(); + let content: AnyElement = match self.panel { + Panel::Tree => div() + .font_family(mono) + .text_sm() + .child(playground.document().tree()) + .into_any_element(), + Panel::Json => div() + .font_family(mono) + .text_sm() + .child(playground.state_pretty_json()) + .into_any_element(), + Panel::Parity => self.parity(cx).into_any_element(), + }; + v_flex() + .w(px(420.)) + .h_full() + .min_h(px(0.)) + .gap_2() + .child( + h_flex() + .gap_1() + .child(tab("tab-tree", "Tree", Panel::Tree, cx)) + .child(tab("tab-json", "JSON", Panel::Json, cx)) + .child(tab("tab-parity", "Parity", Panel::Parity, cx)), + ) + .child( + div() + .id("panel") + .flex_1() + .min_h(px(0.)) + .p_3() + .rounded_md() + .border_1() + .border_color(cx.theme().border) + .overflow_y_scroll() + .child(content), + ) + } + + fn parity(&self, cx: &mut Context) -> impl IntoElement { + let mono = cx.theme().mono_font_family.clone(); + let mut out = v_flex() + .gap_2() + .child( + div().text_sm().text_color(cx.theme().muted_foreground).child( + "The editor here is lexical-editor. Replay runs the conformance vectors \ + that JS Lexical recorded; Export writes this session as a vector that JS \ + replays step by step, in packages/lexical-conformance:", + ), + ) + .child( + div() + .font_family(mono.clone()) + .text_xs() + .child("LEXICAL_REPLAY= pnpm exec vitest run src/replay.test.ts"), + ) + .child( + h_flex() + .gap_2() + .child(Button::new("replay").small().label("Replay vectors").on_click( + cx.listener(|this, _, _, cx| { + this.report = Some(replay_vectors().unwrap_or_else(|e| (e, false))); + cx.notify(); + }), + )) + .child(Button::new("export").small().label("Export session").on_click( + cx.listener(|this, _, _, cx| { + this.status = Some(export(&this.editor.read(cx).surface.playground)); + cx.notify(); + }), + )), + ); + if let Some((report, ok)) = &self.report { + out = out + .child( + div() + .font_weight(FontWeight::SEMIBOLD) + .text_color(if *ok { cx.theme().success } else { cx.theme().danger }) + .child(if *ok { + "Every replayed case matches JS." + } else { + "Some cases differ from JS." + }), + ) + .child(div().font_family(mono).text_xs().child(report.clone())); + } + out + } + + fn status_bar(&self, cx: &mut Context) -> impl IntoElement { + let editor = self.editor.read(cx); + let playground = &editor.surface.playground; + let (message, color) = match (&editor.error, &self.status) { + (Some(error), _) => (format!("The last edit failed: {error}"), cx.theme().danger), + (None, Some(status)) => (status.clone(), cx.theme().muted_foreground), + (None, None) => ( + format!( + "lexical-editor · {} steps · {} blocks", + playground.step_count(), + playground.document().blocks.len() + ), + cx.theme().muted_foreground, + ), + }; + div().px_4().py_2().text_xs().text_color(color).child(message) + } +} + +/// Writes the session's vector file; returns what to tell the user. +fn export(playground: &Playground) -> String { + let path = std::env::var_os("LEXICAL_PLAYGROUND_EXPORT") + .map(std::path::PathBuf::from) + .unwrap_or_else(|| std::env::temp_dir().join("lexical-playground-session.json")); + match std::fs::write(&path, playground.export_vector_file("gpui playground")) { + Ok(()) => format!( + "Exported {} steps to {}", + playground.step_count(), + path.display() + ), + Err(e) => format!("Can't write {}: {e}", path.display()), + } +} + +impl Render for PlaygroundApp { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .bg(cx.theme().background) + .text_color(cx.theme().foreground) + .child(self.toolbar(cx)) + .child( + h_flex() + .flex_1() + .min_h(px(0.)) + .w_full() + .gap_4() + .p_4() + .items_start() + .child( + div() + .flex_1() + .h_full() + .p_5() + .rounded_lg() + .border_1() + .border_color(cx.theme().border) + .text_size(px(16.)) + .line_height(px(24.)) + .child(self.editor.clone()), + ) + .child(self.panel(cx)), + ) + .child(self.status_bar(cx)) + } +} diff --git a/rust/apps/lexical-playground-gpui/src/e2e.rs b/rust/apps/lexical-playground-gpui/src/e2e.rs new file mode 100644 index 00000000000..6683afb31ab --- /dev/null +++ b/rust/apps/lexical-playground-gpui/src/e2e.rs @@ -0,0 +1,166 @@ +//! An end-to-end run for CI, as wevm/zero's zbugs-gpui has: built with the +//! `e2e` feature and run with `LEXICAL_PLAYGROUND_E2E=`, the app +//! drives its own window once it's up, the way macOS does — text through +//! the window's input handler (typing and an input method's composition) +//! and keys through their bindings — checks the document after each step, +//! writes the session to `` as a vector, and exits 0, or 1 with the +//! step that failed. CI then replays the file in JS Lexical +//! (`packages/lexical-conformance/src/replay.test.ts`), which compares +//! every step's state and selection with JS's. +//! +//! A screenshot of the window at the end goes to +//! `LEXICAL_PLAYGROUND_E2E_SHOT`, when it's set. + +use crate::app::PlaygroundApp; +use crate::editor::*; +use gpui_kit::*; +use std::time::{Duration, Instant}; + +type Step = Result<(), String>; + +pub fn run(window: AnyWindowHandle, app: Entity, cx: &mut App) { + let Some(path) = std::env::var_os("LEXICAL_PLAYGROUND_E2E") else { + return; + }; + cx.spawn(async move |cx| { + let result = drive(window, &app, cx).await.and_then(|()| { + let file = app.read_with(cx, |app, cx| { + let playground = &app.editor().read(cx).surface.playground; + playground.export_vector_file("gpui end-to-end run") + }); + std::fs::write(&path, file).map_err(|e| format!("can't write the session: {e}")) + }); + if let Some(shot) = std::env::var_os("LEXICAL_PLAYGROUND_E2E_SHOT") { + let _ = std::process::Command::new("screencapture") + .arg("-x") + .arg(shot) + .status(); + } + match result { + Ok(()) => { + eprintln!("e2e: passed"); + std::process::exit(0); + } + Err(error) => { + eprintln!("e2e: {error}"); + std::process::exit(1); + } + } + }) + .detach(); +} + +async fn drive(window: AnyWindowHandle, app: &Entity, cx: &mut AsyncApp) -> Step { + let editor = app + .read_with(cx, |app, _| app.editor().clone()); + // Keys move the caret through the last paint's layout. + let deadline = Instant::now() + Duration::from_secs(30); + while !editor + .read_with(cx, |e, _| e.is_laid_out()) + { + if Instant::now() > deadline { + return Err("timed out waiting for the first paint".into()); + } + cx.background_executor().timer(Duration::from_millis(50)).await; + } + + let line = |cx: &mut AsyncApp, n: usize| -> Result { + let text = editor.read_with(cx, |e, _| e.surface.display_text()); + Ok(text.split('\n').nth(n).unwrap_or_default().to_owned()) + }; + // One step: a key (its action) or input, then the check. + macro_rules! step { + ($what:expr, $input:expr, $check:expr) => {{ + cx.update_window(window, |_, window, cx| $input(window, cx)) + .map_err(|e| e.to_string())?; + // Actions run when the window next handles events. + cx.background_executor().timer(Duration::from_millis(30)).await; + if let Some(error) = editor + .read_with(cx, |e, _| e.error.clone()) + { + return Err(format!("{}: the edit failed: {error}", $what)); + } + let check: Step = $check(&mut *cx); + check.map_err(|e| format!("{}: {e}", $what))?; + }}; + } + let key = |action: Box| { + move |window: &mut Window, cx: &mut App| window.dispatch_action(action, cx) + }; + let typed = |s: &'static str| { + let editor = editor.clone(); + move |window: &mut Window, cx: &mut App| { + editor.update(cx, |e, cx| e.replace_text_in_range(None, s, window, cx)) + } + }; + let expect = |n: usize, want: &'static str| { + move |cx: &mut AsyncApp| -> Step { + let got = line(cx, n)?; + if got == want { + Ok(()) + } else { + Err(format!("line {n} is {got:?}, not {want:?}")) + } + } + }; + let bold = |want: bool| { + let editor = editor.clone(); + move |cx: &mut AsyncApp| -> Step { + let got = editor + .read_with(cx, |e, _| e.surface.playground.has_format("bold")); + if got == want { + Ok(()) + } else { + Err(format!("bold is {got}, not {want}")) + } + } + }; + + const FIRST: &str = "Hello from Rust. This document lives in a lexical-editor editor."; + step!("End of the line", key(Box::new(LineEnd)), expect(0, FIRST)); + step!("Return", key(Box::new(InsertParagraph)), expect(1, "")); + step!("⌘B", key(Box::new(Bold)), bold(true)); + step!("Typing bold", typed("Typed in GPUI"), expect(1, "Typed in GPUI")); + step!("⌘B again", key(Box::new(Bold)), bold(false)); + step!( + "Typing plain", + typed(", then plain wordy"), + expect(1, "Typed in GPUI, then plain wordy") + ); + step!( + "⌥⌫", + key(Box::new(DeleteWordBackward)), + expect(1, "Typed in GPUI, then plain ") + ); + step!( + "Composing", + { + let editor = editor.clone(); + move |window: &mut Window, cx: &mut App| { + editor.update(cx, |e, cx| { + e.replace_and_mark_text_in_range(None, "にほ", None, window, cx) + }) + } + }, + expect(1, "Typed in GPUI, then plain にほ") + ); + step!( + "Committing the composition", + typed("日本"), + expect(1, "Typed in GPUI, then plain 日本") + ); + step!("Typing an emoji", typed("👍🏽"), expect(1, "Typed in GPUI, then plain 日本👍🏽")); + step!( + "⌫ over the emoji", + key(Box::new(Backspace)), + expect(1, "Typed in GPUI, then plain 日本") + ); + step!("⇧Return", key(Box::new(InsertLineBreak)), expect(2, "")); + step!("Typing after the break", typed("next"), expect(2, "next")); + step!( + "The third paragraph is untouched", + |_: &mut Window, _: &mut App| {}, + expect(3, "Type, delete, press Enter, or select text and make it bold.") + ); + Ok(()) +} diff --git a/rust/apps/lexical-playground-gpui/src/editor.rs b/rust/apps/lexical-playground-gpui/src/editor.rs new file mode 100644 index 00000000000..a2bec2ff52b --- /dev/null +++ b/rust/apps/lexical-playground-gpui/src/editor.rs @@ -0,0 +1,691 @@ +//! The editor: a GPUI element that draws a [`Surface`] (the document, its +//! selection and an input method's composition) and hands it the window's +//! text input (`EntityInputHandler`), keys (actions) and pointer. +//! +//! Lexical owns the document and the selection, as in a browser: every +//! edit runs in `lexical-editor`, and the element is drawn again from its +//! state, as Lexical's reconciler draws the DOM. Moving the caret and +//! laying out text are the element's, as they are the browser's. + +use gpui_kit::component::ActiveTheme; +use gpui_kit::*; +use lexical_playground_core::{Outcome, Playground, Surface, Unit, format_flag}; +use std::ops::Range; +use std::rc::Rc; +use unicode_segmentation::UnicodeSegmentation; + +actions!( + lexical_editor, + [ + Backspace, + Delete, + DeleteWordBackward, + DeleteWordForward, + DeleteLineBackward, + DeleteLineForward, + InsertParagraph, + InsertLineBreak, + InsertTab, + Left, + Right, + Up, + Down, + SelectLeft, + SelectRight, + SelectUp, + SelectDown, + LineStart, + LineEnd, + SelectLineStart, + SelectLineEnd, + SelectAll, + Bold, + Italic, + Underline, + Copy, + Cut, + Paste, + ShowCharacterPalette, + ] +); + +const CONTEXT: &str = "LexicalEditor"; + +/// The editor's keys, as a browser's `contenteditable` takes them on macOS. +pub fn bind_keys(cx: &mut App) { + let c = Some(CONTEXT); + cx.bind_keys([ + KeyBinding::new("backspace", Backspace, c), + KeyBinding::new("shift-backspace", Backspace, c), + KeyBinding::new("delete", Delete, c), + KeyBinding::new("ctrl-d", Delete, c), + KeyBinding::new("alt-backspace", DeleteWordBackward, c), + KeyBinding::new("alt-delete", DeleteWordForward, c), + KeyBinding::new("cmd-backspace", DeleteLineBackward, c), + KeyBinding::new("cmd-delete", DeleteLineForward, c), + KeyBinding::new("enter", InsertParagraph, c), + KeyBinding::new("shift-enter", InsertLineBreak, c), + KeyBinding::new("tab", InsertTab, c), + KeyBinding::new("left", Left, c), + KeyBinding::new("right", Right, c), + KeyBinding::new("up", Up, c), + KeyBinding::new("down", Down, c), + KeyBinding::new("shift-left", SelectLeft, c), + KeyBinding::new("shift-right", SelectRight, c), + KeyBinding::new("shift-up", SelectUp, c), + KeyBinding::new("shift-down", SelectDown, c), + KeyBinding::new("cmd-left", LineStart, c), + KeyBinding::new("home", LineStart, c), + KeyBinding::new("cmd-right", LineEnd, c), + KeyBinding::new("end", LineEnd, c), + KeyBinding::new("cmd-shift-left", SelectLineStart, c), + KeyBinding::new("cmd-shift-right", SelectLineEnd, c), + KeyBinding::new("cmd-a", SelectAll, c), + KeyBinding::new("cmd-b", Bold, c), + KeyBinding::new("cmd-i", Italic, c), + KeyBinding::new("cmd-u", Underline, c), + KeyBinding::new("cmd-c", Copy, c), + KeyBinding::new("cmd-x", Cut, c), + KeyBinding::new("cmd-v", Paste, c), + KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, c), + ]); +} + +/// Where the last paint put each line of the display text. +struct Layout { + bounds: Bounds, + line_height: Pixels, + text: String, + /// Each `\n`-separated line: its first byte, its top, and its shape. + lines: Rc>, +} + +impl Layout { + /// The line holding byte `index`. + fn line_of(&self, index: usize) -> Option<&(usize, Pixels, WrappedLine)> { + self.lines.iter().rev().find(|(start, _, _)| *start <= index) + } + + /// The byte under `position` (window coordinates). + fn index_at(&self, position: Point) -> usize { + let local = position - self.bounds.origin; + if local.y < px(0.) { + return 0; + } + let line = self + .lines + .iter() + .rev() + .find(|(_, top, _)| *top <= local.y) + .or(self.lines.first()); + let Some((start, top, line)) = line else { + return 0; + }; + let at = point(local.x.max(px(0.)), local.y - *top); + match line.closest_index_for_position(at, self.line_height) { + Ok(i) | Err(i) => start + i, + } + } + + /// The top left of the caret at byte `index` (window coordinates). + fn position_of(&self, index: usize) -> Option> { + let (start, top, line) = self.line_of(index)?; + let at = line.position_for_index(index - start, self.line_height)?; + Some(self.bounds.origin + point(at.x, *top + at.y)) + } + + /// Each row of each line as `(byte range, top left, width)`. + fn rows(&self) -> Vec<(Range, Point, Pixels)> { + let mut rows = Vec::new(); + for (start, top, line) in self.lines.iter() { + let layout = &line.unwrapped_layout; + let mut breaks: Vec = line + .wrap_boundaries + .iter() + .map(|b| layout.runs[b.run_ix].glyphs[b.glyph_ix].index) + .collect(); + breaks.insert(0, 0); + breaks.push(line.len()); + for (row, pair) in breaks.windows(2).enumerate() { + let (from, to) = (pair[0], pair[1]); + let x0 = layout.x_for_index(from); + let width = layout.x_for_index(to) - x0; + let y = *top + self.line_height * row as f32; + rows.push((start + from..start + to, self.bounds.origin + point(px(0.), y), width)); + } + } + rows + } +} + +pub struct LexicalEditor { + pub surface: Surface, + focus_handle: FocusHandle, + layout: Option, + selecting: bool, + /// The column a vertical move keeps. + goal_x: Option, + /// The last edit's error. + pub error: Option, +} + +impl LexicalEditor { + pub fn new(playground: Playground, cx: &mut Context) -> Self { + LexicalEditor { + surface: Surface::new(playground), + focus_handle: cx.focus_handle(), + layout: None, + selecting: false, + goal_x: None, + error: None, + } + } + + /// Whether the element has painted, so keys can move the caret. + #[cfg(feature = "e2e")] + pub fn is_laid_out(&self) -> bool { + self.layout.is_some() + } + + /// Runs an edit at the selection, then draws again. + pub fn edit(&mut self, cx: &mut Context, f: impl FnOnce(&mut Playground) -> Outcome) { + let outcome = self.surface.edit(f); + self.done(outcome, cx); + } + + fn done(&mut self, outcome: Outcome, cx: &mut Context) { + self.error = outcome.error; + self.goal_x = None; + cx.emit(EditorEvent::Changed); + cx.notify(); + } + + fn delete(&mut self, unit: Unit, backward: bool, cx: &mut Context) { + let outcome = self.surface.delete(unit, backward); + self.done(outcome, cx); + } + + fn move_to(&mut self, offset: usize, extend: bool, cx: &mut Context) { + self.surface.move_to(offset, extend); + cx.emit(EditorEvent::Changed); + cx.notify(); + } + + fn step(&self, backward: bool) -> usize { + let text = self.surface.display_text(); + let caret = self.surface.caret(); + if backward { + text[..caret].grapheme_indices(true).next_back().map_or(0, |(i, _)| i) + } else { + text[caret..].graphemes(true).next().map_or(caret, |g| caret + g.len()) + } + } + + fn horizontal(&mut self, backward: bool, extend: bool, cx: &mut Context) { + let (range, _) = self.surface.selection(); + let to = if !extend && !range.is_empty() { + if backward { range.start } else { range.end } + } else { + self.step(backward) + }; + self.goal_x = None; + self.move_to(to, extend, cx); + } + + fn vertical(&mut self, up: bool, extend: bool, cx: &mut Context) { + let Some(layout) = &self.layout else { return }; + let caret = self.surface.caret(); + let Some(at) = layout.position_of(caret) else { return }; + let x = *self.goal_x.get_or_insert(at.x); + let y = if up { at.y - layout.line_height * 0.5 } else { at.y + layout.line_height * 1.5 }; + let to = if y < layout.bounds.top() { + 0 + } else { + layout.index_at(point(x, y)) + }; + let goal = self.goal_x; + self.move_to(to, extend, cx); + self.goal_x = goal; + } + + fn line_edge(&mut self, end: bool, extend: bool, cx: &mut Context) { + let caret = self.surface.caret(); + let to = self.layout.as_ref().and_then(|l| { + l.rows() + .into_iter() + .find(|(r, _, _)| r.start <= caret && caret <= r.end) + .map(|(r, _, _)| if end { r.end } else { r.start }) + }); + let text = self.surface.display_text(); + let to = to.unwrap_or(if end { text.len() } else { 0 }); + self.move_to(to, extend, cx); + } + + fn on_mouse_down(&mut self, event: &MouseDownEvent, window: &mut Window, cx: &mut Context) { + window.focus(&self.focus_handle, cx); + let Some(layout) = &self.layout else { return }; + let at = layout.index_at(event.position); + self.selecting = true; + if event.click_count >= 2 { + // A word, as a double click selects it. + let text = &layout.text; + let (start, end) = text + .split_word_bound_indices() + .find(|(i, w)| *i <= at && at <= i + w.len()) + .map_or((at, at), |(i, w)| (i, i + w.len())); + self.move_to(start, false, cx); + self.move_to(end, true, cx); + return; + } + self.move_to(at, event.modifiers.shift, cx); + } + + fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context) { + if self.selecting && let Some(layout) = &self.layout { + let at = layout.index_at(event.position); + self.move_to(at, true, cx); + } + } + + fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut Window, _: &mut Context) { + self.selecting = false; + } + + fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context) { + if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) { + self.edit(cx, |p| p.insert_raw_text(&text)); + } + } + + fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { + let text = self.surface.selected_text(); + if !text.is_empty() { + cx.write_to_clipboard(ClipboardItem::new_string(text)); + } + } + + fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context) { + self.copy(&Copy, window, cx); + self.edit(cx, Playground::remove_text); + } +} + +pub enum EditorEvent { + Changed, +} + +impl EventEmitter for LexicalEditor {} + +impl Focusable for LexicalEditor { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl EntityInputHandler for LexicalEditor { + fn text_for_range( + &mut self, + range: Range, + actual_range: &mut Option>, + _: &mut Window, + _: &mut Context, + ) -> Option { + let (text, actual) = self.surface.text_for_range_utf16(range); + actual_range.replace(actual); + Some(text) + } + + fn selected_text_range( + &mut self, + _: bool, + _: &mut Window, + _: &mut Context, + ) -> Option { + let (range, reversed) = self.surface.selected_range_utf16(); + Some(UTF16Selection { range, reversed }) + } + + fn set_selected_text_range(&mut self, range: Range, _: &mut Window, cx: &mut Context) { + self.surface.set_selected_range_utf16(range); + cx.notify(); + } + + fn text_length_utf16(&mut self, _: &mut Window, _: &mut Context) -> Option { + Some(self.surface.text_length_utf16()) + } + + fn marked_text_range(&self, _: &mut Window, _: &mut Context) -> Option> { + self.surface.marked_range_utf16() + } + + fn unmark_text(&mut self, _: &mut Window, cx: &mut Context) { + if let Some(outcome) = self.surface.unmark() { + self.done(outcome, cx); + } + } + + fn replace_text_in_range( + &mut self, + range: Option>, + text: &str, + _: &mut Window, + cx: &mut Context, + ) { + let outcome = self.surface.replace_text_utf16(range, text); + self.done(outcome, cx); + } + + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + text: &str, + selected: Option>, + _: &mut Window, + cx: &mut Context, + ) { + self.surface.replace_and_mark_utf16(range, text, selected); + cx.notify(); + } + + fn bounds_for_range( + &mut self, + range: Range, + _: Bounds, + _: &mut Window, + _: &mut Context, + ) -> Option> { + let layout = self.layout.as_ref()?; + let text = &layout.text; + let start = lexical_playground_core::byte_of_utf16(text, range.start); + let at = layout.position_of(start)?; + Some(Bounds::new(at, size(px(2.), layout.line_height))) + } + + fn character_index_for_point( + &mut self, + position: Point, + _: &mut Window, + _: &mut Context, + ) -> Option { + let layout = self.layout.as_ref()?; + let index = layout.index_at(position); + Some(lexical_playground_core::utf16_of_byte(&layout.text, index)) + } +} + +impl Render for LexicalEditor { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .id("lexical-editor") + .key_context(CONTEXT) + .track_focus(&self.focus_handle) + .cursor(CursorStyle::IBeam) + .size_full() + .overflow_y_scroll() + .on_action(cx.listener(|this, _: &Backspace, _, cx| this.delete(Unit::Character, true, cx))) + .on_action(cx.listener(|this, _: &Delete, _, cx| this.delete(Unit::Character, false, cx))) + .on_action(cx.listener(|this, _: &DeleteWordBackward, _, cx| this.delete(Unit::Word, true, cx))) + .on_action(cx.listener(|this, _: &DeleteWordForward, _, cx| this.delete(Unit::Word, false, cx))) + .on_action(cx.listener(|this, _: &DeleteLineBackward, _, cx| this.delete(Unit::Line, true, cx))) + .on_action(cx.listener(|this, _: &DeleteLineForward, _, cx| this.delete(Unit::Line, false, cx))) + .on_action(cx.listener(|this, _: &InsertParagraph, _, cx| this.edit(cx, Playground::insert_paragraph))) + .on_action(cx.listener(|this, _: &InsertLineBreak, _, cx| this.edit(cx, Playground::insert_line_break))) + .on_action(cx.listener(|this, _: &InsertTab, _, cx| this.edit(cx, |p| p.insert_raw_text("\t")))) + .on_action(cx.listener(|this, _: &Left, _, cx| this.horizontal(true, false, cx))) + .on_action(cx.listener(|this, _: &Right, _, cx| this.horizontal(false, false, cx))) + .on_action(cx.listener(|this, _: &SelectLeft, _, cx| this.horizontal(true, true, cx))) + .on_action(cx.listener(|this, _: &SelectRight, _, cx| this.horizontal(false, true, cx))) + .on_action(cx.listener(|this, _: &Up, _, cx| this.vertical(true, false, cx))) + .on_action(cx.listener(|this, _: &Down, _, cx| this.vertical(false, false, cx))) + .on_action(cx.listener(|this, _: &SelectUp, _, cx| this.vertical(true, true, cx))) + .on_action(cx.listener(|this, _: &SelectDown, _, cx| this.vertical(false, true, cx))) + .on_action(cx.listener(|this, _: &LineStart, _, cx| this.line_edge(false, false, cx))) + .on_action(cx.listener(|this, _: &LineEnd, _, cx| this.line_edge(true, false, cx))) + .on_action(cx.listener(|this, _: &SelectLineStart, _, cx| this.line_edge(false, true, cx))) + .on_action(cx.listener(|this, _: &SelectLineEnd, _, cx| this.line_edge(true, true, cx))) + .on_action(cx.listener(|this, _: &SelectAll, _, cx| this.edit(cx, Playground::select_all))) + .on_action(cx.listener(|this, _: &Bold, _, cx| this.edit(cx, |p| p.format_text("bold")))) + .on_action(cx.listener(|this, _: &Italic, _, cx| this.edit(cx, |p| p.format_text("italic")))) + .on_action(cx.listener(|this, _: &Underline, _, cx| this.edit(cx, |p| p.format_text("underline")))) + .on_action(cx.listener(Self::copy)) + .on_action(cx.listener(Self::cut)) + .on_action(cx.listener(Self::paste)) + .on_action(cx.listener(|_, _: &ShowCharacterPalette, window, _| window.show_character_palette())) + .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) + .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) + .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up)) + .on_mouse_move(cx.listener(Self::on_mouse_move)) + .child(EditorElement { editor: cx.entity() }) + } +} + +/// The text runs to shape: each run's format as a font, colors and lines. +fn text_runs(surface: &Surface, base: &TextStyle, accent: Hsla) -> Vec { + let base_font = base.font(); + surface + .runs() + .into_iter() + .map(|run| { + let has = |name| run.format & format_flag(name) != 0; + let mut font = if has("code") { font("Menlo") } else { base_font.clone() }; + if has("bold") { + font.weight = FontWeight::BOLD; + } + if has("italic") { + font.style = FontStyle::Italic; + } + let line = |on: bool| on.then_some(px(1.)); + TextRun { + len: run.range.len(), + font, + color: if has("subscript") || has("superscript") { accent } else { base.color }, + background_color: if has("highlight") { + Some(hsla(48. / 360., 0.96, 0.75, 1.)) + } else if has("code") { + Some(base.color.opacity(0.08)) + } else { + None + }, + underline: (has("underline") || run.composing).then(|| UnderlineStyle { + thickness: px(1.), + color: Some(if run.composing { accent } else { base.color }), + wavy: false, + }), + strikethrough: line(has("strikethrough")).map(|thickness| StrikethroughStyle { + thickness, + color: Some(base.color), + }), + } + }) + .collect() +} + +struct EditorElement { + editor: Entity, +} + +struct Prepaint { + lines: Rc>, + selection: Vec, + caret: Option, +} + +impl IntoElement for EditorElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +/// Shapes `text` at `width`: each line's first byte, top and shape. +fn shape( + window: &mut Window, + text: &str, + runs: &[TextRun], + font_size: Pixels, + line_height: Pixels, + width: Option, +) -> Vec<(usize, Pixels, WrappedLine)> { + let shaped = window + .text_system() + .shape_text(SharedString::from(text.to_owned()), font_size, runs, width, None) + .unwrap_or_default(); + let mut out = Vec::new(); + let (mut start, mut top) = (0, px(0.)); + for line in shaped { + let rows = line.wrap_boundaries.len() + 1; + let len = line.len(); + out.push((start, top, line)); + start += len + 1; + top += line_height * rows as f32; + } + out +} + +impl Element for EditorElement { + type RequestLayoutState = (); + type PrepaintState = Prepaint; + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, ()) { + let editor = self.editor.read(cx); + let text = editor.surface.display_text(); + let style = window.text_style(); + let runs = text_runs(&editor.surface, &style, cx.theme().primary); + let font_size = style.font_size.to_pixels(window.rem_size()); + let line_height = window.line_height(); + let mut layout_style = Style::default(); + layout_style.size.width = relative(1.).into(); + let id = window.request_measured_layout(layout_style, move |known, available, window, _| { + let width = known.width.or(match available.width { + AvailableSpace::Definite(w) => Some(w), + _ => None, + }); + let lines = shape(window, &text, &runs, font_size, line_height, width); + let height = lines.last().map_or(line_height, |(_, top, line)| { + *top + line_height * (line.wrap_boundaries.len() + 1) as f32 + }); + size(width.unwrap_or(px(0.)), height) + }); + (id, ()) + } + + fn prepaint( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + bounds: Bounds, + _: &mut (), + window: &mut Window, + cx: &mut App, + ) -> Prepaint { + let editor = self.editor.read(cx); + let text = editor.surface.display_text(); + let style = window.text_style(); + let accent = cx.theme().primary; + let runs = text_runs(&editor.surface, &style, accent); + let font_size = style.font_size.to_pixels(window.rem_size()); + let line_height = window.line_height(); + let lines = Rc::new(shape( + window, + &text, + &runs, + font_size, + line_height, + Some(bounds.size.width), + )); + let layout = Layout { + bounds, + line_height, + text: text.clone(), + lines: lines.clone(), + }; + let (range, _) = editor.surface.selection(); + let mut selection = Vec::new(); + if !range.is_empty() { + for (row, origin, width) in layout.rows() { + if range.end < row.start || range.start > row.end { + continue; + } + let layout_x = |i: usize| { + layout + .position_of(i) + .map_or(origin.x, |p| p.x) + }; + let x0 = if range.start <= row.start { origin.x } else { layout_x(range.start) }; + // A selection that goes on past the row covers its end, and + // past a line's end, the newline. + let x1 = if range.end > row.end { + origin.x + width + px(6.) + } else { + layout_x(range.end) + }; + if x1 > x0 { + selection.push(fill( + Bounds::from_corners(point(x0, origin.y), point(x1, origin.y + line_height)), + cx.theme().selection, + )); + } + } + } + let caret = range + .is_empty() + .then(|| layout.position_of(editor.surface.caret())) + .flatten() + .map(|at| fill(Bounds::new(at, size(px(2.), line_height)), cx.theme().caret)); + self.editor.update(cx, |editor, _| editor.layout = Some(layout)); + Prepaint { + lines, + selection, + caret, + } + } + + fn paint( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&InspectorElementId>, + bounds: Bounds, + _: &mut (), + prepaint: &mut Prepaint, + window: &mut Window, + cx: &mut App, + ) { + let focus_handle = self.editor.read(cx).focus_handle.clone(); + window.handle_input( + &focus_handle, + ElementInputHandler::new(bounds, self.editor.clone()), + cx, + ); + for quad in prepaint.selection.drain(..) { + window.paint_quad(quad); + } + let line_height = window.line_height(); + for (_, top, line) in prepaint.lines.iter() { + let origin = bounds.origin + point(px(0.), *top); + line.paint_background(origin, line_height, TextAlign::Left, Some(bounds), window, cx) + .ok(); + line.paint(origin, line_height, TextAlign::Left, Some(bounds), window, cx) + .ok(); + } + if focus_handle.is_focused(window) + && let Some(caret) = prepaint.caret.take() + { + window.paint_quad(caret); + } + } +} diff --git a/rust/apps/lexical-playground-gpui/src/main.rs b/rust/apps/lexical-playground-gpui/src/main.rs new file mode 100644 index 00000000000..e57c8a9cbcd --- /dev/null +++ b/rust/apps/lexical-playground-gpui/src/main.rs @@ -0,0 +1,62 @@ +//! The Lexical playground in GPUI: a document in `lexical-editor`, drawn +//! and edited in a native window. `editor.rs` is the editor; `app.rs`, the +//! window around it. + +mod app; +#[cfg(feature = "e2e")] +mod e2e; +mod editor; + +use gpui_kit::*; + +actions!(lexical_playground, [Quit, Hide, HideOthers, ShowAll]); + +/// The macOS app menu, as wevm/zero's zbugs-gpui has it: GPUI has no menu +/// bar of its own. +fn app_menu(cx: &mut App) { + cx.on_action(|_: &Quit, cx| cx.quit()); + cx.on_action(|_: &Hide, cx| cx.hide()); + cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps()); + cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps()); + cx.bind_keys([ + KeyBinding::new("cmd-q", Quit, None), + KeyBinding::new("cmd-h", Hide, None), + KeyBinding::new("alt-cmd-h", HideOthers, None), + ]); + cx.set_menus([Menu::new("Lexical Playground").items([ + MenuItem::os_submenu("Services", SystemMenuType::Services), + MenuItem::separator(), + MenuItem::action("Hide Lexical Playground", Hide), + MenuItem::action("Hide Others", HideOthers), + MenuItem::action("Show All", ShowAll), + MenuItem::separator(), + MenuItem::action("Quit Lexical Playground", Quit), + ])]); +} + +fn main() { + gpui_kit::application() + .with_assets(gpui_kit::assets::Assets) + .run(|cx| { + gpui_kit::init(cx); + editor::bind_keys(cx); + app_menu(cx); + let bounds = Bounds::centered(None, size(px(1100.), px(720.)), cx); + let (_window, _app) = gpui_kit::open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + titlebar: Some(TitlebarOptions { + title: Some("Lexical Playground (GPUI)".into()), + ..Default::default() + }), + ..Default::default() + }, + cx, + |window, cx| cx.new(|cx| app::PlaygroundApp::new(window, cx)), + ) + .expect("the window opens"); + cx.activate(true); + #[cfg(feature = "e2e")] + e2e::run(_window, _app, cx); + }); +} diff --git a/rust/apps/lexical-playground-rune/README.md b/rust/apps/lexical-playground-rune/README.md index 42e52b5a5df..32402d9b6de 100644 --- a/rust/apps/lexical-playground-rune/README.md +++ b/rust/apps/lexical-playground-rune/README.md @@ -58,4 +58,4 @@ bundle stays, to open from Finder. Like every Rune app, it renders headless too: `cargo run -- --snap playground.png --size 1100x720 --scene typed` (and -`--tree`, `--script`; see Rune's README). The scenes are `typed` and `json`. +`--tree`, `--script`; see Rune's README). The scenes are `typed`, `json` and `parity` (Replay vectors, run). diff --git a/rust/apps/lexical-playground-rune/src/main.rs b/rust/apps/lexical-playground-rune/src/main.rs index 856dac99e78..fc23773a78e 100644 --- a/rust/apps/lexical-playground-rune/src/main.rs +++ b/rust/apps/lexical-playground-rune/src/main.rs @@ -229,6 +229,10 @@ fn main() { typed(s); s.panel = Panel::Json; }), + ("parity", |s| { + s.panel = Panel::Parity; + s.report = Some(view::replay()); + }), ], // Inline code (`Mark::Code`) draws in the mono slot: the system's // monospaced face (SF Mono on macOS). diff --git a/rust/apps/lexical-playground-rune/src/view.rs b/rust/apps/lexical-playground-rune/src/view.rs index c25fde2a5d3..d9fc079f881 100644 --- a/rust/apps/lexical-playground-rune/src/view.rs +++ b/rust/apps/lexical-playground-rune/src/view.rs @@ -34,7 +34,7 @@ pub fn view(s: &State, cx: &mut Cx) -> El { .w_full() .h_full() .fill(k.c.page) - .child(toolbar(s, k)) + .child(toolbar(s, k, cx.chrome_insets().leading)) .child( row() .w_full() @@ -47,8 +47,15 @@ pub fn view(s: &State, cx: &mut Cx) -> El { .child(status_bar(s, k)) } -fn toolbar(s: &State, k: &Kit) -> El { - let mut bar = row().w_full().gap(6.0).padding(10.0).fill(k.c.bg_base); +/// `leading`: the macOS traffic lights' width, where the toolbar shares the +/// titlebar's band. +fn toolbar(s: &State, k: &Kit, leading: f32) -> El { + let mut bar = row() + .w_full() + .gap(6.0) + .padding(10.0) + .pl(if leading > 0.0 { leading + 12.0 } else { 10.0 }) + .fill(k.c.bg_base); bar = bar.child(text("Lexical").font(k.ty.body_medium).color(k.c.text_base)); for (name, label) in FORMATS { let on = s.playground.has_format(name); @@ -88,7 +95,11 @@ fn toolbar(s: &State, k: &Kit) -> El { } fn editor(s: &State, k: &Kit) -> El { + // The field's own ink is white, for a dark window; the kit's follows + // the system's appearance. let field = text_input(&s.edit, TextStyle::new(16.0, 400.0)) + .color(k.c.text_base) + .caret(k.c.text_base, k.selection()) .key("editor") .w_full() .grow(1.0) @@ -144,6 +155,7 @@ fn mono(s: &str, k: &Kit) -> El { .font(k.ty.mono) .color(k.c.text_base) .selectable(true) + .wrap() .w_full() } @@ -160,6 +172,7 @@ fn parity(s: &State, k: &Kit) -> El { .font(k.ty.small) .color(k.c.text_muted) .selectable(true) + .wrap() .w_full(), ) .child( @@ -193,7 +206,7 @@ fn parity(s: &State, k: &Kit) -> El { out } -fn replay() -> (String, bool) { +pub fn replay() -> (String, bool) { replay_vectors().unwrap_or_else(|e| (e, false)) } From 3150cecf8c349d61c52c03f6a69aca33afc69b7e Mon Sep 17 00:00:00 2001 From: Amp Date: Sat, 14 Nov 2026 00:59:52 +0000 Subject: [PATCH 04/10] chore(rust): format the GPUI playground ## 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 --- rust/apps/lexical-playground-gpui/src/app.rs | 60 +++-- rust/apps/lexical-playground-gpui/src/e2e.rs | 39 ++-- .../lexical-playground-gpui/src/editor.rs | 206 +++++++++++++----- 3 files changed, 223 insertions(+), 82 deletions(-) diff --git a/rust/apps/lexical-playground-gpui/src/app.rs b/rust/apps/lexical-playground-gpui/src/app.rs index d38986e6ab9..30073f678fc 100644 --- a/rust/apps/lexical-playground-gpui/src/app.rs +++ b/rust/apps/lexical-playground-gpui/src/app.rs @@ -78,7 +78,12 @@ impl PlaygroundApp { .py_2() .border_b_1() .border_color(cx.theme().border) - .child(div().font_weight(FontWeight::SEMIBOLD).mr_2().child("Lexical")); + .child( + div() + .font_weight(FontWeight::SEMIBOLD) + .mr_2() + .child("Lexical"), + ); for (name, label) in FORMATS { let on = playground.has_format(name); bar = bar.child( @@ -179,11 +184,14 @@ impl PlaygroundApp { let mut out = v_flex() .gap_2() .child( - div().text_sm().text_color(cx.theme().muted_foreground).child( - "The editor here is lexical-editor. Replay runs the conformance vectors \ + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child( + "The editor here is lexical-editor. Replay runs the conformance vectors \ that JS Lexical recorded; Export writes this session as a vector that JS \ replays step by step, in packages/lexical-conformance:", - ), + ), ) .child( div() @@ -194,25 +202,36 @@ impl PlaygroundApp { .child( h_flex() .gap_2() - .child(Button::new("replay").small().label("Replay vectors").on_click( - cx.listener(|this, _, _, cx| { - this.report = Some(replay_vectors().unwrap_or_else(|e| (e, false))); - cx.notify(); - }), - )) - .child(Button::new("export").small().label("Export session").on_click( - cx.listener(|this, _, _, cx| { - this.status = Some(export(&this.editor.read(cx).surface.playground)); - cx.notify(); - }), - )), + .child( + Button::new("replay") + .small() + .label("Replay vectors") + .on_click(cx.listener(|this, _, _, cx| { + this.report = Some(replay_vectors().unwrap_or_else(|e| (e, false))); + cx.notify(); + })), + ) + .child( + Button::new("export") + .small() + .label("Export session") + .on_click(cx.listener(|this, _, _, cx| { + this.status = + Some(export(&this.editor.read(cx).surface.playground)); + cx.notify(); + })), + ), ); if let Some((report, ok)) = &self.report { out = out .child( div() .font_weight(FontWeight::SEMIBOLD) - .text_color(if *ok { cx.theme().success } else { cx.theme().danger }) + .text_color(if *ok { + cx.theme().success + } else { + cx.theme().danger + }) .child(if *ok { "Every replayed case matches JS." } else { @@ -239,7 +258,12 @@ impl PlaygroundApp { cx.theme().muted_foreground, ), }; - div().px_4().py_2().text_xs().text_color(color).child(message) + div() + .px_4() + .py_2() + .text_xs() + .text_color(color) + .child(message) } } diff --git a/rust/apps/lexical-playground-gpui/src/e2e.rs b/rust/apps/lexical-playground-gpui/src/e2e.rs index 6683afb31ab..062cbd7c346 100644 --- a/rust/apps/lexical-playground-gpui/src/e2e.rs +++ b/rust/apps/lexical-playground-gpui/src/e2e.rs @@ -51,17 +51,16 @@ pub fn run(window: AnyWindowHandle, app: Entity, cx: &mut App) { } async fn drive(window: AnyWindowHandle, app: &Entity, cx: &mut AsyncApp) -> Step { - let editor = app - .read_with(cx, |app, _| app.editor().clone()); + let editor = app.read_with(cx, |app, _| app.editor().clone()); // Keys move the caret through the last paint's layout. let deadline = Instant::now() + Duration::from_secs(30); - while !editor - .read_with(cx, |e, _| e.is_laid_out()) - { + while !editor.read_with(cx, |e, _| e.is_laid_out()) { if Instant::now() > deadline { return Err("timed out waiting for the first paint".into()); } - cx.background_executor().timer(Duration::from_millis(50)).await; + cx.background_executor() + .timer(Duration::from_millis(50)) + .await; } let line = |cx: &mut AsyncApp, n: usize| -> Result { @@ -74,10 +73,10 @@ async fn drive(window: AnyWindowHandle, app: &Entity, cx: &mut As cx.update_window(window, |_, window, cx| $input(window, cx)) .map_err(|e| e.to_string())?; // Actions run when the window next handles events. - cx.background_executor().timer(Duration::from_millis(30)).await; - if let Some(error) = editor - .read_with(cx, |e, _| e.error.clone()) - { + cx.background_executor() + .timer(Duration::from_millis(30)) + .await; + if let Some(error) = editor.read_with(cx, |e, _| e.error.clone()) { return Err(format!("{}: the edit failed: {error}", $what)); } let check: Step = $check(&mut *cx); @@ -106,8 +105,7 @@ async fn drive(window: AnyWindowHandle, app: &Entity, cx: &mut As let bold = |want: bool| { let editor = editor.clone(); move |cx: &mut AsyncApp| -> Step { - let got = editor - .read_with(cx, |e, _| e.surface.playground.has_format("bold")); + let got = editor.read_with(cx, |e, _| e.surface.playground.has_format("bold")); if got == want { Ok(()) } else { @@ -120,7 +118,11 @@ async fn drive(window: AnyWindowHandle, app: &Entity, cx: &mut As step!("End of the line", key(Box::new(LineEnd)), expect(0, FIRST)); step!("Return", key(Box::new(InsertParagraph)), expect(1, "")); step!("⌘B", key(Box::new(Bold)), bold(true)); - step!("Typing bold", typed("Typed in GPUI"), expect(1, "Typed in GPUI")); + step!( + "Typing bold", + typed("Typed in GPUI"), + expect(1, "Typed in GPUI") + ); step!("⌘B again", key(Box::new(Bold)), bold(false)); step!( "Typing plain", @@ -149,7 +151,11 @@ async fn drive(window: AnyWindowHandle, app: &Entity, cx: &mut As typed("日本"), expect(1, "Typed in GPUI, then plain 日本") ); - step!("Typing an emoji", typed("👍🏽"), expect(1, "Typed in GPUI, then plain 日本👍🏽")); + step!( + "Typing an emoji", + typed("👍🏽"), + expect(1, "Typed in GPUI, then plain 日本👍🏽") + ); step!( "⌫ over the emoji", key(Box::new(Backspace)), @@ -160,7 +166,10 @@ async fn drive(window: AnyWindowHandle, app: &Entity, cx: &mut As step!( "The third paragraph is untouched", |_: &mut Window, _: &mut App| {}, - expect(3, "Type, delete, press Enter, or select text and make it bold.") + expect( + 3, + "Type, delete, press Enter, or select text and make it bold." + ) ); Ok(()) } diff --git a/rust/apps/lexical-playground-gpui/src/editor.rs b/rust/apps/lexical-playground-gpui/src/editor.rs index a2bec2ff52b..56367785fec 100644 --- a/rust/apps/lexical-playground-gpui/src/editor.rs +++ b/rust/apps/lexical-playground-gpui/src/editor.rs @@ -103,7 +103,10 @@ struct Layout { impl Layout { /// The line holding byte `index`. fn line_of(&self, index: usize) -> Option<&(usize, Pixels, WrappedLine)> { - self.lines.iter().rev().find(|(start, _, _)| *start <= index) + self.lines + .iter() + .rev() + .find(|(start, _, _)| *start <= index) } /// The byte under `position` (window coordinates). @@ -151,7 +154,11 @@ impl Layout { let x0 = layout.x_for_index(from); let width = layout.x_for_index(to) - x0; let y = *top + self.line_height * row as f32; - rows.push((start + from..start + to, self.bounds.origin + point(px(0.), y), width)); + rows.push(( + start + from..start + to, + self.bounds.origin + point(px(0.), y), + width, + )); } } rows @@ -215,9 +222,15 @@ impl LexicalEditor { let text = self.surface.display_text(); let caret = self.surface.caret(); if backward { - text[..caret].grapheme_indices(true).next_back().map_or(0, |(i, _)| i) + text[..caret] + .grapheme_indices(true) + .next_back() + .map_or(0, |(i, _)| i) } else { - text[caret..].graphemes(true).next().map_or(caret, |g| caret + g.len()) + text[caret..] + .graphemes(true) + .next() + .map_or(caret, |g| caret + g.len()) } } @@ -235,9 +248,15 @@ impl LexicalEditor { fn vertical(&mut self, up: bool, extend: bool, cx: &mut Context) { let Some(layout) = &self.layout else { return }; let caret = self.surface.caret(); - let Some(at) = layout.position_of(caret) else { return }; + let Some(at) = layout.position_of(caret) else { + return; + }; let x = *self.goal_x.get_or_insert(at.x); - let y = if up { at.y - layout.line_height * 0.5 } else { at.y + layout.line_height * 1.5 }; + let y = if up { + at.y - layout.line_height * 0.5 + } else { + at.y + layout.line_height * 1.5 + }; let to = if y < layout.bounds.top() { 0 } else { @@ -261,7 +280,12 @@ impl LexicalEditor { self.move_to(to, extend, cx); } - fn on_mouse_down(&mut self, event: &MouseDownEvent, window: &mut Window, cx: &mut Context) { + fn on_mouse_down( + &mut self, + event: &MouseDownEvent, + window: &mut Window, + cx: &mut Context, + ) { window.focus(&self.focus_handle, cx); let Some(layout) = &self.layout else { return }; let at = layout.index_at(event.position); @@ -281,7 +305,9 @@ impl LexicalEditor { } fn on_mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, cx: &mut Context) { - if self.selecting && let Some(layout) = &self.layout { + if self.selecting + && let Some(layout) = &self.layout + { let at = layout.index_at(event.position); self.move_to(at, true, cx); } @@ -345,7 +371,12 @@ impl EntityInputHandler for LexicalEditor { Some(UTF16Selection { range, reversed }) } - fn set_selected_text_range(&mut self, range: Range, _: &mut Window, cx: &mut Context) { + fn set_selected_text_range( + &mut self, + range: Range, + _: &mut Window, + cx: &mut Context, + ) { self.surface.set_selected_range_utf16(range); cx.notify(); } @@ -422,15 +453,43 @@ impl Render for LexicalEditor { .cursor(CursorStyle::IBeam) .size_full() .overflow_y_scroll() - .on_action(cx.listener(|this, _: &Backspace, _, cx| this.delete(Unit::Character, true, cx))) - .on_action(cx.listener(|this, _: &Delete, _, cx| this.delete(Unit::Character, false, cx))) - .on_action(cx.listener(|this, _: &DeleteWordBackward, _, cx| this.delete(Unit::Word, true, cx))) - .on_action(cx.listener(|this, _: &DeleteWordForward, _, cx| this.delete(Unit::Word, false, cx))) - .on_action(cx.listener(|this, _: &DeleteLineBackward, _, cx| this.delete(Unit::Line, true, cx))) - .on_action(cx.listener(|this, _: &DeleteLineForward, _, cx| this.delete(Unit::Line, false, cx))) - .on_action(cx.listener(|this, _: &InsertParagraph, _, cx| this.edit(cx, Playground::insert_paragraph))) - .on_action(cx.listener(|this, _: &InsertLineBreak, _, cx| this.edit(cx, Playground::insert_line_break))) - .on_action(cx.listener(|this, _: &InsertTab, _, cx| this.edit(cx, |p| p.insert_raw_text("\t")))) + .on_action( + cx.listener(|this, _: &Backspace, _, cx| this.delete(Unit::Character, true, cx)), + ) + .on_action( + cx.listener(|this, _: &Delete, _, cx| this.delete(Unit::Character, false, cx)), + ) + .on_action( + cx.listener(|this, _: &DeleteWordBackward, _, cx| { + this.delete(Unit::Word, true, cx) + }), + ) + .on_action( + cx.listener(|this, _: &DeleteWordForward, _, cx| { + this.delete(Unit::Word, false, cx) + }), + ) + .on_action( + cx.listener(|this, _: &DeleteLineBackward, _, cx| { + this.delete(Unit::Line, true, cx) + }), + ) + .on_action( + cx.listener(|this, _: &DeleteLineForward, _, cx| { + this.delete(Unit::Line, false, cx) + }), + ) + .on_action(cx.listener(|this, _: &InsertParagraph, _, cx| { + this.edit(cx, Playground::insert_paragraph) + })) + .on_action(cx.listener(|this, _: &InsertLineBreak, _, cx| { + this.edit(cx, Playground::insert_line_break) + })) + .on_action( + cx.listener(|this, _: &InsertTab, _, cx| { + this.edit(cx, |p| p.insert_raw_text("\t")) + }), + ) .on_action(cx.listener(|this, _: &Left, _, cx| this.horizontal(true, false, cx))) .on_action(cx.listener(|this, _: &Right, _, cx| this.horizontal(false, false, cx))) .on_action(cx.listener(|this, _: &SelectLeft, _, cx| this.horizontal(true, true, cx))) @@ -441,21 +500,37 @@ impl Render for LexicalEditor { .on_action(cx.listener(|this, _: &SelectDown, _, cx| this.vertical(false, true, cx))) .on_action(cx.listener(|this, _: &LineStart, _, cx| this.line_edge(false, false, cx))) .on_action(cx.listener(|this, _: &LineEnd, _, cx| this.line_edge(true, false, cx))) - .on_action(cx.listener(|this, _: &SelectLineStart, _, cx| this.line_edge(false, true, cx))) + .on_action( + cx.listener(|this, _: &SelectLineStart, _, cx| this.line_edge(false, true, cx)), + ) .on_action(cx.listener(|this, _: &SelectLineEnd, _, cx| this.line_edge(true, true, cx))) - .on_action(cx.listener(|this, _: &SelectAll, _, cx| this.edit(cx, Playground::select_all))) - .on_action(cx.listener(|this, _: &Bold, _, cx| this.edit(cx, |p| p.format_text("bold")))) - .on_action(cx.listener(|this, _: &Italic, _, cx| this.edit(cx, |p| p.format_text("italic")))) - .on_action(cx.listener(|this, _: &Underline, _, cx| this.edit(cx, |p| p.format_text("underline")))) + .on_action( + cx.listener(|this, _: &SelectAll, _, cx| this.edit(cx, Playground::select_all)), + ) + .on_action( + cx.listener(|this, _: &Bold, _, cx| this.edit(cx, |p| p.format_text("bold"))), + ) + .on_action( + cx.listener(|this, _: &Italic, _, cx| this.edit(cx, |p| p.format_text("italic"))), + ) + .on_action(cx.listener(|this, _: &Underline, _, cx| { + this.edit(cx, |p| p.format_text("underline")) + })) .on_action(cx.listener(Self::copy)) .on_action(cx.listener(Self::cut)) .on_action(cx.listener(Self::paste)) - .on_action(cx.listener(|_, _: &ShowCharacterPalette, window, _| window.show_character_palette())) + .on_action( + cx.listener(|_, _: &ShowCharacterPalette, window, _| { + window.show_character_palette() + }), + ) .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up)) .on_mouse_move(cx.listener(Self::on_mouse_move)) - .child(EditorElement { editor: cx.entity() }) + .child(EditorElement { + editor: cx.entity(), + }) } } @@ -467,7 +542,11 @@ fn text_runs(surface: &Surface, base: &TextStyle, accent: Hsla) -> Vec .into_iter() .map(|run| { let has = |name| run.format & format_flag(name) != 0; - let mut font = if has("code") { font("Menlo") } else { base_font.clone() }; + let mut font = if has("code") { + font("Menlo") + } else { + base_font.clone() + }; if has("bold") { font.weight = FontWeight::BOLD; } @@ -478,7 +557,11 @@ fn text_runs(surface: &Surface, base: &TextStyle, accent: Hsla) -> Vec TextRun { len: run.range.len(), font, - color: if has("subscript") || has("superscript") { accent } else { base.color }, + color: if has("subscript") || has("superscript") { + accent + } else { + base.color + }, background_color: if has("highlight") { Some(hsla(48. / 360., 0.96, 0.75, 1.)) } else if has("code") { @@ -529,7 +612,13 @@ fn shape( ) -> Vec<(usize, Pixels, WrappedLine)> { let shaped = window .text_system() - .shape_text(SharedString::from(text.to_owned()), font_size, runs, width, None) + .shape_text( + SharedString::from(text.to_owned()), + font_size, + runs, + width, + None, + ) .unwrap_or_default(); let mut out = Vec::new(); let (mut start, mut top) = (0, px(0.)); @@ -570,17 +659,18 @@ impl Element for EditorElement { let line_height = window.line_height(); let mut layout_style = Style::default(); layout_style.size.width = relative(1.).into(); - let id = window.request_measured_layout(layout_style, move |known, available, window, _| { - let width = known.width.or(match available.width { - AvailableSpace::Definite(w) => Some(w), - _ => None, + let id = + window.request_measured_layout(layout_style, move |known, available, window, _| { + let width = known.width.or(match available.width { + AvailableSpace::Definite(w) => Some(w), + _ => None, + }); + let lines = shape(window, &text, &runs, font_size, line_height, width); + let height = lines.last().map_or(line_height, |(_, top, line)| { + *top + line_height * (line.wrap_boundaries.len() + 1) as f32 + }); + size(width.unwrap_or(px(0.)), height) }); - let lines = shape(window, &text, &runs, font_size, line_height, width); - let height = lines.last().map_or(line_height, |(_, top, line)| { - *top + line_height * (line.wrap_boundaries.len() + 1) as f32 - }); - size(width.unwrap_or(px(0.)), height) - }); (id, ()) } @@ -621,12 +711,12 @@ impl Element for EditorElement { if range.end < row.start || range.start > row.end { continue; } - let layout_x = |i: usize| { - layout - .position_of(i) - .map_or(origin.x, |p| p.x) + let layout_x = |i: usize| layout.position_of(i).map_or(origin.x, |p| p.x); + let x0 = if range.start <= row.start { + origin.x + } else { + layout_x(range.start) }; - let x0 = if range.start <= row.start { origin.x } else { layout_x(range.start) }; // A selection that goes on past the row covers its end, and // past a line's end, the newline. let x1 = if range.end > row.end { @@ -636,7 +726,10 @@ impl Element for EditorElement { }; if x1 > x0 { selection.push(fill( - Bounds::from_corners(point(x0, origin.y), point(x1, origin.y + line_height)), + Bounds::from_corners( + point(x0, origin.y), + point(x1, origin.y + line_height), + ), cx.theme().selection, )); } @@ -647,7 +740,8 @@ impl Element for EditorElement { .then(|| layout.position_of(editor.surface.caret())) .flatten() .map(|at| fill(Bounds::new(at, size(px(2.), line_height)), cx.theme().caret)); - self.editor.update(cx, |editor, _| editor.layout = Some(layout)); + self.editor + .update(cx, |editor, _| editor.layout = Some(layout)); Prepaint { lines, selection, @@ -677,10 +771,24 @@ impl Element for EditorElement { let line_height = window.line_height(); for (_, top, line) in prepaint.lines.iter() { let origin = bounds.origin + point(px(0.), *top); - line.paint_background(origin, line_height, TextAlign::Left, Some(bounds), window, cx) - .ok(); - line.paint(origin, line_height, TextAlign::Left, Some(bounds), window, cx) - .ok(); + line.paint_background( + origin, + line_height, + TextAlign::Left, + Some(bounds), + window, + cx, + ) + .ok(); + line.paint( + origin, + line_height, + TextAlign::Left, + Some(bounds), + window, + cx, + ) + .ok(); } if focus_handle.is_focused(window) && let Some(caret) = prepaint.caret.take() From 423f208f7ac312fbe85eaf7fc88e0797ee58436a Mon Sep 17 00:00:00 2001 From: Amp Date: Sat, 14 Nov 2026 01:13:42 +0000 Subject: [PATCH 05/10] feat(rust): app icons and a blinking caret for the desktop playgrounds ## 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 --- .../AppIcon.icon/Assets/Accent.svg | 1 + .../AppIcon.icon/Assets/Frame.svg | 1 + .../AppIcon.icon/Assets/Text.svg | 1 + .../AppIcon.icon/Assets/gpui.svg | 9 ++ .../AppIcon.icon/icon.json | 130 ++++++++++++++++++ rust/apps/lexical-playground-gpui/README.md | 10 +- rust/apps/lexical-playground-gpui/bundle.sh | 27 +++- rust/apps/lexical-playground-gpui/src/app.rs | 11 +- .../lexical-playground-gpui/src/editor.rs | 85 +++++++++++- .../AppIcon.icon/Assets/Accent.svg | 1 + .../AppIcon.icon/Assets/Frame.svg | 1 + .../AppIcon.icon/Assets/Text.svg | 1 + .../AppIcon.icon/Assets/rune.svg | 27 ++++ .../AppIcon.icon/icon.json | 130 ++++++++++++++++++ rust/apps/lexical-playground-rune/README.md | 10 +- rust/apps/lexical-playground-rune/bundle.sh | 27 +++- rust/apps/lexical-playground-rune/src/main.rs | 65 ++++++++- rust/apps/lexical-playground-rune/src/view.rs | 12 +- 18 files changed, 530 insertions(+), 19 deletions(-) create mode 100644 rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/Accent.svg create mode 100644 rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/Frame.svg create mode 100644 rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/Text.svg create mode 100644 rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/gpui.svg create mode 100644 rust/apps/lexical-playground-gpui/AppIcon.icon/icon.json create mode 100644 rust/apps/lexical-playground-rune/AppIcon.icon/Assets/Accent.svg create mode 100644 rust/apps/lexical-playground-rune/AppIcon.icon/Assets/Frame.svg create mode 100644 rust/apps/lexical-playground-rune/AppIcon.icon/Assets/Text.svg create mode 100644 rust/apps/lexical-playground-rune/AppIcon.icon/Assets/rune.svg create mode 100644 rust/apps/lexical-playground-rune/AppIcon.icon/icon.json diff --git a/rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/Accent.svg b/rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/Accent.svg new file mode 100644 index 00000000000..9a3a0fe2a26 --- /dev/null +++ b/rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/Accent.svg @@ -0,0 +1 @@ + diff --git a/rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/Frame.svg b/rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/Frame.svg new file mode 100644 index 00000000000..7bbffd8b097 --- /dev/null +++ b/rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/Frame.svg @@ -0,0 +1 @@ + diff --git a/rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/Text.svg b/rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/Text.svg new file mode 100644 index 00000000000..7b6d55ba6b9 --- /dev/null +++ b/rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/Text.svg @@ -0,0 +1 @@ + diff --git a/rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/gpui.svg b/rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/gpui.svg new file mode 100644 index 00000000000..2ef288b274a --- /dev/null +++ b/rust/apps/lexical-playground-gpui/AppIcon.icon/Assets/gpui.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/rust/apps/lexical-playground-gpui/AppIcon.icon/icon.json b/rust/apps/lexical-playground-gpui/AppIcon.icon/icon.json new file mode 100644 index 00000000000..d699b85c26b --- /dev/null +++ b/rust/apps/lexical-playground-gpui/AppIcon.icon/icon.json @@ -0,0 +1,130 @@ +{ + "fill-specializations": [ + { + "value": { + "solid": "srgb:1.00000,1.00000,1.00000,1.00000" + } + }, + { + "appearance": "dark", + "value": { + "solid": "srgb:0.10980,0.11373,0.12157,1.00000" + } + } + ], + "groups": [ + { + "layers": [ + { + "image-name": "gpui.svg", + "name": "gpui", + "position": { + "scale": 1, + "translation-in-points": [255, 255] + } + } + ], + "shadow": { + "kind": "neutral", + "opacity": 0.5 + }, + "translucency": { + "enabled": false, + "value": 0.5 + } + }, + { + "layers": [ + { + "fill-specializations": [ + { + "value": { + "solid": "srgb:0.46275,0.71373,1.00000,1.00000" + } + }, + { + "appearance": "dark", + "value": { + "solid": "srgb:0.46275,0.71373,1.00000,1.00000" + } + } + ], + "glass": false, + "image-name": "Accent.svg", + "name": "Accent", + "position": { + "scale": 0.8, + "translation-in-points": [-64.0, -64.0] + } + }, + { + "fill-specializations": [ + { + "value": { + "solid": "srgb:0.00000,0.00000,0.00000,1.00000" + } + }, + { + "appearance": "dark", + "value": { + "solid": "srgb:1.00000,1.00000,1.00000,1.00000" + } + }, + { + "appearance": "tinted", + "value": { + "solid": "srgb:1.00000,1.00000,1.00000,1.00000" + } + } + ], + "glass": false, + "image-name": "Text.svg", + "name": "Text", + "position": { + "scale": 0.8, + "translation-in-points": [-64.0, -64.0] + } + }, + { + "fill-specializations": [ + { + "value": { + "solid": "srgb:0.00000,0.00000,0.00000,1.00000" + } + }, + { + "appearance": "dark", + "value": { + "solid": "srgb:1.00000,1.00000,1.00000,1.00000" + } + }, + { + "appearance": "tinted", + "value": { + "solid": "srgb:1.00000,1.00000,1.00000,1.00000" + } + } + ], + "glass": false, + "image-name": "Frame.svg", + "name": "Frame", + "position": { + "scale": 0.8, + "translation-in-points": [-64.0, -64.0] + } + } + ], + "shadow": { + "kind": "neutral", + "opacity": 0.5 + }, + "translucency": { + "enabled": false, + "value": 0.5 + } + } + ], + "supported-platforms": { + "squares": "shared" + } +} diff --git a/rust/apps/lexical-playground-gpui/README.md b/rust/apps/lexical-playground-gpui/README.md index 9c6553c6645..951c91b1ba5 100644 --- a/rust/apps/lexical-playground-gpui/README.md +++ b/rust/apps/lexical-playground-gpui/README.md @@ -56,8 +56,14 @@ installs that toolchain on the first build. `cargo run` runs the app as an app: `bundle.sh`, Cargo's runner on macOS (`.cargo/config.toml`), makes `Lexical Playground (GPUI).app` beside the -executable and runs the app from inside it, so it shows in the Dock. The -bundle stays, to open from Finder. +executable and runs the app from inside it, so it shows in the Dock with its +icon. The bundle stays, to open from Finder. + +The icon, `AppIcon.icon`, is an Icon Composer document, as wevm/zero's +zbugs-gpui has: the Lexical mark of the Swift playground's icon, with the +GPUI mark in the bottom right corner, in light and dark. `bundle.sh` +compiles it with Xcode's `actool` when it changes. Open it in Icon Composer +to edit it. ## End to end diff --git a/rust/apps/lexical-playground-gpui/bundle.sh b/rust/apps/lexical-playground-gpui/bundle.sh index cdfc37bf1f2..565f0286ae7 100755 --- a/rust/apps/lexical-playground-gpui/bundle.sh +++ b/rust/apps/lexical-playground-gpui/bundle.sh @@ -1,7 +1,9 @@ #!/bin/sh # Cargo's runner on macOS (.cargo/config.toml): runs the playground from # inside an app bundle, `Lexical Playground (GPUI).app` beside the -# executable, so it launches as an app in the Dock and the app switcher. The +# executable, so it launches as an app with its icon (AppIcon.icon: the +# Lexical mark with the GPUI mark in the corner) in the Dock and the app +# switcher, as wevm/zero's zbugs-gpui does. The # bundle stays, to open from Finder or keep in the Dock. Tests and other # executables run as they are. set -eu @@ -9,8 +11,23 @@ exe=$1 shift [ "$(basename "$exe")" = lexical-playground-gpui ] || exec "$exe" "$@" +here=$(cd "$(dirname "$0")" && pwd) +# Absolute: actool takes relative paths from a directory of its own. app="$(cd "$(dirname "$exe")" && pwd)/Lexical Playground (GPUI).app" -mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" +res="$app/Contents/Resources" +mkdir -p "$app/Contents/MacOS" "$res" + +# The icon, through Xcode's actool, when it changed: Assets.car for macOS 26 +# and later, AppIcon.icns for earlier ones. +if [ ! -e "$res/Assets.car" ] || [ -n "$(find "$here/AppIcon.icon" -newer "$res/Assets.car")" ]; then + partial=$(mktemp) + out=$(xcrun actool "$here/AppIcon.icon" --compile "$res" --platform macosx \ + --minimum-deployment-target 11.0 --app-icon AppIcon \ + --output-partial-info-plist "$partial" --output-format human-readable-text) || + { echo "$out" >&2; exit 1; } + rm -f "$partial" +fi + cat > "$app/Contents/Info.plist" <<'PLIST' @@ -24,6 +41,10 @@ cat > "$app/Contents/Info.plist" <<'PLIST' Lexical Playground (GPUI) CFBundlePackageType APPL + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon LSApplicationCategoryType public.app-category.developer-tools NSHighResolutionCapable @@ -33,6 +54,8 @@ cat > "$app/Contents/Info.plist" <<'PLIST' PLIST # A hard link, not a copy: the executable can be large in a debug build. ln -f "$exe" "$app/Contents/MacOS/lexical-playground-gpui" +# LaunchServices can see the bundle before it's whole, as when actool runs, +# and the Dock then shows the app as broken: register it as it is now. /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \ -f "$app" exec "$app/Contents/MacOS/lexical-playground-gpui" "$@" diff --git a/rust/apps/lexical-playground-gpui/src/app.rs b/rust/apps/lexical-playground-gpui/src/app.rs index 30073f678fc..af8baf8c121 100644 --- a/rust/apps/lexical-playground-gpui/src/app.rs +++ b/rust/apps/lexical-playground-gpui/src/app.rs @@ -41,7 +41,11 @@ impl PlaygroundApp { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let editor = cx.new(|cx| { - LexicalEditor::new(Playground::new(example()).expect("the example loads"), cx) + LexicalEditor::new( + Playground::new(example()).expect("the example loads"), + window, + cx, + ) }); let subscription = cx.subscribe(&editor, |this, _, _: &EditorEvent, cx| { this.status = None; @@ -113,8 +117,9 @@ impl PlaygroundApp { .ghost() .label("Reset") .on_click(cx.listener(|this, _, window, cx| { - let editor = - cx.new(|cx| LexicalEditor::new(Playground::new(example()).unwrap(), cx)); + let editor = cx.new(|cx| { + LexicalEditor::new(Playground::new(example()).unwrap(), window, cx) + }); this._subscription = cx.subscribe(&editor, |this, _, _: &EditorEvent, cx| { this.status = None; cx.notify(); diff --git a/rust/apps/lexical-playground-gpui/src/editor.rs b/rust/apps/lexical-playground-gpui/src/editor.rs index 56367785fec..ab32cfcb7b1 100644 --- a/rust/apps/lexical-playground-gpui/src/editor.rs +++ b/rust/apps/lexical-playground-gpui/src/editor.rs @@ -12,6 +12,7 @@ use gpui_kit::*; use lexical_playground_core::{Outcome, Playground, Surface, Unit, format_flag}; use std::ops::Range; use std::rc::Rc; +use std::time::Duration; use unicode_segmentation::UnicodeSegmentation; actions!( @@ -51,6 +52,9 @@ actions!( const CONTEXT: &str = "LexicalEditor"; +/// How long the caret stays on, then off, as a Mac text field's does. +const BLINK_PHASE: Duration = Duration::from_millis(500); + /// The editor's keys, as a browser's `contenteditable` takes them on macOS. pub fn bind_keys(cx: &mut App) { let c = Some(CONTEXT); @@ -174,20 +178,88 @@ pub struct LexicalEditor { goal_x: Option, /// The last edit's error. pub error: Option, + /// Whether the caret is in its on phase (see [`LexicalEditor::blink`]). + caret_on: bool, + /// Bumped by every restart, so an older blink loop stops. + blink_epoch: usize, + _blink: Task<()>, + _subscriptions: Vec, } impl LexicalEditor { - pub fn new(playground: Playground, cx: &mut Context) -> Self { + pub fn new(playground: Playground, window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); + let this = cx.entity().downgrade(); + let _subscriptions = vec![ + // Every keystroke shows the caret, before its action runs, as + // gpui-component's input does: a key binding can consume it. + cx.intercept_keystrokes({ + let focus_handle = focus_handle.clone(); + move |_, window, cx| { + if focus_handle.is_focused(window) { + _ = this.update(cx, |editor, cx| editor.blink(cx)); + } + } + }), + cx.observe_window_activation(window, |editor, window, cx| { + if window.is_window_active() && editor.focus_handle.is_focused(window) { + editor.blink(cx); + } else { + editor.stop_blink(cx); + } + }), + cx.on_focus(&focus_handle, window, |editor, _, cx| editor.blink(cx)), + cx.on_blur(&focus_handle, window, |editor, _, cx| editor.stop_blink(cx)), + ]; LexicalEditor { surface: Surface::new(playground), - focus_handle: cx.focus_handle(), + focus_handle, layout: None, selecting: false, goal_x: None, error: None, + caret_on: true, + blink_epoch: 0, + _blink: Task::ready(()), + _subscriptions, } } + /// Shows the caret and blinks it from now, as a Mac text field does + /// after a key, an edit or a move: on for a phase, then off and on + /// every phase, until the editor or the window loses the keyboard. + fn blink(&mut self, cx: &mut Context) { + self.blink_epoch = self.blink_epoch.wrapping_add(1); + let epoch = self.blink_epoch; + self.caret_on = true; + cx.notify(); + self._blink = cx.spawn(async move |this, cx| { + loop { + cx.background_executor().timer(BLINK_PHASE).await; + let blinking = this.update(cx, |editor, cx| { + if editor.blink_epoch != epoch { + return false; + } + editor.caret_on = !editor.caret_on; + cx.notify(); + true + }); + if !matches!(blinking, Ok(true)) { + break; + } + } + }); + } + + /// Stops blinking; the caret isn't drawn while the editor or the + /// window doesn't have the keyboard. + fn stop_blink(&mut self, cx: &mut Context) { + self.blink_epoch = self.blink_epoch.wrapping_add(1); + self._blink = Task::ready(()); + self.caret_on = false; + cx.notify(); + } + /// Whether the element has painted, so keys can move the caret. #[cfg(feature = "e2e")] pub fn is_laid_out(&self) -> bool { @@ -203,6 +275,7 @@ impl LexicalEditor { fn done(&mut self, outcome: Outcome, cx: &mut Context) { self.error = outcome.error; self.goal_x = None; + self.blink(cx); cx.emit(EditorEvent::Changed); cx.notify(); } @@ -214,6 +287,7 @@ impl LexicalEditor { fn move_to(&mut self, offset: usize, extend: bool, cx: &mut Context) { self.surface.move_to(offset, extend); + self.blink(cx); cx.emit(EditorEvent::Changed); cx.notify(); } @@ -759,7 +833,10 @@ impl Element for EditorElement { window: &mut Window, cx: &mut App, ) { - let focus_handle = self.editor.read(cx).focus_handle.clone(); + let (focus_handle, caret_on) = { + let editor = self.editor.read(cx); + (editor.focus_handle.clone(), editor.caret_on) + }; window.handle_input( &focus_handle, ElementInputHandler::new(bounds, self.editor.clone()), @@ -791,6 +868,8 @@ impl Element for EditorElement { .ok(); } if focus_handle.is_focused(window) + && window.is_window_active() + && caret_on && let Some(caret) = prepaint.caret.take() { window.paint_quad(caret); diff --git a/rust/apps/lexical-playground-rune/AppIcon.icon/Assets/Accent.svg b/rust/apps/lexical-playground-rune/AppIcon.icon/Assets/Accent.svg new file mode 100644 index 00000000000..9a3a0fe2a26 --- /dev/null +++ b/rust/apps/lexical-playground-rune/AppIcon.icon/Assets/Accent.svg @@ -0,0 +1 @@ + diff --git a/rust/apps/lexical-playground-rune/AppIcon.icon/Assets/Frame.svg b/rust/apps/lexical-playground-rune/AppIcon.icon/Assets/Frame.svg new file mode 100644 index 00000000000..7bbffd8b097 --- /dev/null +++ b/rust/apps/lexical-playground-rune/AppIcon.icon/Assets/Frame.svg @@ -0,0 +1 @@ + diff --git a/rust/apps/lexical-playground-rune/AppIcon.icon/Assets/Text.svg b/rust/apps/lexical-playground-rune/AppIcon.icon/Assets/Text.svg new file mode 100644 index 00000000000..7b6d55ba6b9 --- /dev/null +++ b/rust/apps/lexical-playground-rune/AppIcon.icon/Assets/Text.svg @@ -0,0 +1 @@ + diff --git a/rust/apps/lexical-playground-rune/AppIcon.icon/Assets/rune.svg b/rust/apps/lexical-playground-rune/AppIcon.icon/Assets/rune.svg new file mode 100644 index 00000000000..2d6b518d22c --- /dev/null +++ b/rust/apps/lexical-playground-rune/AppIcon.icon/Assets/rune.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rust/apps/lexical-playground-rune/AppIcon.icon/icon.json b/rust/apps/lexical-playground-rune/AppIcon.icon/icon.json new file mode 100644 index 00000000000..c0af340dc44 --- /dev/null +++ b/rust/apps/lexical-playground-rune/AppIcon.icon/icon.json @@ -0,0 +1,130 @@ +{ + "fill-specializations": [ + { + "value": { + "solid": "srgb:1.00000,1.00000,1.00000,1.00000" + } + }, + { + "appearance": "dark", + "value": { + "solid": "srgb:0.10980,0.11373,0.12157,1.00000" + } + } + ], + "groups": [ + { + "layers": [ + { + "image-name": "rune.svg", + "name": "rune", + "position": { + "scale": 1, + "translation-in-points": [255, 255] + } + } + ], + "shadow": { + "kind": "neutral", + "opacity": 0.5 + }, + "translucency": { + "enabled": false, + "value": 0.5 + } + }, + { + "layers": [ + { + "fill-specializations": [ + { + "value": { + "solid": "srgb:0.46275,0.71373,1.00000,1.00000" + } + }, + { + "appearance": "dark", + "value": { + "solid": "srgb:0.46275,0.71373,1.00000,1.00000" + } + } + ], + "glass": false, + "image-name": "Accent.svg", + "name": "Accent", + "position": { + "scale": 0.8, + "translation-in-points": [-64.0, -64.0] + } + }, + { + "fill-specializations": [ + { + "value": { + "solid": "srgb:0.00000,0.00000,0.00000,1.00000" + } + }, + { + "appearance": "dark", + "value": { + "solid": "srgb:1.00000,1.00000,1.00000,1.00000" + } + }, + { + "appearance": "tinted", + "value": { + "solid": "srgb:1.00000,1.00000,1.00000,1.00000" + } + } + ], + "glass": false, + "image-name": "Text.svg", + "name": "Text", + "position": { + "scale": 0.8, + "translation-in-points": [-64.0, -64.0] + } + }, + { + "fill-specializations": [ + { + "value": { + "solid": "srgb:0.00000,0.00000,0.00000,1.00000" + } + }, + { + "appearance": "dark", + "value": { + "solid": "srgb:1.00000,1.00000,1.00000,1.00000" + } + }, + { + "appearance": "tinted", + "value": { + "solid": "srgb:1.00000,1.00000,1.00000,1.00000" + } + } + ], + "glass": false, + "image-name": "Frame.svg", + "name": "Frame", + "position": { + "scale": 0.8, + "translation-in-points": [-64.0, -64.0] + } + } + ], + "shadow": { + "kind": "neutral", + "opacity": 0.5 + }, + "translucency": { + "enabled": false, + "value": 0.5 + } + } + ], + "supported-platforms": { + "squares": "shared" + } +} diff --git a/rust/apps/lexical-playground-rune/README.md b/rust/apps/lexical-playground-rune/README.md index 32402d9b6de..dd219565046 100644 --- a/rust/apps/lexical-playground-rune/README.md +++ b/rust/apps/lexical-playground-rune/README.md @@ -53,8 +53,14 @@ the first build. On macOS, `cargo run` runs the app as an app: `bundle.sh`, Cargo's runner there (`.cargo/config.toml`), makes `Lexical Playground (Rune).app` beside the -executable and runs the app from inside it, so it shows in the Dock. The -bundle stays, to open from Finder. +executable and runs the app from inside it, so it shows in the Dock with its +icon. The bundle stays, to open from Finder. + +The icon, `AppIcon.icon`, is an Icon Composer document, as wevm/zero's +zbugs-rune has: the Lexical mark of the Swift playground's icon, with the +Rune mark in the bottom right corner, in light and dark. `bundle.sh` +compiles it with Xcode's `actool` when it changes. Open it in Icon Composer +to edit it. Like every Rune app, it renders headless too: `cargo run -- --snap playground.png --size 1100x720 --scene typed` (and diff --git a/rust/apps/lexical-playground-rune/bundle.sh b/rust/apps/lexical-playground-rune/bundle.sh index 70f3604212b..86db7a3babc 100755 --- a/rust/apps/lexical-playground-rune/bundle.sh +++ b/rust/apps/lexical-playground-rune/bundle.sh @@ -1,7 +1,9 @@ #!/bin/sh # Cargo's runner on macOS (.cargo/config.toml): runs the playground from # inside an app bundle, `Lexical Playground (Rune).app` beside the -# executable, so it launches as an app in the Dock and the app switcher. The +# executable, so it launches as an app with its icon (AppIcon.icon: the +# Lexical mark with the Rune mark in the corner) in the Dock and the app +# switcher, as wevm/zero's zbugs-rune does. The # bundle stays, to open from Finder or keep in the Dock. Tests and other # executables run as they are. set -eu @@ -9,8 +11,23 @@ exe=$1 shift [ "$(basename "$exe")" = lexical-playground-rune ] || exec "$exe" "$@" +here=$(cd "$(dirname "$0")" && pwd) +# Absolute: actool takes relative paths from a directory of its own. app="$(cd "$(dirname "$exe")" && pwd)/Lexical Playground (Rune).app" -mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" +res="$app/Contents/Resources" +mkdir -p "$app/Contents/MacOS" "$res" + +# The icon, through Xcode's actool, when it changed: Assets.car for macOS 26 +# and later, AppIcon.icns for earlier ones. +if [ ! -e "$res/Assets.car" ] || [ -n "$(find "$here/AppIcon.icon" -newer "$res/Assets.car")" ]; then + partial=$(mktemp) + out=$(xcrun actool "$here/AppIcon.icon" --compile "$res" --platform macosx \ + --minimum-deployment-target 11.0 --app-icon AppIcon \ + --output-partial-info-plist "$partial" --output-format human-readable-text) || + { echo "$out" >&2; exit 1; } + rm -f "$partial" +fi + cat > "$app/Contents/Info.plist" <<'PLIST' @@ -24,6 +41,10 @@ cat > "$app/Contents/Info.plist" <<'PLIST' Lexical Playground (Rune) CFBundlePackageType APPL + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon LSApplicationCategoryType public.app-category.developer-tools NSHighResolutionCapable @@ -33,6 +54,8 @@ cat > "$app/Contents/Info.plist" <<'PLIST' PLIST # A hard link, not a copy: the executable can be large in a debug build. ln -f "$exe" "$app/Contents/MacOS/lexical-playground-rune" +# LaunchServices can see the bundle before it's whole, as when actool runs, +# and the Dock then shows the app as broken: register it as it is now. /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \ -f "$app" exec "$app/Contents/MacOS/lexical-playground-rune" "$@" diff --git a/rust/apps/lexical-playground-rune/src/main.rs b/rust/apps/lexical-playground-rune/src/main.rs index fc23773a78e..8115a8bae73 100644 --- a/rust/apps/lexical-playground-rune/src/main.rs +++ b/rust/apps/lexical-playground-rune/src/main.rs @@ -33,6 +33,31 @@ pub struct State { pub report: Option<(String, bool)>, /// What the last action said (an export's path, an edit's error). pub status: Option, + /// The caret's blink: see [`State::caret_visible`]. + pub blink: Blink, +} + +/// How long the caret stays on, then off, as a Mac text field's does. +const BLINK_PHASE: f64 = 0.5; + +/// The caret blinks as a Mac text field's: solid for a phase after any +/// key, edit or move, then off and on every phase, and hidden while the +/// window or the field doesn't have the keyboard. Rune's field draws its +/// caret solid, so the view paints it clear in the off phases. +pub struct Blink { + /// The window's clock (seconds) at the last key, edit or move. + since: f64, + /// The field's selection then, to see the pointer move the caret. + selection: (usize, usize), + window_focused: bool, + pub on: bool, +} + +impl Blink { + fn restart(&mut self, clock: f64) { + self.since = clock; + self.on = true; + } } impl State { @@ -44,6 +69,12 @@ impl State { panel: Panel::Tree, report: None, status: None, + blink: Blink { + since: 0.0, + selection: (0, 0), + window_focused: true, + on: true, + }, }; state.edit.focused = true; state.sync(); @@ -197,18 +228,45 @@ pub fn decorations(doc: &Document) -> Vec { spans } -pub fn on_key(s: &mut State, ev: &KeyEvent, _: &mut Tx) -> bool { +pub fn on_key(s: &mut State, ev: &KeyEvent, tx: &mut Tx) -> bool { + s.blink.restart(tx.clock()); s.key(ev) } -pub fn on_paste(s: &mut State, content: &ClipboardContent, _: &mut Tx) -> bool { +pub fn on_paste(s: &mut State, content: &ClipboardContent, tx: &mut Tx) -> bool { + s.blink.restart(tx.clock()); s.paste(content) } -pub fn on_change(s: &mut State, _: &mut Tx) { +pub fn on_change(s: &mut State, tx: &mut Tx) { + s.blink.restart(tx.clock()); s.changed(); } +pub fn on_window_focus(s: &mut State, focused: bool, tx: &mut Tx) { + s.blink.window_focused = focused; + s.blink.restart(tx.clock()); +} + +/// Every frame: the caret's blink phase, and a wake for the next one. +fn poll(s: &mut State, clock: f64, tx: &mut Tx) -> bool { + let b = &mut s.blink; + let selection = (s.edit.anchor, s.edit.focus); + if selection != b.selection { + b.selection = selection; + b.since = clock; + } + let was = b.on; + if b.window_focused && s.edit.focused { + let phase = ((clock - b.since).max(0.0) / BLINK_PHASE).floor(); + b.on = (phase as u64).is_multiple_of(2); + tx.wake_at(b.since + (phase + 1.0) * BLINK_PHASE); + } else { + b.on = false; + } + b.on != was +} + /// A session headless snapshots show: typed text, a new paragraph, bold. fn typed(s: &mut State) { let end = s.playground.document().end(); @@ -237,6 +295,7 @@ fn main() { // Inline code (`Mark::Code`) draws in the mono slot: the system's // monospaced face (SF Mono on macOS). families: ("Inter", "ui-monospace"), + poll: Some(poll), ..AppSpec::new( "Lexical Playground (Rune)", (1100.0, 720.0), diff --git a/rust/apps/lexical-playground-rune/src/view.rs b/rust/apps/lexical-playground-rune/src/view.rs index d9fc079f881..3dab8820e06 100644 --- a/rust/apps/lexical-playground-rune/src/view.rs +++ b/rust/apps/lexical-playground-rune/src/view.rs @@ -1,7 +1,7 @@ //! The window: a toolbar, the editor, and a side panel with the editor's //! tree, its JSON, and parity with JS Lexical. -use crate::{Panel, State, decorations, on_change, on_key, on_paste}; +use crate::{Panel, State, decorations, on_change, on_key, on_paste, on_window_focus}; use lexical_playground_core::{Playground, example, replay_vectors}; use rune::{Color, Cx, El, ScrollSpec, TextStyle, col, row, text, text_input}; use rune_kit::{Button, ButtonVariant, Kit, SegmentedControl}; @@ -34,6 +34,7 @@ pub fn view(s: &State, cx: &mut Cx) -> El { .w_full() .h_full() .fill(k.c.page) + .on_window_focus(on_window_focus) .child(toolbar(s, k, cx.chrome_insets().leading)) .child( row() @@ -99,7 +100,14 @@ fn editor(s: &State, k: &Kit) -> El { // the system's appearance. let field = text_input(&s.edit, TextStyle::new(16.0, 400.0)) .color(k.c.text_base) - .caret(k.c.text_base, k.selection()) + .caret( + if s.blink.on { + k.c.text_base + } else { + Color::TRANSPARENT + }, + k.selection(), + ) .key("editor") .w_full() .grow(1.0) From ebab324f6b244cf18347610b07068f8acb9e6863 Mon Sep 17 00:00:00 2001 From: Amp Date: Sat, 14 Nov 2026 01:21:41 +0000 Subject: [PATCH 06/10] feat(rust): @lexical/history behind the history feature ## 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 --- packages/lexical-conformance/src/ops.ts | 77 +- .../lexical-conformance/src/scripts.test.ts | 262 +++++- .../vectors/scripts/history.json | 12 + rust/DESIGN.md | 4 +- rust/PARITY.md | 10 +- rust/conformance/Cargo.toml | 2 +- rust/conformance/src/scripts.rs | 153 ++- rust/conformance/src/suites/mod.rs | 5 + rust/lexical-editor/Cargo.toml | 3 + rust/lexical-editor/src/commands.rs | 34 + rust/lexical-editor/src/editor.rs | 49 +- rust/lexical-editor/src/history.rs | 889 ++++++++++++++++++ rust/lexical-editor/src/lib.rs | 14 +- 13 files changed, 1478 insertions(+), 36 deletions(-) create mode 100644 packages/lexical-conformance/vectors/scripts/history.json create mode 100644 rust/lexical-editor/src/commands.rs create mode 100644 rust/lexical-editor/src/history.rs diff --git a/packages/lexical-conformance/src/ops.ts b/packages/lexical-conformance/src/ops.ts index 6e5a34c4ba2..9e0d472a7f8 100644 --- a/packages/lexical-conformance/src/ops.ts +++ b/packages/lexical-conformance/src/ops.ts @@ -6,6 +6,7 @@ * */ +import {createEmptyHistoryState, registerHistory} from '@lexical/history'; import { $caretRangeFromSelection, $createNodeSelection, @@ -28,7 +29,12 @@ import { $selectAll, $setSelection, $setSelectionFromCaretRange, + type AnyLexicalCommand, + CAN_REDO_COMMAND, + CAN_UNDO_COMMAND, type CaretDirection, + CLEAR_EDITOR_COMMAND, + CLEAR_HISTORY_COMMAND, COMMAND_PRIORITY_EDITOR, createCommand, createEditor, @@ -39,10 +45,12 @@ import { type NodeCaret, ParagraphNode, type PointCaret, + REDO_COMMAND, resetRandomKey, RootNode, type SerializedLexicalNode, TextNode, + UNDO_COMMAND, } from 'lexical'; // A small, JSON-only script language for editor sessions, interpreted here by @@ -95,7 +103,11 @@ export type Action = | {do: 'logNormalizeCaret'; caret: CaretSpec} | {do: 'dispatch'; command: string; payload?: Json}; -/** A command handler: logs, runs its actions, then returns `result`. */ +/** + * A command handler: logs, runs its actions, then returns `result`. Like + * `dispatch`, `command` names one of Lexical's own commands (CORE_COMMANDS) + * or a command the script makes. + */ export type HandlerSpec = { id: string; command: string; @@ -104,10 +116,16 @@ export type HandlerSpec = { actions?: Action[]; }; -/** A step: one discrete update, or a command dispatched outside an update. */ -export type Step = +/** + * A step: one discrete update, or a command dispatched outside an update. + * `now` sets the session's clock (in ms, what `Date.now()` returns while + * steps run) before the step; without it the clock keeps its last value, + * which starts at 0. + */ +export type Step = ( | {update: Action[]; tag?: string[]} - | {dispatch: string; payload?: Json}; + | {dispatch: string; payload?: Json} +) & {now?: number}; export type Script = { name: string; @@ -116,6 +134,11 @@ export type Script = { handlers?: HandlerSpec[]; /** Log update, mutation and text content listener calls. */ listen?: boolean; + /** + * `registerHistory(editor, createEmptyHistoryState(), delay)` (@lexical/history), + * registered after the handlers and listeners, on the session's clock. + */ + history?: {delay: number}; steps: Step[]; /** Record `state` after every step (default), or after the last only. */ snapshot?: 'every' | 'final'; @@ -455,7 +478,24 @@ function $runAction(action: Action, ctx: Context): void { } } +/** + * Lexical's own commands, which `dispatch` and handlers name as JS exports + * them. Any other name is a command the script makes. + */ +const CORE_COMMANDS = new Map([ + ['CAN_REDO_COMMAND', CAN_REDO_COMMAND], + ['CAN_UNDO_COMMAND', CAN_UNDO_COMMAND], + ['CLEAR_EDITOR_COMMAND', CLEAR_EDITOR_COMMAND], + ['CLEAR_HISTORY_COMMAND', CLEAR_HISTORY_COMMAND], + ['REDO_COMMAND', REDO_COMMAND], + ['UNDO_COMMAND', UNDO_COMMAND], +]); + function command(ctx: Context, name: string): LexicalCommand { + const core = CORE_COMMANDS.get(name); + if (core !== undefined) { + return core; + } let cmd = ctx.commands.get(name); if (cmd === undefined) { cmd = createCommand(name); @@ -586,7 +626,36 @@ export function startSession(script: Omit): Session { ctx.log.push(['textContent', content]); }); } + // The session's clock: `Date.now()` returns it while a step runs, + // including the read below that commits a dispatch's update, so the + // history listener sees it. + let clock = 0; + const atClock = (f: () => T): T => { + const dateNow = Date.now; + Date.now = () => clock; + try { + return f(); + } finally { + Date.now = dateNow; + } + }; + if (script.history !== undefined) { + const {delay} = script.history; + atClock(() => + // `registerHistory`'s default clock would keep the `Date.now` of the + // moment it registers; this one reads it at every update. + registerHistory(editor, createEmptyHistoryState(), delay, () => + Date.now(), + ), + ); + } const run = (step: Step, snapshot: boolean): StepResult => { + if (step.now !== undefined) { + clock = step.now; + } + return atClock(() => runAtClock(step, snapshot)); + }; + const runAtClock = (step: Step, snapshot: boolean): StepResult => { ctx.log = []; let error = false; try { diff --git a/packages/lexical-conformance/src/scripts.test.ts b/packages/lexical-conformance/src/scripts.test.ts index 36919e9d2f5..9906bcc3930 100644 --- a/packages/lexical-conformance/src/scripts.test.ts +++ b/packages/lexical-conformance/src/scripts.test.ts @@ -9,7 +9,13 @@ import type {SerializedLexicalNode} from 'lexical'; import {test} from 'vitest'; -import {type Action, runScript, type Script, type Step} from './ops'; +import { + type Action, + type HandlerSpec, + runScript, + type Script, + type Step, +} from './ops'; import {emitVectors} from './vectors'; // Hand-written editor sessions over the core nodes, recorded step by step by @@ -644,3 +650,257 @@ test('scripts/caret', () => { ], ); }); + +test('scripts/history', () => { + // `registerHistory` with a 300 ms delay, on the session's clock, which each + // step sets with `now`. The handlers log CAN_UNDO_COMMAND and + // CAN_REDO_COMMAND. + const history = {delay: 300}; + const handlers: HandlerSpec[] = [ + {command: 'CAN_UNDO_COMMAND', id: 'canUndo', priority: 0, result: false}, + {command: 'CAN_REDO_COMMAND', id: 'canRedo', priority: 0, result: false}, + ]; + const at = (now: number, step: Step): Step => ({...step, now}); + const type = (now: number, text: string, tag?: string[]): Step => ({ + now, + tag, + update: [call('insertText', text)], + }); + const undo = (now: number): Step => ({dispatch: 'UNDO_COMMAND', now}); + const redo = (now: number): Step => ({dispatch: 'REDO_COMMAND', now}); + emit( + 'history', + '@lexical/history: which updates merge into one undo entry and which push a new one, undo, redo and clearing, and the CAN_UNDO/CAN_REDO notifications.', + [ + { + handlers, + history, + name: 'typing merges within the delay and pushes after it, then undo and redo', + steps: [ + at(0, doc(p(t('Hello')))), + // A selection-only update merges into the current entry. + at(0, update(select(['2', 5, 'text']))), + type(100, '!'), + type(200, 'a'), + // 499 < 200 + 300: merges. + type(499, 'b'), + // 799 is not < 499 + 300: pushes. + type(799, 'c'), + type(800, 'd'), + undo(900), + undo(901), + // The undo stack is empty: nothing changes. + undo(902), + redo(903), + redo(904), + // The redo stack is empty: nothing changes. + redo(905), + ], + }, + { + handlers, + history, + listen: true, + name: 'a new edit after undo clears the redo stack', + steps: [ + at(0, doc(p(t('ab')))), + at(0, update(select(['2', 2, 'text']))), + type(1000, 'c'), + type(2000, 'd'), + undo(2100), + undo(2200), + type(3000, 'x'), + redo(3100), + undo(3200), + ], + }, + { + handlers, + history, + name: 'history-merge and history-push tags', + steps: [ + at(0, doc(p(t('a')))), + at(0, update(select(['2', 1, 'text']))), + type(1000, 'b'), + // Within the delay, but tagged history-push. + type(1010, 'c', ['history-push']), + type(1020, 'd'), + // Long after, but tagged history-merge. + { + now: 5000, + tag: ['history-merge'], + update: [node('2', 'setTextContent', 'merged')], + }, + // An unchanged text node marked dirty merges, unless tagged + // history-push. + {now: 6000, tag: ['history-push'], update: [node('2', 'markDirty')]}, + at(7000, update(node('2', 'markDirty'))), + undo(8000), + undo(8001), + undo(8002), + undo(8003), + ], + }, + { + handlers, + history, + name: 'selection-only updates merge, and one that clears the selection is discarded', + steps: [ + at(0, doc(p(t('one')))), + at(10, update(select(['2', 3, 'text']))), + at(20, update({do: 'clearSelection'})), + at(1000, update(select(['2', 1, 'text']), call('insertText', 'X'))), + // Back to the selection at 3: the cleared one was never recorded. + undo(1100), + redo(1200), + ], + }, + { + handlers, + history, + name: 'formatText, insertParagraph and a deletion, each undone', + steps: [ + at(0, doc(p(t('Hello world')))), + at( + 1000, + update( + select(['2', 0, 'text'], ['2', 5, 'text']), + call('formatText', 'bold'), + ), + ), + at(2000, update(node('2', 'selectEnd'), call('insertParagraph'))), + at(3000, update({do: 'selectAll'}, call('removeText'))), + undo(4000), + undo(4001), + undo(4002), + undo(4003), + redo(4004), + redo(4005), + redo(4006), + ], + }, + { + history, + name: 'typing into an empty paragraph merges from the first character', + steps: [ + at(0, doc(p())), + at(0, update(select(['1', 0, 'element']))), + type(10, 'a'), + type(20, 'b'), + type(30, 'c'), + undo(40), + redo(50), + ], + }, + { + handlers, + history, + name: 'deleting backward and forward', + steps: [ + at(0, doc(p(t('abcdef')))), + at(0, update(select(['2', 6, 'text']))), + // Backward: the caret moves back over the deleted character. + at( + 10, + update( + select(['2', 5, 'text'], ['2', 6, 'text']), + call('removeText'), + ), + ), + at( + 20, + update( + select(['2', 4, 'text'], ['2', 5, 'text']), + call('removeText'), + ), + ), + // Forward: the caret stays. + at( + 30, + update( + select(['2', 2, 'text'], ['2', 3, 'text']), + call('removeText'), + ), + ), + at( + 40, + update( + select(['2', 2, 'text'], ['2', 3, 'text']), + call('removeText'), + ), + ), + undo(50), + undo(60), + undo(70), + ], + }, + { + handlers, + history, + name: 'paste and cut are their own entries', + steps: [ + at(0, doc(p(t('ab')))), + at(0, update(select(['2', 2, 'text']))), + type(10, 'c'), + type(20, 'P', ['paste']), + type(30, 'd'), + { + now: 40, + tag: ['cut'], + update: [ + select(['2', 0, 'text'], ['2', 1, 'text']), + call('removeText'), + ], + }, + type(50, 'e'), + undo(60), + undo(70), + undo(80), + undo(90), + ], + }, + { + handlers, + history, + name: 'a composition that changes nothing merges', + steps: [ + at(0, doc(p(t('ab')))), + at(0, update(select(['2', 2, 'text']))), + type(1000, 'c'), + type(2000, 'x', ['composition-start']), + { + now: 3000, + tag: ['composition-end'], + update: [ + select(['2', 3, 'text'], ['2', 4, 'text']), + call('removeText'), + ], + }, + type(3010, 'd'), + undo(4000), + undo(4010), + ], + }, + { + handlers, + history, + name: 'CLEAR_HISTORY_COMMAND and CLEAR_EDITOR_COMMAND', + steps: [ + at(0, doc(p(t('a')))), + at(0, update(select(['2', 1, 'text']))), + type(1000, 'b'), + type(2000, 'c'), + undo(2100), + {dispatch: 'CLEAR_HISTORY_COMMAND', now: 2200}, + undo(2300), + redo(2400), + type(3000, 'd'), + type(4000, 'e'), + // History clears, but nothing handles the command. + {dispatch: 'CLEAR_EDITOR_COMMAND', now: 4100}, + undo(4200), + ], + }, + ], + ); +}); diff --git a/packages/lexical-conformance/vectors/scripts/history.json b/packages/lexical-conformance/vectors/scripts/history.json new file mode 100644 index 00000000000..f54eaa4201f --- /dev/null +++ b/packages/lexical-conformance/vectors/scripts/history.json @@ -0,0 +1,12 @@ +{"description":"@lexical/history: which updates merge into one undo entry and which push a new one, undo, redo and clearing, and the CAN_UNDO/CAN_REDO notifications.","formatVersion":1,"kind":"script","cases":[ +{"handlers":[{"command":"CAN_UNDO_COMMAND","id":"canUndo","priority":0,"result":false},{"command":"CAN_REDO_COMMAND","id":"canRedo","priority":0,"result":false}],"history":{"delay":300},"name":"typing merges within the delay and pushes after it, then undo and redo","steps":[{"update":[{"do":"append","key":"root","nodes":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Hello","type":"text","version":1}],"direction":null,"format":"","indent":0,"textFormat":0,"textStyle":"","type":"paragraph","version":1}]}],"now":0},{"update":[{"anchor":["2",5,"text"],"do":"select"}],"now":0},{"now":100,"update":[{"args":["!"],"do":"selection","method":"insertText"}]},{"now":200,"update":[{"args":["a"],"do":"selection","method":"insertText"}]},{"now":499,"update":[{"args":["b"],"do":"selection","method":"insertText"}]},{"now":799,"update":[{"args":["c"],"do":"selection","method":"insertText"}]},{"now":800,"update":[{"args":["d"],"do":"selection","method":"insertText"}]},{"dispatch":"UNDO_COMMAND","now":900},{"dispatch":"UNDO_COMMAND","now":901},{"dispatch":"UNDO_COMMAND","now":902},{"dispatch":"REDO_COMMAND","now":903},{"dispatch":"REDO_COMMAND","now":904},{"dispatch":"REDO_COMMAND","now":905}],"results":[{"log":[],"selection":null,"text":"Hello","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello\"}]}]}}"},{"log":[],"selection":["range",["2",5,"text"],["2",5,"text"],0,""],"text":"Hello","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",6,"text"],["2",6,"text"],0,""],"text":"Hello!","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello!\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello!\"}]}]}}"},{"log":[],"selection":["range",["2",7,"text"],["2",7,"text"],0,""],"text":"Hello!a","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello!a\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello!a\"}]}]}}"},{"log":[],"selection":["range",["2",8,"text"],["2",8,"text"],0,""],"text":"Hello!ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello!ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello!ab\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",9,"text"],["2",9,"text"],0,""],"text":"Hello!abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello!abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello!abc\"}]}]}}"},{"log":[],"selection":["range",["2",10,"text"],["2",10,"text"],0,""],"text":"Hello!abcd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello!abcd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello!abcd\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",8,"text"],["2",8,"text"],0,""],"text":"Hello!ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello!ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello!ab\"}]}]}}"},{"log":[["handler","canRedo",true],["handler","canUndo",false],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",5,"text"],["2",5,"text"],0,""],"text":"Hello","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello\"}]}]}}"},{"log":[["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",5,"text"],["2",5,"text"],0,""],"text":"Hello","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello\"}]}]}}"},{"log":[["handler","canUndo",true],["dispatched","REDO_COMMAND",true]],"selection":["range",["2",8,"text"],["2",8,"text"],0,""],"text":"Hello!ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello!ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello!ab\"}]}]}}"},{"log":[["handler","canUndo",true],["handler","canRedo",false],["dispatched","REDO_COMMAND",true]],"selection":["range",["2",10,"text"],["2",10,"text"],0,""],"text":"Hello!abcd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello!abcd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello!abcd\"}]}]}}"},{"log":[["dispatched","REDO_COMMAND",true]],"selection":["range",["2",10,"text"],["2",10,"text"],0,""],"text":"Hello!abcd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello!abcd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello!abcd\"}]}]}}"}]}, +{"handlers":[{"command":"CAN_UNDO_COMMAND","id":"canUndo","priority":0,"result":false},{"command":"CAN_REDO_COMMAND","id":"canRedo","priority":0,"result":false}],"history":{"delay":300},"listen":true,"name":"a new edit after undo clears the redo stack","steps":[{"update":[{"do":"append","key":"root","nodes":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"ab","type":"text","version":1}],"direction":null,"format":"","indent":0,"textFormat":0,"textStyle":"","type":"paragraph","version":1}]}],"now":0},{"update":[{"anchor":["2",2,"text"],"do":"select"}],"now":0},{"now":1000,"update":[{"args":["c"],"do":"selection","method":"insertText"}]},{"now":2000,"update":[{"args":["d"],"do":"selection","method":"insertText"}]},{"dispatch":"UNDO_COMMAND","now":2100},{"dispatch":"UNDO_COMMAND","now":2200},{"now":3000,"update":[{"args":["x"],"do":"selection","method":"insertText"}]},{"dispatch":"REDO_COMMAND","now":3100},{"dispatch":"UNDO_COMMAND","now":3200}],"results":[{"log":[["textContent","ab"],["update",[["1",true],["root",true]],["2"],[]]],"selection":null,"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[["update",[],[],[]]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[["textContent","abc"],["update",[["1",false],["root",true]],["2"],[]],["handler","canUndo",true]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abc\"}]}]}}"},{"log":[["textContent","abcd"],["update",[["1",false],["root",true]],["2"],[]],["handler","canUndo",true]],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"abcd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcd\"}]}]}}"},{"log":[["handler","canRedo",true],["update",[],[],["historic"]],["dispatched","UNDO_COMMAND",true],["textContent","abc"],["update",[["root",true]],[],["historic"]]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abc\"}]}]}}"},{"log":[["handler","canRedo",true],["handler","canUndo",false],["update",[],[],["historic"]],["dispatched","UNDO_COMMAND",true],["textContent","ab"],["update",[["root",true]],[],["historic"]]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[["textContent","abx"],["update",[["1",false],["root",true]],["2"],[]],["handler","canRedo",false],["handler","canUndo",true]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abx","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abx\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abx\"}]}]}}"},{"log":[["dispatched","REDO_COMMAND",true]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abx","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abx\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abx\"}]}]}}"},{"log":[["handler","canRedo",true],["handler","canUndo",false],["update",[],[],["historic"]],["dispatched","UNDO_COMMAND",true],["textContent","ab"],["update",[["root",true]],[],["historic"]]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"}]}, +{"handlers":[{"command":"CAN_UNDO_COMMAND","id":"canUndo","priority":0,"result":false},{"command":"CAN_REDO_COMMAND","id":"canRedo","priority":0,"result":false}],"history":{"delay":300},"name":"history-merge and history-push tags","steps":[{"update":[{"do":"append","key":"root","nodes":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"a","type":"text","version":1}],"direction":null,"format":"","indent":0,"textFormat":0,"textStyle":"","type":"paragraph","version":1}]}],"now":0},{"update":[{"anchor":["2",1,"text"],"do":"select"}],"now":0},{"now":1000,"update":[{"args":["b"],"do":"selection","method":"insertText"}]},{"now":1010,"tag":["history-push"],"update":[{"args":["c"],"do":"selection","method":"insertText"}]},{"now":1020,"update":[{"args":["d"],"do":"selection","method":"insertText"}]},{"now":5000,"tag":["history-merge"],"update":[{"args":["merged"],"do":"node","key":"2","method":"setTextContent"}]},{"now":6000,"tag":["history-push"],"update":[{"args":[],"do":"node","key":"2","method":"markDirty"}]},{"update":[{"args":[],"do":"node","key":"2","method":"markDirty"}],"now":7000},{"dispatch":"UNDO_COMMAND","now":8000},{"dispatch":"UNDO_COMMAND","now":8001},{"dispatch":"UNDO_COMMAND","now":8002},{"dispatch":"UNDO_COMMAND","now":8003}],"results":[{"log":[],"selection":null,"text":"a","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"a\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"a\"}]}]}}"},{"log":[],"selection":["range",["2",1,"text"],["2",1,"text"],0,""],"text":"a","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"a\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"a\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abc\"}]}]}}"},{"log":[],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"abcd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcd\"}]}]}}"},{"log":[],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"merged","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"merged\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"merged\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"merged","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"merged\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"merged\"}]}]}}"},{"log":[],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"merged","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"merged\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"merged\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"merged","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"merged\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"merged\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[["handler","canRedo",true],["handler","canUndo",false],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",1,"text"],["2",1,"text"],0,""],"text":"a","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"a\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"a\"}]}]}}"},{"log":[["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",1,"text"],["2",1,"text"],0,""],"text":"a","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"a\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"a\"}]}]}}"}]}, +{"handlers":[{"command":"CAN_UNDO_COMMAND","id":"canUndo","priority":0,"result":false},{"command":"CAN_REDO_COMMAND","id":"canRedo","priority":0,"result":false}],"history":{"delay":300},"name":"selection-only updates merge, and one that clears the selection is discarded","steps":[{"update":[{"do":"append","key":"root","nodes":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"one","type":"text","version":1}],"direction":null,"format":"","indent":0,"textFormat":0,"textStyle":"","type":"paragraph","version":1}]}],"now":0},{"update":[{"anchor":["2",3,"text"],"do":"select"}],"now":10},{"update":[{"do":"clearSelection"}],"now":20},{"update":[{"anchor":["2",1,"text"],"do":"select"},{"args":["X"],"do":"selection","method":"insertText"}],"now":1000},{"dispatch":"UNDO_COMMAND","now":1100},{"dispatch":"REDO_COMMAND","now":1200}],"results":[{"log":[],"selection":null,"text":"one","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"one\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"one\"}]}]}}"},{"log":[],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"one","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"one\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"one\"}]}]}}"},{"log":[],"selection":null,"text":"one","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"one\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"one\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"oXne","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"oXne\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"oXne\"}]}]}}"},{"log":[["handler","canRedo",true],["handler","canUndo",false],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"one","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"one\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"one\"}]}]}}"},{"log":[["handler","canUndo",true],["handler","canRedo",false],["dispatched","REDO_COMMAND",true]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"oXne","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"oXne\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"oXne\"}]}]}}"}]}, +{"handlers":[{"command":"CAN_UNDO_COMMAND","id":"canUndo","priority":0,"result":false},{"command":"CAN_REDO_COMMAND","id":"canRedo","priority":0,"result":false}],"history":{"delay":300},"name":"formatText, insertParagraph and a deletion, each undone","steps":[{"update":[{"do":"append","key":"root","nodes":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Hello world","type":"text","version":1}],"direction":null,"format":"","indent":0,"textFormat":0,"textStyle":"","type":"paragraph","version":1}]}],"now":0},{"update":[{"anchor":["2",0,"text"],"do":"select","focus":["2",5,"text"]},{"args":["bold"],"do":"selection","method":"formatText"}],"now":1000},{"update":[{"args":[],"do":"node","key":"2","method":"selectEnd"},{"args":[],"do":"selection","method":"insertParagraph"}],"now":2000},{"update":[{"do":"selectAll"},{"args":[],"do":"selection","method":"removeText"}],"now":3000},{"dispatch":"UNDO_COMMAND","now":4000},{"dispatch":"UNDO_COMMAND","now":4001},{"dispatch":"UNDO_COMMAND","now":4002},{"dispatch":"UNDO_COMMAND","now":4003},{"dispatch":"REDO_COMMAND","now":4004},{"dispatch":"REDO_COMMAND","now":4005},{"dispatch":"REDO_COMMAND","now":4006}],"results":[{"log":[],"selection":null,"text":"Hello world","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello world\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello world\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",0,"text"],["2",5,"text"],1,""],"text":"Hello world","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":1,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" world\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":1,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"format\":1,\"text\":\"Hello\"},{\"type\":\"text\",\"text\":\" world\"}],\"textFormat\":1}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["3",0,"text"],["3",0,"text"],1,""],"text":"Hello\n\n world","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":1,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":1,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" world\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"format\":1,\"text\":\"Hello\"}],\"textFormat\":1},{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\" world\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["1",0,"element"],["1",0,"element"],0,""],"text":"","state":"{\"root\":{\"children\":[{\"children\":[],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["3",0,"text"],["3",0,"text"],1,""],"text":"Hello\n\n world","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":1,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":1,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" world\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"format\":1,\"text\":\"Hello\"}],\"textFormat\":1},{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\" world\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",0,"text"],["2",5,"text"],1,""],"text":"Hello world","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":1,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" world\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":1,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"format\":1,\"text\":\"Hello\"},{\"type\":\"text\",\"text\":\" world\"}],\"textFormat\":1}]}}"},{"log":[["handler","canRedo",true],["handler","canUndo",false],["dispatched","UNDO_COMMAND",true]],"selection":null,"text":"Hello world","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello world\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello world\"}]}]}}"},{"log":[["dispatched","UNDO_COMMAND",true]],"selection":null,"text":"Hello world","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello world\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"Hello world\"}]}]}}"},{"log":[["handler","canUndo",true],["dispatched","REDO_COMMAND",true]],"selection":["range",["2",0,"text"],["2",5,"text"],1,""],"text":"Hello world","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":1,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" world\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":1,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"format\":1,\"text\":\"Hello\"},{\"type\":\"text\",\"text\":\" world\"}],\"textFormat\":1}]}}"},{"log":[["handler","canUndo",true],["dispatched","REDO_COMMAND",true]],"selection":["range",["3",0,"text"],["3",0,"text"],1,""],"text":"Hello\n\n world","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":1,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":1,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" world\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"format\":1,\"text\":\"Hello\"}],\"textFormat\":1},{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\" world\"}]}]}}"},{"log":[["handler","canUndo",true],["handler","canRedo",false],["dispatched","REDO_COMMAND",true]],"selection":["range",["1",0,"element"],["1",0,"element"],0,""],"text":"","state":"{\"root\":{\"children\":[{\"children\":[],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[]}]}}"}]}, +{"history":{"delay":300},"name":"typing into an empty paragraph merges from the first character","steps":[{"update":[{"do":"append","key":"root","nodes":[{"children":[],"direction":null,"format":"","indent":0,"textFormat":0,"textStyle":"","type":"paragraph","version":1}]}],"now":0},{"update":[{"anchor":["1",0,"element"],"do":"select"}],"now":0},{"now":10,"update":[{"args":["a"],"do":"selection","method":"insertText"}]},{"now":20,"update":[{"args":["b"],"do":"selection","method":"insertText"}]},{"now":30,"update":[{"args":["c"],"do":"selection","method":"insertText"}]},{"dispatch":"UNDO_COMMAND","now":40},{"dispatch":"REDO_COMMAND","now":50}],"results":[{"log":[],"selection":null,"text":"","state":"{\"root\":{\"children\":[{\"children\":[],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[]}]}}"},{"log":[],"selection":["range",["1",0,"element"],["1",0,"element"],0,""],"text":"","state":"{\"root\":{\"children\":[{\"children\":[],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[]}]}}"},{"log":[],"selection":["range",["2",1,"text"],["2",1,"text"],0,""],"text":"a","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"a\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"a\"}]}]}}"},{"log":[],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abc\"}]}]}}"},{"log":[["dispatched","UNDO_COMMAND",true]],"selection":["range",["1",0,"element"],["1",0,"element"],0,""],"text":"","state":"{\"root\":{\"children\":[{\"children\":[],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[]}]}}"},{"log":[["dispatched","REDO_COMMAND",true]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abc\"}]}]}}"}]}, +{"handlers":[{"command":"CAN_UNDO_COMMAND","id":"canUndo","priority":0,"result":false},{"command":"CAN_REDO_COMMAND","id":"canRedo","priority":0,"result":false}],"history":{"delay":300},"name":"deleting backward and forward","steps":[{"update":[{"do":"append","key":"root","nodes":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"abcdef","type":"text","version":1}],"direction":null,"format":"","indent":0,"textFormat":0,"textStyle":"","type":"paragraph","version":1}]}],"now":0},{"update":[{"anchor":["2",6,"text"],"do":"select"}],"now":0},{"update":[{"anchor":["2",5,"text"],"do":"select","focus":["2",6,"text"]},{"args":[],"do":"selection","method":"removeText"}],"now":10},{"update":[{"anchor":["2",4,"text"],"do":"select","focus":["2",5,"text"]},{"args":[],"do":"selection","method":"removeText"}],"now":20},{"update":[{"anchor":["2",2,"text"],"do":"select","focus":["2",3,"text"]},{"args":[],"do":"selection","method":"removeText"}],"now":30},{"update":[{"anchor":["2",2,"text"],"do":"select","focus":["2",3,"text"]},{"args":[],"do":"selection","method":"removeText"}],"now":40},{"dispatch":"UNDO_COMMAND","now":50},{"dispatch":"UNDO_COMMAND","now":60},{"dispatch":"UNDO_COMMAND","now":70}],"results":[{"log":[],"selection":null,"text":"abcdef","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcdef\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcdef\"}]}]}}"},{"log":[],"selection":["range",["2",6,"text"],["2",6,"text"],0,""],"text":"abcdef","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcdef\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcdef\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",5,"text"],["2",5,"text"],0,""],"text":"abcde","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcde\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcde\"}]}]}}"},{"log":[],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"abcd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcd\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"abd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abd\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"abd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abd\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"abcd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcd\"}]}]}}"},{"log":[["handler","canRedo",true],["handler","canUndo",false],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",6,"text"],["2",6,"text"],0,""],"text":"abcdef","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcdef\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcdef\"}]}]}}"}]}, +{"handlers":[{"command":"CAN_UNDO_COMMAND","id":"canUndo","priority":0,"result":false},{"command":"CAN_REDO_COMMAND","id":"canRedo","priority":0,"result":false}],"history":{"delay":300},"name":"paste and cut are their own entries","steps":[{"update":[{"do":"append","key":"root","nodes":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"ab","type":"text","version":1}],"direction":null,"format":"","indent":0,"textFormat":0,"textStyle":"","type":"paragraph","version":1}]}],"now":0},{"update":[{"anchor":["2",2,"text"],"do":"select"}],"now":0},{"now":10,"update":[{"args":["c"],"do":"selection","method":"insertText"}]},{"now":20,"tag":["paste"],"update":[{"args":["P"],"do":"selection","method":"insertText"}]},{"now":30,"update":[{"args":["d"],"do":"selection","method":"insertText"}]},{"now":40,"tag":["cut"],"update":[{"anchor":["2",0,"text"],"do":"select","focus":["2",1,"text"]},{"args":[],"do":"selection","method":"removeText"}]},{"now":50,"update":[{"args":["e"],"do":"selection","method":"insertText"}]},{"dispatch":"UNDO_COMMAND","now":60},{"dispatch":"UNDO_COMMAND","now":70},{"dispatch":"UNDO_COMMAND","now":80},{"dispatch":"UNDO_COMMAND","now":90}],"results":[{"log":[],"selection":null,"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abc\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"abcP","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcP\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcP\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",5,"text"],["2",5,"text"],0,""],"text":"abcPd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcPd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcPd\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",0,"text"],["2",0,"text"],0,""],"text":"bcPd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"bcPd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"bcPd\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",1,"text"],["2",1,"text"],0,""],"text":"ebcPd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ebcPd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ebcPd\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",0,"text"],["2",0,"text"],0,""],"text":"bcPd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"bcPd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"bcPd\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",5,"text"],["2",5,"text"],0,""],"text":"abcPd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcPd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcPd\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"abcP","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcP\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcP\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abc\"}]}]}}"}]}, +{"handlers":[{"command":"CAN_UNDO_COMMAND","id":"canUndo","priority":0,"result":false},{"command":"CAN_REDO_COMMAND","id":"canRedo","priority":0,"result":false}],"history":{"delay":300},"name":"a composition that changes nothing merges","steps":[{"update":[{"do":"append","key":"root","nodes":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"ab","type":"text","version":1}],"direction":null,"format":"","indent":0,"textFormat":0,"textStyle":"","type":"paragraph","version":1}]}],"now":0},{"update":[{"anchor":["2",2,"text"],"do":"select"}],"now":0},{"now":1000,"update":[{"args":["c"],"do":"selection","method":"insertText"}]},{"now":2000,"tag":["composition-start"],"update":[{"args":["x"],"do":"selection","method":"insertText"}]},{"now":3000,"tag":["composition-end"],"update":[{"anchor":["2",3,"text"],"do":"select","focus":["2",4,"text"]},{"args":[],"do":"selection","method":"removeText"}]},{"now":3010,"update":[{"args":["d"],"do":"selection","method":"insertText"}]},{"dispatch":"UNDO_COMMAND","now":4000},{"dispatch":"UNDO_COMMAND","now":4010}],"results":[{"log":[],"selection":null,"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abc\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"abcx","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcx\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcx\"}]}]}}"},{"log":[],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abc\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"abcd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abcd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abcd\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abc\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abc\"}]}]}}"}]}, +{"handlers":[{"command":"CAN_UNDO_COMMAND","id":"canUndo","priority":0,"result":false},{"command":"CAN_REDO_COMMAND","id":"canRedo","priority":0,"result":false}],"history":{"delay":300},"name":"CLEAR_HISTORY_COMMAND and CLEAR_EDITOR_COMMAND","steps":[{"update":[{"do":"append","key":"root","nodes":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"a","type":"text","version":1}],"direction":null,"format":"","indent":0,"textFormat":0,"textStyle":"","type":"paragraph","version":1}]}],"now":0},{"update":[{"anchor":["2",1,"text"],"do":"select"}],"now":0},{"now":1000,"update":[{"args":["b"],"do":"selection","method":"insertText"}]},{"now":2000,"update":[{"args":["c"],"do":"selection","method":"insertText"}]},{"dispatch":"UNDO_COMMAND","now":2100},{"dispatch":"CLEAR_HISTORY_COMMAND","now":2200},{"dispatch":"UNDO_COMMAND","now":2300},{"dispatch":"REDO_COMMAND","now":2400},{"now":3000,"update":[{"args":["d"],"do":"selection","method":"insertText"}]},{"now":4000,"update":[{"args":["e"],"do":"selection","method":"insertText"}]},{"dispatch":"CLEAR_EDITOR_COMMAND","now":4100},{"dispatch":"UNDO_COMMAND","now":4200}],"results":[{"log":[],"selection":null,"text":"a","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"a\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"a\"}]}]}}"},{"log":[],"selection":["range",["2",1,"text"],["2",1,"text"],0,""],"text":"a","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"a\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"a\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abc","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abc\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abc\"}]}]}}"},{"log":[["handler","canRedo",true],["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[["handler","canRedo",false],["handler","canUndo",false],["dispatched","CLEAR_HISTORY_COMMAND",true]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[["dispatched","REDO_COMMAND",true]],"selection":["range",["2",2,"text"],["2",2,"text"],0,""],"text":"ab","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"ab\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"ab\"}]}]}}"},{"log":[],"selection":["range",["2",3,"text"],["2",3,"text"],0,""],"text":"abd","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abd\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abd\"}]}]}}"},{"log":[["handler","canUndo",true]],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"abde","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abde\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abde\"}]}]}}"},{"log":[["dispatched","CLEAR_EDITOR_COMMAND",false]],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"abde","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abde\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abde\"}]}]}}"},{"log":[["dispatched","UNDO_COMMAND",true]],"selection":["range",["2",4,"text"],["2",4,"text"],0,""],"text":"abde","state":"{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"abde\",\"type\":\"text\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"textFormat\":0,\"textStyle\":\"\",\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}","compact":"{\"root\":{\"type\":\"root\",\"children\":[{\"type\":\"paragraph\",\"children\":[{\"type\":\"text\",\"text\":\"abde\"}]}]}}"}]} +]} diff --git a/rust/DESIGN.md b/rust/DESIGN.md index bd2451ffdfd..9e0b905fe97 100644 --- a/rust/DESIGN.md +++ b/rust/DESIGN.md @@ -61,7 +61,7 @@ depends on as the upstream package depends on them: | `code` | `@lexical/code-core` | `utils` | | `overflow` | `@lexical/overflow` | | | `hashtag` | `@lexical/hashtag` | `utils` | -| `history` | `@lexical/history` | `utils` | +| `history` | `@lexical/history` | (upstream lists `utils`, but uses nothing from it) | | `html` | `@lexical/html` | `selection` | | `markdown` | `@lexical/markdown` | `rich-text`, `list`, `link`, `code` | | `file` | `@lexical/file` | | @@ -175,7 +175,7 @@ Each milestone ends with green CI and a report to the Space. | M0 | The `rust/` workspace, JSON as JS writes it, the core nodes (root, paragraph, text, tab, line break), `parseEditorState` and `toJSON` in both forms, the conformance runner, CI | `meta`, `serialization/core`, the core-only cases of `property/documents` (JSON and text) | done | | M1 | The core model: `editor.update` and the commit (dirty nodes, normalization, transforms, listeners, commands, tags), node creation and methods (`splitText`, `mergeWithSibling`, `spliceText`, `insertAfter`, `remove`, `replace`...) with UTF-16 offsets, `RangeSelection` and `NodeSelection` editing, carets, NodeState, slots, the schema facility, `DecoratorNode`, the extension API (`buildEditorFromExtensions`), the parity ledger | `scripts/*`, `fuzz/core`, `file/*` | in progress: the update cycle, `setEditorState`, commands, transforms, listeners, the node methods, carets, selection reading and `RangeSelection`/`NodeSelection` editing are done (`scripts/*` and `fuzz/core` pass), and the platform's `Selection.modify` is an editor hook (`SelectionMovement`); next, NodeState, `DecoratorNode`, the extension API | | P | Desktop playgrounds (`apps/`): `lexical-playground-rune` on Rune, then `lexical-playground-gpui` on GPUI, sharing `lexical-playground-core`; they grow with each milestone below | the vectors, replayed in the app; a session exported from the app replays in JS (`replay.test.ts`); in CI, a GPUI session typed in a real window replays in JS | both apps run on macOS with typing, Return, deletion, formatting, input methods and the Parity panel; they grow with each milestone | -| M2 | `rich-text` (headings, quotes, and the command handlers a text surface dispatches: typing, Enter, Backspace, formatting) and `history` (undo), which the playgrounds need first; then the node packages `list`, `link`, `code`, `overflow`, `hashtag`, with `selection` and `utils` | `serialization/*`, `property/documents` (JSON and text), `entities/hashtag-*` | | +| M2 | `rich-text` (headings, quotes, and the command handlers a text surface dispatches: typing, Enter, Backspace, formatting) and `history` (undo), which the playgrounds need first; then the node packages `list`, `link`, `code`, `overflow`, `hashtag`, with `selection` and `utils` | `serialization/*`, `property/documents` (JSON and text), `entities/hashtag-*`, `scripts/history` | in progress: `history` is done (`scripts/history` passes) | | M3 | The commands `property/sequences` dispatches | `property/sequences` | | | M4 | `html`: a DOM abstraction over an HTML parser, CSS declarations, export and import | `html/*`, `css/*`, `property/documents` (HTML) | | | M5 | `markdown` | `markdown/import-export`, `property/documents` (Markdown) | | diff --git a/rust/PARITY.md b/rust/PARITY.md index 5b95781317f..ccefbe8f1c0 100644 --- a/rust/PARITY.md +++ b/rust/PARITY.md @@ -27,6 +27,7 @@ updated with each milestone. | Editor state | `LexicalEditorState.ts` (`toJSON`, `read`, `isEmpty`, `_parsed`) | `editor_state` | `serialization/core` | | Parsing | `parseEditorState`, `$parseSerializedNode` (`LexicalUpdates.ts`) | `updates` | `serialization/core`, `property/documents` | | Editor | `createEditor` (node registration), `update`, `read`, `getEditorState`, `setEditorState`, `registerNodeTransform`, `registerUpdateListener`, `registerTextContentListener`, `registerCommand`, `dispatchCommand`, `createCommand` | `editor` | `scripts/*` | +| History (the `history` feature) | `packages/lexical-history/src/index.ts`: `registerHistory` (with `dateNow`, `onHistoryStateChange` and `maxDepth`), `createEmptyHistoryState`, `createMergeActionGetter`, `getChangeType`, `getDirtyNodes`, `isTextNodeUnchanged`, `areNodesUnchanged`, `getInverseEntry`, `getUndoEntry`, `undo`, `redo`, `clearHistory`; the commands it handles and dispatches from `LexicalCommands.ts` (`UNDO_COMMAND`, `REDO_COMMAND`, `CAN_UNDO_COMMAND`, `CAN_REDO_COMMAND`, `CLEAR_HISTORY_COMMAND`, `CLEAR_EDITOR_COMMAND`) and its tags from `LexicalUpdateTags.ts` | `history`, `commands`, `editor` | `scripts/history`; unit tests for a history shared by two editors (upstream's #8623 test) and `maxDepth` | | UTF-16 offsets | `String#length`, `String#slice` | `utf16` | through the text methods; unit tests against Node | | Upstream version | `meta.json` | `UPSTREAM_VERSION` | `meta` | @@ -56,9 +57,16 @@ touches: `$getCommonAncestorAcrossSlots`). Without slots, NodeState or the text edit log, `$removeTextFromCaretRange` never meets a slot host and carries no NodeState or deletion records. +- `HistoryExtension` and `SharedHistoryExtension` (`@lexical/history`), which + need the extension API. Sharing one `HistoryState` between editors through + `register_history` works. `register_history` takes the delay and + `maxDepth` as values, where JS also takes signals, and `maxDepth` as a + `usize`. - Composition, the DOM reconciler, mutation listeners, selection-change notifications and `$onUpdate` callbacks (none of which a headless editor - runs, except `$onUpdate`), and `RootNode.__cachedText`. + runs, except `$onUpdate`), and `RootNode.__cachedText`. So `history` passes + `false` where JS passes `editor.isComposing()`, as a headless JS editor + does; the `composition-start` and `composition-end` tags work as in JS. ## Documented exceptions diff --git a/rust/conformance/Cargo.toml b/rust/conformance/Cargo.toml index 003cb58958c..d5d5c8f301c 100644 --- a/rust/conformance/Cargo.toml +++ b/rust/conformance/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true repository.workspace = true [dependencies] -lexical.workspace = true +lexical = { workspace = true, features = ["history"] } [lints] workspace = true diff --git a/rust/conformance/src/scripts.rs b/rust/conformance/src/scripts.rs index 72b52f7a9e1..46262ee088f 100644 --- a/rust/conformance/src/scripts.rs +++ b/rust/conformance/src/scripts.rs @@ -5,22 +5,30 @@ //! log of what listeners, transforms and command handlers saw must match //! what JS recorded. //! +//! A script with `history: {delay}` registers `@lexical/history` on the +//! session's clock, which starts at 0 and which a step's `now` sets; JS +//! makes `Date.now()` return it while a step runs. `dispatch` and handlers +//! name Lexical's own commands (`UNDO_COMMAND`, `CAN_UNDO_COMMAND`...) as JS +//! exports them, and any other name is a command the script makes. +//! //! A script that uses an action the port can't run yet (selection editing) //! is counted as waiting on that part of `lexical`, never as passing. use crate::case::{Case, Outcome, same}; +use lexical::history::{create_empty_history_state, register_history}; use lexical::json::{Object, Value}; use lexical::{ + CAN_REDO_COMMAND, CAN_UNDO_COMMAND, CLEAR_EDITOR_COMMAND, CLEAR_HISTORY_COMMAND, COMMAND_PRIORITY_EDITOR, CaretDirection, CreateEditorArgs, Direction, Editor, LexicalCommand, - LexicalError, NodeRef, PointCaret, PointType, RangeSelection, SelectionRef, SliceMode, - TextOffset, TextPointCaretSlice, UpdateOptions, Which, caret_range_from_selection, - create_command, create_editor, create_node_selection, create_range_selection, get_caret_range, - get_child_caret, get_node_by_key, get_root, get_selection, get_sibling_caret, - get_text_point_caret, insert_nodes, normalize_caret, parse_serialized_node, - remove_text_from_caret_range, reset_random_key, select_all, set_selection, - set_selection_from_caret_range, utf16, + LexicalError, NodeRef, PointCaret, PointType, REDO_COMMAND, RangeSelection, SelectionRef, + SliceMode, TextOffset, TextPointCaretSlice, UNDO_COMMAND, Unregister, UpdateOptions, Which, + caret_range_from_selection, create_command, create_editor, create_node_selection, + create_range_selection, get_caret_range, get_child_caret, get_node_by_key, get_root, + get_selection, get_sibling_caret, get_text_point_caret, insert_nodes, normalize_caret, + parse_serialized_node, remove_text_from_caret_range, reset_random_key, select_all, + set_selection, set_selection_from_caret_range, utf16, }; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::rc::Rc; @@ -36,13 +44,74 @@ impl Ctx { self.log.borrow_mut().push(Value::Array(entry)); } - /// `command(ctx, name)`: one command per name. - fn command(&self, name: &str) -> LexicalCommand { - self.commands - .borrow_mut() - .entry(name.to_owned()) - .or_insert_with(|| create_command(name)) - .clone() + /// `command(ctx, name)`: one of Lexical's own commands + /// (`CORE_COMMANDS`), or one command per other name. + fn command(&self, name: &str) -> ScriptCommand { + match name { + "CAN_REDO_COMMAND" => ScriptCommand::Bool(CAN_REDO_COMMAND), + "CAN_UNDO_COMMAND" => ScriptCommand::Bool(CAN_UNDO_COMMAND), + "CLEAR_EDITOR_COMMAND" => ScriptCommand::Void(CLEAR_EDITOR_COMMAND), + "CLEAR_HISTORY_COMMAND" => ScriptCommand::Void(CLEAR_HISTORY_COMMAND), + "REDO_COMMAND" => ScriptCommand::Void(REDO_COMMAND), + "UNDO_COMMAND" => ScriptCommand::Void(UNDO_COMMAND), + _ => ScriptCommand::Json( + self.commands + .borrow_mut() + .entry(name.to_owned()) + .or_insert_with(|| create_command(name)) + .clone(), + ), + } + } +} + +/// A command a script names, by its payload type: Lexical's own take +/// nothing (`void`) or a boolean, and those the script makes take any JSON. +enum ScriptCommand { + Void(LexicalCommand<()>), + Bool(LexicalCommand), + Json(LexicalCommand), +} + +impl ScriptCommand { + /// `editor.dispatchCommand(command, payload)`. A payload the command + /// doesn't take is an error, which JS would pass on as it is. + fn dispatch(&self, editor: &Editor, payload: Value) -> Result { + match self { + ScriptCommand::Void(command) if payload.is_null() => { + editor.dispatch_command(command, ()) + } + ScriptCommand::Bool(command) if payload.as_bool().is_some() => { + editor.dispatch_command(command, payload.as_bool() == Some(true)) + } + ScriptCommand::Json(command) => editor.dispatch_command(command, payload), + _ => Err(err(format!( + "{payload} is not a payload this command takes" + ))), + } + } + + /// `editor.registerCommand(command, listener, priority)`, the listener + /// getting the payload as JSON (`null` for `void`). + fn register( + &self, + editor: &Editor, + listener: impl Fn(&Value, &Editor) -> Result + 'static, + priority: u8, + ) -> Unregister { + match self { + ScriptCommand::Void(command) => editor.register_command( + command, + move |_, editor| listener(&Value::Null, editor), + priority, + ), + ScriptCommand::Bool(command) => editor.register_command( + command, + move |payload, editor| listener(&(*payload).into(), editor), + priority, + ), + ScriptCommand::Json(command) => editor.register_command(command, listener, priority), + } } } @@ -440,7 +509,7 @@ fn run_action(action: &Value, ctx: &Ctx, editor: &Editor) -> Result<(), LexicalE Some("dispatch") => { let name = action.get("command").and_then(Value::as_str).unwrap_or(""); let payload = action.get("payload").cloned().unwrap_or(Value::Null); - let handled = editor.dispatch_command(&ctx.command(name), payload)?; + let handled = ctx.command(name).dispatch(editor, payload)?; ctx.log(vec!["dispatched".into(), name.into(), handled.into()]); Ok(()) } @@ -560,6 +629,19 @@ fn transform_name(name: &str) -> Option<&'static str> { pub struct Session { editor: Editor, ctx: Rc, + /// The session's clock, in milliseconds: what the history merges by. + /// It starts at 0, and a step's `now` sets it. + clock: Rc>, + /// Unregisters the history, whose entries hold the editor. + unregister_history: Option, +} + +impl Drop for Session { + fn drop(&mut self) { + if let Some(unregister) = self.unregister_history.take() { + unregister(); + } + } } /// One step's result. @@ -609,8 +691,8 @@ impl Session { .cloned() .unwrap_or_default(); let ctx = ctx.clone(); - let _unregister = editor.register_command( - &command, + let _unregister = command.register( + &editor, move |payload: &Value, editor: &Editor| { ctx.log(vec!["handler".into(), id.as_str().into(), payload.clone()]); for action in &actions { @@ -650,7 +732,33 @@ impl Session { log.log(vec!["textContent".into(), text.into()]); }); } - Ok(Session { editor, ctx }) + // `registerHistory(editor, createEmptyHistoryState(), delay)` on the + // session's clock. + let clock = Rc::new(Cell::new(0.0)); + let unregister_history = match script.get("history") { + None => None, + Some(history) => { + let delay = history + .get("delay") + .and_then(Value::as_f64) + .ok_or("history needs a delay")?; + let clock = clock.clone(); + Some(register_history( + &editor, + &create_empty_history_state(), + delay, + Some(Rc::new(move || clock.get())), + None, + None, + )) + } + }; + Ok(Session { + editor, + ctx, + clock, + unregister_history, + }) } /// The session's editor. @@ -663,6 +771,9 @@ impl Session { /// forms. pub fn run(&self, step: &Value, snapshot: bool) -> Result { let (editor, ctx) = (&self.editor, &self.ctx); + if let Some(now) = step.get("now").and_then(Value::as_f64) { + self.clock.set(now); + } ctx.log.borrow_mut().clear(); let outcome = if let Some(actions) = step.get("update").and_then(Value::as_array) { let tags = step @@ -692,8 +803,8 @@ impl Session { } else { let name = step.str("dispatch")?; let payload = step.get("payload").cloned().unwrap_or(Value::Null); - editor - .dispatch_command(&ctx.command(name), payload) + ctx.command(name) + .dispatch(editor, payload) .map(|handled| ctx.log(vec!["dispatched".into(), name.into(), handled.into()])) }; // A dispatch outside an update commits at the next read. diff --git a/rust/conformance/src/suites/mod.rs b/rust/conformance/src/suites/mod.rs index 3229b104ccf..e25fa60f03e 100644 --- a/rust/conformance/src/suites/mod.rs +++ b/rust/conformance/src/suites/mod.rs @@ -49,6 +49,11 @@ pub fn all() -> Vec { variant: None, check: crate::scripts::check, }, + Suite { + file: "scripts/history", + variant: None, + check: crate::scripts::check, + }, Suite { file: "fuzz/core", variant: None, diff --git a/rust/lexical-editor/Cargo.toml b/rust/lexical-editor/Cargo.toml index 5961b8edf06..e844f32b874 100644 --- a/rust/lexical-editor/Cargo.toml +++ b/rust/lexical-editor/Cargo.toml @@ -16,6 +16,9 @@ publish = false # (rust/DESIGN.md, "Packages and features"). The core (`lexical`) is always # built. The default build has no third-party dependencies. [features] +# @lexical/history. Upstream it depends on @lexical/utils, which isn't ported; +# it needs nothing from it. +history = [] [lints] workspace = true diff --git a/rust/lexical-editor/src/commands.rs b/rust/lexical-editor/src/commands.rs new file mode 100644 index 00000000000..463f8035fb8 --- /dev/null +++ b/rust/lexical-editor/src/commands.rs @@ -0,0 +1,34 @@ +//! `LexicalCommands.ts`: the core's own commands, which packages handle and +//! dispatch. Only those a ported package uses are here. +//! +//! They are constants, compared by id as every command is; their ids count +//! down from `u64::MAX`, where `create_command`'s count up from 1. + +use crate::editor::LexicalCommand; + +/// `UNDO_COMMAND`: dispatched on undo (⌘Z on macOS, Ctrl+Z elsewhere). +pub const UNDO_COMMAND: LexicalCommand<()> = LexicalCommand::constant(u64::MAX, "UNDO_COMMAND"); + +/// `REDO_COMMAND`: dispatched on redo (⇧⌘Z on macOS, Ctrl+Shift+Z or Ctrl+Y +/// elsewhere). +pub const REDO_COMMAND: LexicalCommand<()> = LexicalCommand::constant(u64::MAX - 1, "REDO_COMMAND"); + +/// `CLEAR_EDITOR_COMMAND`: dispatched to clear all editor content. +pub const CLEAR_EDITOR_COMMAND: LexicalCommand<()> = + LexicalCommand::constant(u64::MAX - 2, "CLEAR_EDITOR_COMMAND"); + +/// `CLEAR_HISTORY_COMMAND`: dispatched to clear the undo and redo stacks. +pub const CLEAR_HISTORY_COMMAND: LexicalCommand<()> = + LexicalCommand::constant(u64::MAX - 3, "CLEAR_HISTORY_COMMAND"); + +/// `CAN_REDO_COMMAND`: dispatched when whether redo is possible changes; +/// the payload is whether it is. JS deprecates it (v0.49.0) for +/// `HistoryExtension`'s `canRedo` signal, which isn't ported yet. +pub const CAN_REDO_COMMAND: LexicalCommand = + LexicalCommand::constant(u64::MAX - 4, "CAN_REDO_COMMAND"); + +/// `CAN_UNDO_COMMAND`: dispatched when whether undo is possible changes; +/// the payload is whether it is. JS deprecates it (v0.49.0) for +/// `HistoryExtension`'s `canUndo` signal, which isn't ported yet. +pub const CAN_UNDO_COMMAND: LexicalCommand = + LexicalCommand::constant(u64::MAX - 5, "CAN_UNDO_COMMAND"); diff --git a/rust/lexical-editor/src/editor.rs b/rust/lexical-editor/src/editor.rs index 8b827a120ad..7d938c7a9f2 100644 --- a/rust/lexical-editor/src/editor.rs +++ b/rust/lexical-editor/src/editor.rs @@ -65,10 +65,32 @@ impl NodeClass { } } +/// `HISTORIC_TAG` (`LexicalUpdateTags.ts`): the update applies a state from +/// the undo or redo stack. +pub const HISTORIC_TAG: &str = "historic"; + +/// `HISTORY_PUSH_TAG` (`LexicalUpdateTags.ts`): the update starts a new +/// history entry. +pub const HISTORY_PUSH_TAG: &str = "history-push"; + /// `HISTORY_MERGE_TAG` (`LexicalUpdateTags.ts`): the update joins the /// previous history entry. pub const HISTORY_MERGE_TAG: &str = "history-merge"; +/// `PASTE_TAG` (`LexicalUpdateTags.ts`): the update is a paste. +pub const PASTE_TAG: &str = "paste"; + +/// `CUT_TAG` (`LexicalUpdateTags.ts`): the update is a cut. +pub const CUT_TAG: &str = "cut"; + +/// `COMPOSITION_START_TAG` (`LexicalUpdateTags.ts`): the update starts an +/// input method composition. +pub const COMPOSITION_START_TAG: &str = "composition-start"; + +/// `COMPOSITION_END_TAG` (`LexicalUpdateTags.ts`): the update ends an input +/// method composition. +pub const COMPOSITION_END_TAG: &str = "composition-end"; + /// `CreateEditorArgs`: the node classes to register besides the core ones. #[derive(Clone, Default)] pub struct CreateEditorArgs { @@ -109,10 +131,18 @@ pub type Unregister = Box; /// with the same name are different commands. pub struct LexicalCommand

{ id: u64, - name: Rc, + name: CommandName, _payload: PhantomData, } +/// A command's name: shared when [`create_command`] makes it, static for +/// the core's own commands (`commands`), which are constants. +#[derive(Clone)] +enum CommandName { + Static(&'static str), + Shared(Rc), +} + impl

Clone for LexicalCommand

{ fn clone(&self) -> Self { Self { @@ -124,8 +154,21 @@ impl

Clone for LexicalCommand

{ } impl

LexicalCommand

{ + /// One of the core's own commands, with an id [`create_command`] never + /// gives (they count up from 1). + pub(crate) const fn constant(id: u64, name: &'static str) -> Self { + Self { + id, + name: CommandName::Static(name), + _payload: PhantomData, + } + } + pub fn name(&self) -> &str { - &self.name + match &self.name { + CommandName::Static(name) => name, + CommandName::Shared(name) => name, + } } pub(crate) fn id(&self) -> u64 { self.id @@ -141,7 +184,7 @@ pub fn create_command

(name: &str) -> LexicalCommand

{ let id = NEXT_COMMAND_ID.with(|c| c.replace(c.get() + 1)); LexicalCommand { id, - name: name.into(), + name: CommandName::Shared(name.into()), _payload: PhantomData, } } diff --git a/rust/lexical-editor/src/history.rs b/rust/lexical-editor/src/history.rs new file mode 100644 index 00000000000..de32a0a4f97 --- /dev/null +++ b/rust/lexical-editor/src/history.rs @@ -0,0 +1,889 @@ +//! `@lexical/history` (`packages/lexical-history/src/index.ts`): undo and +//! redo. [`register_history`] records the editor's states on an undo stack, +//! merging an update into the entry before it when it continues the same +//! kind of change (typing, deleting) within a delay, and handles +//! [`UNDO_COMMAND`], [`REDO_COMMAND`], [`CLEAR_HISTORY_COMMAND`] and +//! [`CLEAR_EDITOR_COMMAND`], dispatching [`CAN_UNDO_COMMAND`] and +//! [`CAN_REDO_COMMAND`] as the stacks fill and empty. +//! +//! `HistoryExtension` and `SharedHistoryExtension` wait on the extension +//! API; a [`HistoryState`] passed to `register_history` for several editors +//! is shared history all the same. The delay and the depth are plain values +//! where JS also takes signals. + +use crate::commands::{ + CAN_REDO_COMMAND, CAN_UNDO_COMMAND, CLEAR_EDITOR_COMMAND, CLEAR_HISTORY_COMMAND, REDO_COMMAND, + UNDO_COMMAND, +}; +use crate::editor::{ + COMMAND_PRIORITY_EDITOR, COMPOSITION_END_TAG, COMPOSITION_START_TAG, CUT_TAG, EditorInner, + HISTORIC_TAG, HISTORY_MERGE_TAG, HISTORY_PUSH_TAG, LexicalCommand, PASTE_TAG, Unregister, + UpdateListenerPayload, +}; +use crate::json::Value; +use crate::node::{LexicalNode, NodeKey, NodeRef}; +use crate::nodes::root::RootNode; +use crate::ordered::OrderedSet; +use crate::selection::{PointType, RangeSelection}; +use crate::{Editor, EditorState, LexicalError, utf16}; +use std::cell::RefCell; +use std::rc::{Rc, Weak}; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// `MergeAction`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum MergeAction { + /// `HISTORY_MERGE`: the update replaces the current entry. + HistoryMerge, + /// `HISTORY_PUSH`: the current entry goes on the undo stack. + HistoryPush, + /// `DISCARD_HISTORY_CANDIDATE`: the update isn't recorded. + DiscardHistoryCandidate, +} + +/// `ChangeType`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ChangeType { + Other, + ComposingCharacter, + InsertCharacterAfterSelection, + DeleteCharacterBeforeSelection, + DeleteCharacterAfterSelection, +} + +/// `HistoryStateEntry`: an editor and one of its states. +#[derive(Clone)] +pub struct HistoryStateEntry { + pub editor: Editor, + pub editor_state: EditorState, +} + +/// `HistoryState`: the `current` entry, and the undo and redo stacks of +/// previous and future entries. [`create_empty_history_state`] makes an +/// empty one, shared through an `Rc` as JS shares the object; pass the same +/// one to [`register_history`] for several editors to share their history. +#[derive(Clone, Default)] +pub struct HistoryState { + pub current: Option, + pub redo_stack: Vec, + pub undo_stack: Vec, +} + +/// The clock [`register_history`] merges by: milliseconds, as `Date.now()` +/// gives them. +pub type DateNow = Rc f64>; + +/// What [`register_history`] calls when the history state changes +/// (`onHistoryStateChange`). It gets the state borrowed, so it must not +/// change it. +pub type HistoryStateChange = Rc; + +/// `Date.now()`: milliseconds since the Unix epoch, the default clock. +fn system_date_now() -> f64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0.0, |d| d.as_millis() as f64) +} + +/// `getDirtyNodes(editorState, dirtyLeaves, dirtyElements)`: the dirty +/// leaves, then the elements marked dirty themselves, but not the root. +fn get_dirty_nodes( + editor_state: &EditorState, + dirty_leaves: &[NodeKey], + dirty_elements: &[(NodeKey, bool)], +) -> Vec> { + let node_map = editor_state.0.node_map.borrow(); + let mut nodes: Vec> = dirty_leaves + .iter() + .filter_map(|key| node_map.get(key).cloned()) + .collect(); + for (key, intentionally_marked_as_dirty) in dirty_elements { + if !intentionally_marked_as_dirty { + continue; + } + if let Some(dirty_element) = node_map.get(key) + && !dirty_element.is::() + { + nodes.push(dirty_element.clone()); + } + } + nodes +} + +fn range_selection(editor_state: &EditorState) -> Option { + editor_state.selection().and_then(|s| s.range()) +} + +/// `getChangeType(prevEditorState, nextEditorState, dirtyLeavesSet, +/// dirtyElementsSet, isComposing)`. +fn get_change_type( + prev_editor_state: &EditorState, + next_editor_state: &EditorState, + dirty_leaves: &[NodeKey], + dirty_elements: &[(NodeKey, bool)], + is_composing: bool, +) -> ChangeType { + // An update listener always has a previous state, which JS checks for + // `null` here. + if dirty_leaves.is_empty() && dirty_elements.is_empty() && !is_composing { + return ChangeType::Other; + } + if is_composing { + return ChangeType::ComposingCharacter; + } + let (Some(next_selection), Some(prev_selection)) = ( + range_selection(next_editor_state), + range_selection(prev_editor_state), + ) else { + return ChangeType::Other; + }; + if !prev_selection.is_collapsed() || !next_selection.is_collapsed() { + return ChangeType::Other; + } + let dirty_nodes = get_dirty_nodes(next_editor_state, dirty_leaves, dirty_elements); + if dirty_nodes.is_empty() { + return ChangeType::Other; + } + let next_node_map = next_editor_state.0.node_map.borrow(); + let prev_node_map = prev_editor_state.0.node_map.borrow(); + + // Catching the case when inserting new text node into an element (e.g. + // first char in paragraph/list), or after existing node. + if dirty_nodes.len() > 1 { + let next_anchor_node = next_node_map.get(&next_selection.anchor.key); + // Read from the next state, as JS does. + let prev_anchor_node = next_node_map.get(&prev_selection.anchor.key); + if let (Some(next_anchor_node), Some(_)) = (next_anchor_node, prev_anchor_node) + && !prev_node_map.contains_key(next_anchor_node.get_key()) + && let Some(text) = next_anchor_node.text() + && utf16::len(&text.text) == 1 + && next_selection.anchor.offset == 1 + { + return ChangeType::InsertCharacterAfterSelection; + } + return ChangeType::Other; + } + + let next_dirty_node = &dirty_nodes[0]; + let prev_dirty_node = prev_node_map.get(next_dirty_node.get_key()); + let (Some(prev_text), Some(next_text)) = ( + prev_dirty_node.and_then(|n| n.text()), + next_dirty_node.text(), + ) else { + return ChangeType::Other; + }; + if prev_text.mode != next_text.mode || prev_text.text == next_text.text { + return ChangeType::Other; + } + let next_anchor = &next_selection.anchor; + let prev_anchor = &prev_selection.anchor; + if next_anchor.key != prev_anchor.key || next_anchor.point_type != PointType::Text { + return ChangeType::Other; + } + let next_anchor_offset = next_anchor.offset as i64; + let prev_anchor_offset = prev_anchor.offset as i64; + let text_diff = utf16::len(&next_text.text) as i64 - utf16::len(&prev_text.text) as i64; + if text_diff == 1 && prev_anchor_offset == next_anchor_offset - 1 { + return ChangeType::InsertCharacterAfterSelection; + } + if text_diff == -1 && prev_anchor_offset == next_anchor_offset + 1 { + return ChangeType::DeleteCharacterBeforeSelection; + } + if text_diff == -1 && prev_anchor_offset == next_anchor_offset { + return ChangeType::DeleteCharacterAfterSelection; + } + ChangeType::Other +} + +fn get_node(editor_state: &EditorState, key: &str) -> Option> { + editor_state.0.node_map.borrow().get(key).cloned() +} + +/// `JSON.stringify(node.exportJSON())`, read in `editor_state`. +fn export_json_text(editor_state: &EditorState, node: &Rc) -> String { + editor_state.read(|| Value::Object(node.export_json(false)).to_string()) +} + +/// `isTextNodeUnchanged(key, prevEditorState, nextEditorState)`. +fn is_text_node_unchanged( + key: &str, + prev_editor_state: &EditorState, + next_editor_state: &EditorState, +) -> bool { + let prev_node = get_node(prev_editor_state, key); + let next_node = get_node(next_editor_state, key); + let is_deleting_line = match ( + range_selection(prev_editor_state), + range_selection(next_editor_state), + ) { + (Some(prev), Some(next)) => { + prev.anchor.point_type == PointType::Element + && prev.focus.point_type == PointType::Element + && next.anchor.point_type == PointType::Text + && next.focus.point_type == PointType::Text + } + _ => false, + }; + if !is_deleting_line + && let (Some(prev_node), Some(next_node)) = (prev_node, next_node) + && prev_node.text().is_some() + && next_node.text().is_some() + && prev_node.base().parent == next_node.base().parent + { + // This has the assumption that object key order won't change if the + // content did not change, which should normally be safe given the + // manner in which nodes and exportJSON are typically implemented. + return export_json_text(prev_editor_state, &prev_node) + == export_json_text(next_editor_state, &next_node); + } + false +} + +/// `$serializeNode(node)`: the node's JSON and, for an element, its +/// children's keys. +fn serialize_node(node: &Rc) -> String { + let children_keys = if node.element().is_some() { + Value::Array( + NodeRef::new(node.get_key()) + .get_children() + .iter() + .map(|child| child.get_key().into()) + .collect(), + ) + } else { + Value::Null + }; + Value::Array(vec![Value::Object(node.export_json(false)), children_keys]).to_string() +} + +/// `areNodesUnchanged(keys, prevEditorState, nextEditorState)`. +fn are_nodes_unchanged( + keys: &OrderedSet, + prev_editor_state: &EditorState, + next_editor_state: &EditorState, +) -> bool { + for key in keys.iter() { + match ( + get_node(prev_editor_state, key), + get_node(next_editor_state, key), + ) { + (None, None) => continue, + (Some(prev_node), Some(next_node)) if Rc::ptr_eq(&prev_node, &next_node) => continue, + (Some(prev_node), Some(next_node)) => { + if prev_editor_state.read(|| serialize_node(&prev_node)) + != next_editor_state.read(|| serialize_node(&next_node)) + { + return false; + } + } + _ => return false, + } + } + true +} + +/// The state of `createMergeActionGetter(editor, delay, dateNow)`'s +/// closure; [`get`](Self::get) is the function it returns. +struct MergeActionGetter { + delay: f64, + date_now: DateNow, + prev_change_time: f64, + prev_change_type: ChangeType, + composition_start_time: f64, + composition_start_change_type: ChangeType, + composition_start_state: Option, + /// Include every update in the composition, not only its final + /// deletion. + composition_dirty_keys: OrderedSet, +} + +impl MergeActionGetter { + /// `createMergeActionGetter(editor, delay, dateNow)`. + fn new(delay: f64, date_now: DateNow) -> Self { + let prev_change_time = date_now(); + Self { + delay, + date_now, + prev_change_time, + prev_change_type: ChangeType::Other, + composition_start_time: prev_change_time, + composition_start_change_type: ChangeType::Other, + composition_start_state: None, + composition_dirty_keys: OrderedSet::new(), + } + } + + /// The function `createMergeActionGetter` returns, with + /// `prevEditorState`, `nextEditorState`, `dirtyLeaves`, `dirtyElements` + /// and `tags` from the update listener's payload. + fn get( + &mut self, + editor: &Editor, + current_history_entry: Option<&HistoryStateEntry>, + payload: &UpdateListenerPayload, + ) -> MergeAction { + let next_editor_state = &payload.editor_state; + let dirty_leaves = &payload.dirty_leaves; + let dirty_elements = &payload.dirty_elements; + let tags = &payload.tags; + let has = |tag: &str| tags.iter().any(|t| t == tag); + let change_time = (self.date_now)(); + + if has(COMPOSITION_START_TAG) { + self.composition_start_time = self.prev_change_time; + self.composition_start_change_type = self.prev_change_type; + self.composition_start_state = Some(payload.prev_editor_state.clone()); + self.composition_dirty_keys = OrderedSet::new(); + } + if self.composition_start_state.is_some() { + for key in dirty_leaves { + self.composition_dirty_keys.add(key); + } + for (key, _) in dirty_elements { + self.composition_dirty_keys.add(key); + } + } + + // If applying changes from history stack there's no need to run + // history logic again, as history entries already calculated. + if has(HISTORIC_TAG) { + self.prev_change_type = ChangeType::Other; + self.prev_change_time = change_time; + self.composition_start_state = None; + self.composition_dirty_keys = OrderedSet::new(); + return MergeAction::DiscardHistoryCandidate; + } + + let is_composition_end = has(COMPOSITION_END_TAG); + let mut prev_editor_state = payload.prev_editor_state.clone(); + if is_composition_end && let Some(start) = &self.composition_start_state { + self.prev_change_time = self.composition_start_time; + self.prev_change_type = self.composition_start_change_type; + prev_editor_state = start.clone(); + } + + // A paste or cut is never a continuation of the surrounding typing, + // so it is OTHER: it doesn't merge into the keystrokes before it, and + // as `prev_change_type` it stops the keystrokes after it from + // merging into it, which gives it its own undo entry (#8609). + let change_type = if has(PASTE_TAG) || has(CUT_TAG) { + ChangeType::Other + } else { + // JS passes `editor.isComposing()`. Composition isn't ported + // (rust/PARITY.md), so it is false, as in a headless JS editor. + get_change_type( + &prev_editor_state, + next_editor_state, + dirty_leaves, + dirty_elements, + false, + ) + }; + + let mut is_unchanged_composition = false; + let merge_action = 'merge_action: { + let is_same_editor = + current_history_entry.is_none_or(|entry| entry.editor.ptr_eq(editor)); + let should_push_history = has(HISTORY_PUSH_TAG); + let should_merge_history = + !should_push_history && is_same_editor && has(HISTORY_MERGE_TAG); + + if is_composition_end + && let Some(start) = &self.composition_start_state + && is_same_editor + && !should_push_history + && are_nodes_unchanged(&self.composition_dirty_keys, start, next_editor_state) + { + is_unchanged_composition = true; + break 'merge_action MergeAction::HistoryMerge; + } + + if should_merge_history { + break 'merge_action MergeAction::HistoryMerge; + } + + if change_type == ChangeType::ComposingCharacter { + break 'merge_action MergeAction::DiscardHistoryCandidate; + } + + // JS pushes when there is no previous state, which an update + // listener always has. + + let has_dirty_nodes = !dirty_leaves.is_empty() || !dirty_elements.is_empty(); + if !has_dirty_nodes { + if next_editor_state.selection().is_some() { + break 'merge_action MergeAction::HistoryMerge; + } + break 'merge_action MergeAction::DiscardHistoryCandidate; + } + + if !should_push_history + && change_type != ChangeType::Other + && change_type == self.prev_change_type + && change_time < self.prev_change_time + self.delay + && is_same_editor + { + break 'merge_action MergeAction::HistoryMerge; + } + + // A single node might have been marked as dirty, but not have + // changed due to some node transform reverting the change. An + // explicit HISTORY_PUSH_TAG always asks for its own entry, so it + // must not be merged away here even when the only dirty leaf is + // an unchanged TextNode (#9217). + if !should_push_history + && dirty_leaves.len() == 1 + && is_text_node_unchanged(&dirty_leaves[0], &prev_editor_state, next_editor_state) + { + break 'merge_action MergeAction::HistoryMerge; + } + + MergeAction::HistoryPush + }; + + if is_composition_end { + self.composition_start_state = None; + self.composition_dirty_keys = OrderedSet::new(); + } + if !is_unchanged_composition { + self.prev_change_time = change_time; + self.prev_change_type = change_type; + } + merge_action + } +} + +/// `getInverseEntry(historyStateEntry, current)`: the entry that reverses +/// `history_state_entry`, for the opposite stack. +/// +/// With a single editor this is always `current`, since `current` tracks +/// the live state of that editor. With a shared [`HistoryState`] the stacks +/// interleave entries from several editors, so `current` may belong to an +/// editor that is not about to change; then it is the live state of the +/// entry's own editor. +fn get_inverse_entry( + history_state_entry: &HistoryStateEntry, + current: Option<&HistoryStateEntry>, +) -> Option { + if let Some(current) = current + && current.editor.ptr_eq(&history_state_entry.editor) + { + return Some(current.clone()); + } + let editor = &history_state_entry.editor; + let editor_state = editor.get_editor_state(); + // An empty EditorState can not be restored (setEditorState fails), so + // there is nothing to reverse to. + (!editor_state.is_empty()).then(|| HistoryStateEntry { + editor: editor.clone(), + editor_state, + }) +} + +/// `getUndoEntry(editor, prevEditorState, current)`: the entry an update +/// from `editor` pushes onto the undo stack: `current`, or with a shared +/// [`HistoryState`] whose `current` belongs to another editor, this +/// editor's own state before the update. +fn get_undo_entry( + editor: &Editor, + prev_editor_state: &EditorState, + current: Option<&HistoryStateEntry>, +) -> Option { + let current = current?; + if current.editor.ptr_eq(editor) { + return Some(current.clone()); + } + // An empty EditorState can not be restored (setEditorState fails). + // Skipping it mirrors the way the first update of an editor is not + // undoable. + (!prev_editor_state.is_empty()).then(|| HistoryStateEntry { + editor: editor.clone(), + editor_state: prev_editor_state.clone(), + }) +} + +fn notify(history_state: &RefCell, on_change: Option<&HistoryStateChange>) { + if let Some(on_change) = on_change { + on_change(&history_state.borrow()); + } +} + +/// `redo(editor, historyState, onChange)`. +fn redo( + editor: &Editor, + history_state: &RefCell, + on_change: Option<&HistoryStateChange>, +) -> Result<(), LexicalError> { + let Some(history_state_entry) = history_state.borrow_mut().redo_stack.pop() else { + return Ok(()); + }; + let current = history_state.borrow().current.clone(); + if let Some(inverse_entry) = get_inverse_entry(&history_state_entry, current.as_ref()) { + history_state.borrow_mut().undo_stack.push(inverse_entry); + editor.dispatch_command(&CAN_UNDO_COMMAND, true)?; + } + if history_state.borrow().redo_stack.is_empty() { + editor.dispatch_command(&CAN_REDO_COMMAND, false)?; + } + history_state.borrow_mut().current = Some(history_state_entry.clone()); + notify(history_state, on_change); + history_state_entry + .editor + .set_editor_state(&history_state_entry.editor_state, Some(HISTORIC_TAG)) +} + +/// `undo(editor, historyState, onChange)`. +fn undo( + editor: &Editor, + history_state: &RefCell, + on_change: Option<&HistoryStateChange>, +) -> Result<(), LexicalError> { + let Some(history_state_entry) = history_state.borrow_mut().undo_stack.pop() else { + return Ok(()); + }; + let current = history_state.borrow().current.clone(); + if let Some(inverse_entry) = get_inverse_entry(&history_state_entry, current.as_ref()) { + history_state.borrow_mut().redo_stack.push(inverse_entry); + editor.dispatch_command(&CAN_REDO_COMMAND, true)?; + } + if history_state.borrow().undo_stack.is_empty() { + editor.dispatch_command(&CAN_UNDO_COMMAND, false)?; + } + history_state.borrow_mut().current = Some(history_state_entry.clone()); + notify(history_state, on_change); + history_state_entry + .editor + .set_editor_state(&history_state_entry.editor_state, Some(HISTORIC_TAG)) +} + +/// `clearHistory(historyState, onChange)`. +fn clear_history(history_state: &RefCell, on_change: Option<&HistoryStateChange>) { + { + let mut state = history_state.borrow_mut(); + state.undo_stack = Vec::new(); + state.redo_stack = Vec::new(); + state.current = None; + } + notify(history_state, on_change); +} + +/// Dispatches `CAN_UNDO_COMMAND` or `CAN_REDO_COMMAND` from the update +/// listener, which has no caller to give an error to: a handler's failed +/// update is discarded, as in JS. +fn dispatch_from_listener(editor: &Editor, command: &LexicalCommand, payload: bool) { + let _ = editor.dispatch_command(command, payload); +} + +/// `registerHistory(editor, historyState, delay, dateNow, +/// onHistoryStateChange, maxDepth)`: records the editor's updates in +/// `history_state` and handles the undo, redo and clear commands. Returns +/// the function that unregisters it all. +/// +/// - `delay`: how long, in milliseconds, an update of the same kind as the +/// one before it (typing, deleting backward, deleting forward) still +/// merges into its entry instead of pushing a new one. +/// - `date_now`: the clock, in milliseconds; `None` is the system clock +/// (`Date.now`). +/// - `on_history_state_change`: called once now, and again whenever +/// `history_state` changes (not when an update is discarded). +/// - `max_depth`: the most entries the undo stack keeps; the oldest go +/// first. `None` keeps them all. +/// +/// The editor's listeners hold `history_state`, and its entries hold their +/// editors, so the editor stays alive until this is unregistered. +pub fn register_history( + editor: &Editor, + history_state: &Rc>, + delay: f64, + date_now: Option, + on_history_state_change: Option, + max_depth: Option, +) -> Unregister { + let date_now = date_now.unwrap_or_else(|| Rc::new(system_date_now)); + let get_merge_action = RefCell::new(MergeActionGetter::new(delay, date_now)); + let weak: Weak = Rc::downgrade(&editor.0); + + let apply_change = { + let history_state = history_state.clone(); + let on_change = on_history_state_change.clone(); + move |payload: &UpdateListenerPayload| { + let Some(editor) = weak.upgrade().map(Editor) else { + return; + }; + let current = history_state.borrow().current.clone(); + if let Some(current) = ¤t + && payload.editor_state.ptr_eq(¤t.editor_state) + { + return; + } + let merge_action = + get_merge_action + .borrow_mut() + .get(&editor, current.as_ref(), payload); + match merge_action { + MergeAction::HistoryPush => { + if !history_state.borrow().redo_stack.is_empty() { + history_state.borrow_mut().redo_stack = Vec::new(); + dispatch_from_listener(&editor, &CAN_REDO_COMMAND, false); + } + let undo_entry = + get_undo_entry(&editor, &payload.prev_editor_state, current.as_ref()); + if let Some(undo_entry) = undo_entry { + { + let mut state = history_state.borrow_mut(); + state.undo_stack.push(undo_entry); + if let Some(cap) = max_depth + && state.undo_stack.len() > cap + { + // FIFO-evict the oldest entries so the stack + // stays at `cap`. + let excess = state.undo_stack.len() - cap; + state.undo_stack.drain(..excess); + } + } + dispatch_from_listener(&editor, &CAN_UNDO_COMMAND, true); + } + } + MergeAction::DiscardHistoryCandidate => return, + MergeAction::HistoryMerge => {} + } + history_state.borrow_mut().current = Some(HistoryStateEntry { + editor, + editor_state: payload.editor_state.clone(), + }); + notify(&history_state, on_change.as_ref()); + } + }; + + // Lets the caller read pre-populated stacks right away, without waiting + // for the first edit. + notify(history_state, on_history_state_change.as_ref()); + + let unregisters: Vec = vec![ + { + let history_state = history_state.clone(); + let on_change = on_history_state_change.clone(); + editor.register_command( + &UNDO_COMMAND, + move |_, editor| { + undo(editor, &history_state, on_change.as_ref())?; + Ok(true) + }, + COMMAND_PRIORITY_EDITOR, + ) + }, + { + let history_state = history_state.clone(); + let on_change = on_history_state_change.clone(); + editor.register_command( + &REDO_COMMAND, + move |_, editor| { + redo(editor, &history_state, on_change.as_ref())?; + Ok(true) + }, + COMMAND_PRIORITY_EDITOR, + ) + }, + { + let history_state = history_state.clone(); + let on_change = on_history_state_change.clone(); + editor.register_command( + &CLEAR_EDITOR_COMMAND, + move |_, _| { + clear_history(&history_state, on_change.as_ref()); + Ok(false) + }, + COMMAND_PRIORITY_EDITOR, + ) + }, + { + let history_state = history_state.clone(); + let on_change = on_history_state_change; + editor.register_command( + &CLEAR_HISTORY_COMMAND, + move |_, editor| { + clear_history(&history_state, on_change.as_ref()); + editor.dispatch_command(&CAN_REDO_COMMAND, false)?; + editor.dispatch_command(&CAN_UNDO_COMMAND, false)?; + Ok(true) + }, + COMMAND_PRIORITY_EDITOR, + ) + }, + editor.register_update_listener(apply_change), + ]; + Box::new(move || { + for unregister in unregisters { + unregister(); + } + }) +} + +/// `createEmptyHistoryState()`. +pub fn create_empty_history_state() -> Rc> { + Rc::new(RefCell::new(HistoryState::default())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + CreateEditorArgs, UpdateOptions, create_editor, create_paragraph_node, create_text_node, + get_root, + }; + use std::cell::Cell; + + fn discrete() -> UpdateOptions { + UpdateOptions { + discrete: true, + ..UpdateOptions::default() + } + } + + /// `$setParagraphText(text)` in an update of its own. + fn set_paragraph_text(editor: &Editor, text: &'static str) { + editor + .update( + move || { + let root = get_root(); + root.clear()?; + let paragraph = create_paragraph_node(); + paragraph.append(&[create_text_node(text)])?; + root.append(&[paragraph]) + }, + discrete(), + ) + .unwrap(); + } + + /// The editor's text, after committing a dispatch's update (which JS's + /// tests `await`). + fn text(editor: &Editor) -> String { + editor.read(|| get_root().get_text_content()).unwrap() + } + + /// Upstream's "undo and redo apply to the editor that changed" (#8623): + /// two editors sharing one history state. + #[test] + fn shared_history_undoes_the_editor_that_changed() { + let parent = create_editor(CreateEditorArgs::default()); + let nested = create_editor(CreateEditorArgs::default()); + // Content before history, so the edits below are changes rather + // than initialization. + for editor in [&parent, &nested] { + set_paragraph_text(editor, "initial"); + } + let shared = create_empty_history_state(); + let unregister_parent = register_history(&parent, &shared, 0.0, None, None, None); + let unregister_nested = register_history(&nested, &shared, 0.0, None, None, None); + + set_paragraph_text(&parent, "parent 1"); + set_paragraph_text(&parent, "parent 2"); + set_paragraph_text(&nested, "nested 1"); + { + let state = shared.borrow(); + assert_eq!(state.undo_stack.len(), 2); + assert!(state.undo_stack[0].editor.ptr_eq(&parent)); + assert!(state.undo_stack[1].editor.ptr_eq(&nested)); + } + + // The most recent change was in the nested editor. + nested.dispatch_command(&UNDO_COMMAND, ()).unwrap(); + assert_eq!(text(&nested), "initial"); + assert_eq!(text(&parent), "parent 2"); + + // The next entry belongs to the parent editor. + nested.dispatch_command(&UNDO_COMMAND, ()).unwrap(); + assert_eq!(text(&nested), "initial"); + assert_eq!(text(&parent), "parent 1"); + assert!(shared.borrow().undo_stack.is_empty()); + + nested.dispatch_command(&REDO_COMMAND, ()).unwrap(); + assert_eq!(text(&nested), "initial"); + assert_eq!(text(&parent), "parent 2"); + + nested.dispatch_command(&REDO_COMMAND, ()).unwrap(); + assert_eq!(text(&nested), "nested 1"); + assert_eq!(text(&parent), "parent 2"); + assert!(shared.borrow().redo_stack.is_empty()); + + unregister_parent(); + unregister_nested(); + } + + /// Upstream's `maxDepth` tests: `n` separate events leave `n - 1` + /// entries, and a cap keeps the newest. + #[test] + fn max_depth_evicts_the_oldest_entries() { + for (max_depth, expected) in [(None, 24), (Some(5), 5)] { + let editor = create_editor(CreateEditorArgs::default()); + let history_state = create_empty_history_state(); + let unregister = register_history(&editor, &history_state, 0.0, None, None, max_depth); + for i in 0..25 { + editor + .update( + move || { + let paragraph = create_paragraph_node(); + paragraph.append(&[create_text_node(&format!("p{i}"))])?; + get_root().append(&[paragraph]) + }, + discrete(), + ) + .unwrap(); + } + let state = history_state.borrow(); + assert_eq!(state.undo_stack.len(), expected, "{max_depth:?}"); + // The newest entry is the state before the last paragraph. + let last = state.undo_stack.last().unwrap(); + assert_eq!( + last.editor_state.read(|| get_root().get_children_size()), + 24 + ); + drop(state); + unregister(); + } + } + + /// `onHistoryStateChange` runs on registration and on every change of + /// the stacks, but not for a discarded update; the injected clock + /// decides merging; unregistering stops it all. + #[test] + fn on_change_clock_and_unregister() { + let editor = create_editor(CreateEditorArgs::default()); + let history_state = create_empty_history_state(); + let changes = Rc::new(Cell::new(0)); + let now = Rc::new(Cell::new(0.0)); + let unregister = { + let (changes, now) = (changes.clone(), now.clone()); + register_history( + &editor, + &history_state, + 300.0, + Some(Rc::new(move || now.get())), + Some(Rc::new(move |_: &HistoryState| { + changes.set(changes.get() + 1) + })), + None, + ) + }; + assert_eq!(changes.get(), 1); + set_paragraph_text(&editor, "a"); + assert_eq!(changes.get(), 2); + // Nothing dirty and no selection: discarded. + editor.update(|| Ok(()), discrete()).unwrap(); + assert_eq!(changes.get(), 2); + // A different change each time: these push however close they are. + now.set(10.0); + set_paragraph_text(&editor, "b"); + assert_eq!(history_state.borrow().undo_stack.len(), 1); + editor.dispatch_command(&UNDO_COMMAND, ()).unwrap(); + assert_eq!(text(&editor), "a"); + editor.dispatch_command(&CLEAR_HISTORY_COMMAND, ()).unwrap(); + assert!(history_state.borrow().current.is_none()); + let before = changes.get(); + unregister(); + set_paragraph_text(&editor, "c"); + assert_eq!(changes.get(), before); + assert!(!editor.dispatch_command(&UNDO_COMMAND, ()).unwrap()); + } +} diff --git a/rust/lexical-editor/src/lib.rs b/rust/lexical-editor/src/lib.rs index b165657b804..6dd141147af 100644 --- a/rust/lexical-editor/src/lib.rs +++ b/rust/lexical-editor/src/lib.rs @@ -47,12 +47,15 @@ mod caret; mod caret_tree; mod caret_utils; +mod commands; pub mod constants; mod editor; mod editor_state; mod error; mod gc; mod generated_json; +#[cfg(feature = "history")] +pub mod history; pub mod json; mod node; pub mod nodes; @@ -85,11 +88,16 @@ pub use caret_utils::{ normalize_caret, remove_text_from_caret_range, rewind_sibling_caret, set_point_from_caret, set_selection_from_caret_range, update_range_selection_from_caret_range, }; +pub use commands::{ + CAN_REDO_COMMAND, CAN_UNDO_COMMAND, CLEAR_EDITOR_COMMAND, CLEAR_HISTORY_COMMAND, REDO_COMMAND, + UNDO_COMMAND, +}; pub use editor::{ COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_HIGH, - COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, CreateEditorArgs, Editor, HISTORY_MERGE_TAG, - LexicalCommand, NodeClass, Transform, Unregister, UpdateListenerPayload, UpdateOptions, - create_command, create_editor, + COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, COMPOSITION_END_TAG, COMPOSITION_START_TAG, + CUT_TAG, CreateEditorArgs, Editor, HISTORIC_TAG, HISTORY_MERGE_TAG, HISTORY_PUSH_TAG, + LexicalCommand, NodeClass, PASTE_TAG, Transform, Unregister, UpdateListenerPayload, + UpdateOptions, create_command, create_editor, }; pub use editor_state::EditorState; pub use error::LexicalError; From 95d6fc909d905d6efe311c71ed08910516ed86ae Mon Sep 17 00:00:00 2001 From: Amp Date: Sat, 14 Nov 2026 01:31:58 +0000 Subject: [PATCH 07/10] feat(rust): undo and redo in the desktop playgrounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- rust/apps/lexical-playground-core/src/lib.rs | 134 ++++++++++++++++-- .../apps/lexical-playground-core/src/tests.rs | 41 ++++++ rust/apps/lexical-playground-gpui/README.md | 8 +- rust/apps/lexical-playground-gpui/src/e2e.rs | 5 +- .../lexical-playground-gpui/src/editor.rs | 6 + rust/apps/lexical-playground-rune/README.md | 6 +- rust/apps/lexical-playground-rune/src/main.rs | 8 +- 7 files changed, 190 insertions(+), 18 deletions(-) diff --git a/rust/apps/lexical-playground-core/src/lib.rs b/rust/apps/lexical-playground-core/src/lib.rs index e0924ead136..eb480f1d564 100644 --- a/rust/apps/lexical-playground-core/src/lib.rs +++ b/rust/apps/lexical-playground-core/src/lib.rs @@ -9,7 +9,7 @@ //! byte offsets. //! - [`Pos`] and [`Document::point_at`] / [`Document::pos_of`]: between a //! position in that text and a Lexical selection point (UTF-16 offsets). -//! - [`Playground`]: edits (typing, Enter, Backspace, formatting) run as +//! - [`Playground`]: edits (typing, Enter, Backspace, formatting, undo) run as //! steps of the conformance script language (`ops.ts`), through the same //! interpreter that replays the vectors. Every step and its result are //! kept, so [`Playground::export_case`] writes the session as a @@ -24,6 +24,8 @@ use lexical::json::{self, Object, Value}; use lexical::{NodeRef, get_root}; use lexical_conformance::scripts::Session; +use std::rc::Rc; +use std::time::Instant; mod surface; pub use surface::{DisplayRun, Surface, byte_of_utf16, utf16_of_byte}; @@ -562,10 +564,24 @@ pub struct Outcome { pub error: Option, } +/// How long typing keeps merging into one undo step, in ms: +/// `HistoryExtension`'s default delay. +pub const HISTORY_DELAY: f64 = 300.0; + +/// A clock in milliseconds, for history's merging by time. +pub type Clock = Rc f64>; + /// A playground editor: a script session, the document it draws, and the /// selection. pub struct Playground { session: Session, + /// The session's script without its steps: history and the CAN_UNDO / + /// CAN_REDO handlers, which the exported case carries for JS. + script: Object, + /// Each step's `now`, which history merges typing by. + clock: Clock, + can_undo: bool, + can_redo: bool, steps: Vec, results: Vec, document: Document, @@ -578,13 +594,46 @@ pub struct Playground { } impl Playground { - /// A playground whose root starts with `children` (serialized nodes). + /// A playground whose root starts with `children` (serialized nodes), + /// with undo and redo (`@lexical/history`) on the wall clock. pub fn new(children: Vec) -> Result { + let start = Instant::now(); + Playground::with_clock( + children, + Rc::new(move || start.elapsed().as_millis() as f64), + ) + } + + /// [`Playground::new`] on `clock` (ms), which stamps each step's `now`. + pub fn with_clock(children: Vec, clock: Clock) -> Result { let mut script = Object::new(); script.insert("name", "playground".into()); - let session = Session::start(&Value::Object(script))?; + let mut history = Object::new(); + history.insert("delay", HISTORY_DELAY.into()); + script.insert("history", Value::Object(history)); + // History says whether undo and redo can run; a handler that + // doesn't stop the command logs it. + let handler = |id: &str, command: &str| { + let mut h = Object::new(); + h.insert("id", id.into()); + h.insert("command", command.into()); + h.insert("result", false.into()); + Value::Object(h) + }; + script.insert( + "handlers", + Value::Array(vec![ + handler("canUndo", "CAN_UNDO_COMMAND"), + handler("canRedo", "CAN_REDO_COMMAND"), + ]), + ); + let session = Session::start(&Value::Object(script.clone()))?; let mut playground = Playground { session, + script, + clock, + can_undo: false, + can_redo: false, steps: Vec::new(), results: Vec::new(), document: Document::default(), @@ -747,6 +796,40 @@ impl Playground { self.edit("formatText", vec![name.into()]) } + /// ⌘Z: `UNDO_COMMAND`, which history handles. The app's selection + /// reaches the editor first, as a browser's `selectionchange` does. + pub fn undo(&mut self) -> Outcome { + self.dispatch("UNDO_COMMAND") + } + + /// ⌘⇧Z: `REDO_COMMAND`. + pub fn redo(&mut self) -> Outcome { + self.dispatch("REDO_COMMAND") + } + + /// Whether there is something to undo, as history last said + /// (`CAN_UNDO_COMMAND`). + pub fn can_undo(&self) -> bool { + self.can_undo + } + + /// Whether there is something to redo (`CAN_REDO_COMMAND`). + pub fn can_redo(&self) -> bool { + self.can_redo + } + + fn dispatch(&mut self, command: &str) -> Outcome { + if let Some(select) = self.select_action() { + let outcome = self.run_update(vec![select]); + if outcome.error.is_some() { + return outcome; + } + } + let mut step = Object::new(); + step.insert("dispatch", command.into()); + self.run(Value::Object(step)) + } + /// `$selectAll()`. pub fn select_all(&mut self) -> Outcome { let mut action = Object::new(); @@ -813,13 +896,23 @@ impl Playground { self.update(vec![Value::Object(action)]) } - /// Runs one update step: first a `select` action when the app's - /// selection isn't the editor's, then `actions`. + /// Runs `actions` as one update step. When the app's selection isn't + /// the editor's, a step with a `select` action runs first, as a + /// browser's `selectionchange` reaches Lexical before the key does, so + /// history merges typing after a click as it does in a browser. pub fn update(&mut self, actions: Vec) -> Outcome { - let mut all = self.select_action().into_iter().collect::>(); - all.extend(actions); + if let Some(select) = self.select_action() { + let outcome = self.run_update(vec![select]); + if outcome.error.is_some() || actions.is_empty() { + return outcome; + } + } + self.run_update(actions) + } + + fn run_update(&mut self, actions: Vec) -> Outcome { let mut step = Object::new(); - step.insert("update", Value::Array(all)); + step.insert("update", Value::Array(actions)); self.run(Value::Object(step)) } @@ -843,6 +936,10 @@ impl Playground { } fn run(&mut self, step: Value) -> Outcome { + let mut step = step; + if let Value::Object(o) = &mut step { + o.insert("now", (self.clock)().into()); + } let result = match self.session.run(&step, true) { Ok(r) => r, Err(e) => { @@ -857,6 +954,25 @@ impl Playground { .editor() .read(Document::read) .unwrap_or_default(); + for entry in result + .json + .get("log") + .and_then(Value::as_array) + .into_iter() + .flatten() + { + let entry = entry.as_array().map(Vec::as_slice).unwrap_or_default(); + if let [kind, id, can] = entry + && kind.as_str() == Some("handler") + && let Some(can) = can.as_bool() + { + match id.as_str() { + Some("canUndo") => self.can_undo = can, + Some("canRedo") => self.can_redo = can, + _ => {} + } + } + } self.model_selection = result.json.get("selection").cloned().unwrap_or(Value::Null); self.selection = self.selection_from_model(); self.last_error = result.error.map(|e| e.to_string()); @@ -879,7 +995,7 @@ impl Playground { /// of each as this editor recorded it. JS Lexical replays it and /// compares (`packages/lexical-conformance/src/replay.test.ts`). pub fn export_case(&self, name: &str) -> Value { - let mut case = Object::new(); + let mut case = self.script.clone(); case.insert("name", name.into()); case.insert("steps", Value::Array(self.steps.clone())); case.insert("results", Value::Array(self.results.clone())); diff --git a/rust/apps/lexical-playground-core/src/tests.rs b/rust/apps/lexical-playground-core/src/tests.rs index f0b4d2da7ba..2851464a5b2 100644 --- a/rust/apps/lexical-playground-core/src/tests.rs +++ b/rust/apps/lexical-playground-core/src/tests.rs @@ -78,6 +78,47 @@ fn typing_enter_and_formatting_edit_the_document() { ); } +#[test] +fn undo_takes_back_typing_merged_by_time_and_redo_brings_it_back() { + use std::cell::Cell; + use std::rc::Rc; + let now = Rc::new(Cell::new(0.0)); + let clock = now.clone(); + let mut p = Playground::with_clock(example(), Rc::new(move || clock.get())).unwrap(); + let second = |p: &Playground| p.document().blocks[1].text.clone(); + let original = second(&p); + assert!(!p.can_undo()); + p.set_selection(Some(Sel::caret(Pos { block: 1, byte: 0 }))); + // "ab" within the delay is one undo step; "c" after it, another. + for (t, ch) in [(1000.0, "a"), (1100.0, "b"), (2000.0, "c")] { + now.set(t); + assert_eq!(p.insert_text(ch).error, None); + } + assert_eq!(second(&p), format!("abc{original}")); + assert!(p.can_undo()); + now.set(2100.0); + assert_eq!(p.undo().error, None); + assert_eq!(second(&p), format!("ab{original}")); + assert_eq!(p.selection(), Some(Sel::caret(Pos { block: 1, byte: 2 }))); + assert_eq!(p.undo().error, None); + assert_eq!(second(&p), original); + assert!(!p.can_undo()); + assert!(p.can_redo()); + assert_eq!(p.redo().error, None); + assert_eq!(second(&p), format!("ab{original}")); + // A new edit drops the redo. + now.set(3000.0); + assert_eq!(p.insert_text("x").error, None); + assert!(!p.can_redo()); + let case = p.export_case("undo"); + assert!(case.get("history").is_some()); + assert!( + case.get("steps").and_then(Value::as_array).unwrap()[1] + .get("now") + .is_some() + ); +} + #[test] fn a_change_the_field_made_replays_as_a_lexical_edit() { let mut p = playground(); diff --git a/rust/apps/lexical-playground-gpui/README.md b/rust/apps/lexical-playground-gpui/README.md index 951c91b1ba5..f4a2e3a928e 100644 --- a/rust/apps/lexical-playground-gpui/README.md +++ b/rust/apps/lexical-playground-gpui/README.md @@ -14,7 +14,9 @@ zbugs-gpui is. macOS. - Typed and committed text runs `insertText`; Return `insertParagraph`; Shift-Return `insertLineBreak`; Backspace and Delete delete by grapheme, Option by word and Command by line; paste runs `insertRawText`; ⌘A, ⌘B, - ⌘I and ⌘U select all and format. + ⌘I and ⌘U select all and format. ⌘Z and ⌘⇧Z dispatch `UNDO_COMMAND` + and `REDO_COMMAND` to `@lexical/history` (the `history` feature), which + merges typing within 300 ms into one step, as `HistoryExtension` does. - An input method's composition is drawn underlined and kept out of Lexical until it commits, as a browser keeps it out of the editor state. @@ -40,7 +42,7 @@ zbugs-gpui is. macOS. LEXICAL_REPLAY=/path/to/lexical-playground-session.json pnpm exec vitest run src/replay.test.ts ``` -Undo, headings, lists, links, code blocks, HTML and Markdown arrive as the +Headings, lists, links, code blocks, HTML and Markdown arrive as the port gets the packages ([DESIGN.md](../../DESIGN.md), milestones). ## Running it @@ -69,7 +71,7 @@ to edit it. Built with the `e2e` feature, the app drives its own window once it's up: it types, formats, composes with an input method, types an emoji and -deletes it, and adds a paragraph and a line break, through the input +deletes it, adds a paragraph and a line break, and undoes and redoes, through the input handler and the key bindings, checking the document after each step. It writes the session to `$LEXICAL_PLAYGROUND_E2E` and exits 0, or 1 with the step that failed. CI (`.github/workflows/rust-playground.yml`) then replays diff --git a/rust/apps/lexical-playground-gpui/src/e2e.rs b/rust/apps/lexical-playground-gpui/src/e2e.rs index 062cbd7c346..7c23d0d67b2 100644 --- a/rust/apps/lexical-playground-gpui/src/e2e.rs +++ b/rust/apps/lexical-playground-gpui/src/e2e.rs @@ -2,7 +2,8 @@ //! `e2e` feature and run with `LEXICAL_PLAYGROUND_E2E=`, the app //! drives its own window once it's up, the way macOS does — text through //! the window's input handler (typing and an input method's composition) -//! and keys through their bindings — checks the document after each step, +//! and keys through their bindings, undo and redo included — checks the +//! document after each step, //! writes the session to `` as a vector, and exits 0, or 1 with the //! step that failed. CI then replays the file in JS Lexical //! (`packages/lexical-conformance/src/replay.test.ts`), which compares @@ -163,6 +164,8 @@ async fn drive(window: AnyWindowHandle, app: &Entity, cx: &mut As ); step!("⇧Return", key(Box::new(InsertLineBreak)), expect(2, "")); step!("Typing after the break", typed("next"), expect(2, "next")); + step!("⌘Z", key(Box::new(Undo)), expect(2, "")); + step!("⌘⇧Z", key(Box::new(Redo)), expect(2, "next")); step!( "The third paragraph is untouched", |_: &mut Window, _: &mut App| {}, diff --git a/rust/apps/lexical-playground-gpui/src/editor.rs b/rust/apps/lexical-playground-gpui/src/editor.rs index ab32cfcb7b1..e76340d2a82 100644 --- a/rust/apps/lexical-playground-gpui/src/editor.rs +++ b/rust/apps/lexical-playground-gpui/src/editor.rs @@ -43,6 +43,8 @@ actions!( Bold, Italic, Underline, + Undo, + Redo, Copy, Cut, Paste, @@ -88,6 +90,8 @@ pub fn bind_keys(cx: &mut App) { KeyBinding::new("cmd-b", Bold, c), KeyBinding::new("cmd-i", Italic, c), KeyBinding::new("cmd-u", Underline, c), + KeyBinding::new("cmd-z", Undo, c), + KeyBinding::new("cmd-shift-z", Redo, c), KeyBinding::new("cmd-c", Copy, c), KeyBinding::new("cmd-x", Cut, c), KeyBinding::new("cmd-v", Paste, c), @@ -590,6 +594,8 @@ impl Render for LexicalEditor { .on_action(cx.listener(|this, _: &Underline, _, cx| { this.edit(cx, |p| p.format_text("underline")) })) + .on_action(cx.listener(|this, _: &Undo, _, cx| this.edit(cx, Playground::undo))) + .on_action(cx.listener(|this, _: &Redo, _, cx| this.edit(cx, Playground::redo))) .on_action(cx.listener(Self::copy)) .on_action(cx.listener(Self::cut)) .on_action(cx.listener(Self::paste)) diff --git a/rust/apps/lexical-playground-rune/README.md b/rust/apps/lexical-playground-rune/README.md index dd219565046..8724606c351 100644 --- a/rust/apps/lexical-playground-rune/README.md +++ b/rust/apps/lexical-playground-rune/README.md @@ -12,7 +12,9 @@ macOS first; it also builds and renders on Linux. - Typing, Return (`insertParagraph`), Shift-Return (`insertLineBreak`), Backspace and Delete (with Option for a word, Command for a line), Tab, paste (`insertRawText`), ⌘A, ⌘B, ⌘I and ⌘U run as Lexical updates - before the field sees them. + before the field sees them. ⌘Z and ⌘⇧Z dispatch `UNDO_COMMAND` and + `REDO_COMMAND` to `@lexical/history` (the `history` feature), which + merges typing within 300 ms into one step, as `HistoryExtension` does. - A change the field still makes itself, such as an input method's commit or a cut, is replayed into Lexical as the text it replaced. - After each edit the mirror is rebuilt from Lexical's state. @@ -35,7 +37,7 @@ macOS first; it also builds and renders on Linux. LEXICAL_REPLAY=/path/to/lexical-playground-session.json pnpm exec vitest run src/replay.test.ts ``` -Undo, headings, lists, links, code blocks, HTML and Markdown arrive as the +Headings, lists, links, code blocks, HTML and Markdown arrive as the port gets the packages ([DESIGN.md](../../DESIGN.md), milestones). ## Running it diff --git a/rust/apps/lexical-playground-rune/src/main.rs b/rust/apps/lexical-playground-rune/src/main.rs index 8115a8bae73..a6419f1d543 100644 --- a/rust/apps/lexical-playground-rune/src/main.rs +++ b/rust/apps/lexical-playground-rune/src/main.rs @@ -140,9 +140,11 @@ impl State { 'i' => self.edit_with(|p| p.format_text("italic")), 'u' => self.edit_with(|p| p.format_text("underline")), 'a' => self.edit_with(Playground::select_all), - // Undo comes with @lexical/history; until then the field's - // own undo would edit behind Lexical's back. - 'z' | 'y' => {} + // Lexical's history, not the field's own undo, which would + // edit behind Lexical's back. + 'z' | 'Z' if m.shift => self.edit_with(Playground::redo), + 'z' => self.edit_with(Playground::undo), + 'y' => {} _ => return false, }, _ => return false, From b14361d0dfa6fe266de2c1c692174bb3cb970210 Mon Sep 17 00:00:00 2001 From: Amp Date: Sat, 14 Nov 2026 01:40:55 +0000 Subject: [PATCH 08/10] feat(rust): the GPUI playground follows the Mac's dark mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- rust/apps/lexical-playground-gpui/README.md | 2 ++ rust/apps/lexical-playground-gpui/src/app.rs | 8 ++++++++ .../apps/lexical-playground-gpui/src/editor.rs | 18 ++++++++++++------ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/rust/apps/lexical-playground-gpui/README.md b/rust/apps/lexical-playground-gpui/README.md index f4a2e3a928e..4dc58c020b6 100644 --- a/rust/apps/lexical-playground-gpui/README.md +++ b/rust/apps/lexical-playground-gpui/README.md @@ -28,6 +28,8 @@ zbugs-gpui is. macOS. - **The toolbar.** Bold, italic, underline, strikethrough, code, highlight, subscript and superscript follow the selection's format. Select all, and Reset. +- **Light and dark.** The window follows the Mac's appearance and switches + live when it changes, as the Rune playground does. - **The side panel.** - **Tree:** the node tree with keys. - **JSON:** the editor state's JSON. diff --git a/rust/apps/lexical-playground-gpui/src/app.rs b/rust/apps/lexical-playground-gpui/src/app.rs index af8baf8c121..f03ce5e5e1e 100644 --- a/rust/apps/lexical-playground-gpui/src/app.rs +++ b/rust/apps/lexical-playground-gpui/src/app.rs @@ -31,6 +31,7 @@ pub struct PlaygroundApp { report: Option<(String, bool)>, status: Option, _subscription: Subscription, + _appearance: Subscription, } impl PlaygroundApp { @@ -47,6 +48,12 @@ impl PlaygroundApp { cx, ) }); + // Light or dark like the Mac, switching live when the system + // appearance changes, as the Rune app does. + Theme::sync_system_appearance(Some(window), cx); + let appearance = cx.observe_window_appearance(window, |_, window, cx| { + Theme::sync_system_appearance(Some(window), cx); + }); let subscription = cx.subscribe(&editor, |this, _, _: &EditorEvent, cx| { this.status = None; cx.notify(); @@ -58,6 +65,7 @@ impl PlaygroundApp { report: None, status: None, _subscription: subscription, + _appearance: appearance, } } diff --git a/rust/apps/lexical-playground-gpui/src/editor.rs b/rust/apps/lexical-playground-gpui/src/editor.rs index e76340d2a82..d479ee916b3 100644 --- a/rust/apps/lexical-playground-gpui/src/editor.rs +++ b/rust/apps/lexical-playground-gpui/src/editor.rs @@ -7,7 +7,7 @@ //! state, as Lexical's reconciler draws the DOM. Moving the caret and //! laying out text are the element's, as they are the browser's. -use gpui_kit::component::ActiveTheme; +use gpui_kit::component::{ActiveTheme, Theme}; use gpui_kit::*; use lexical_playground_core::{Outcome, Playground, Surface, Unit, format_flag}; use std::ops::Range; @@ -615,8 +615,15 @@ impl Render for LexicalEditor { } /// The text runs to shape: each run's format as a font, colors and lines. -fn text_runs(surface: &Surface, base: &TextStyle, accent: Hsla) -> Vec { +fn text_runs(surface: &Surface, base: &TextStyle, theme: &Theme) -> Vec { let base_font = base.font(); + let accent = theme.primary; + // A marker yellow behind dark text, a deep amber behind light text. + let highlight = if theme.is_dark() { + hsla(40. / 360., 0.75, 0.3, 1.) + } else { + hsla(48. / 360., 0.96, 0.75, 1.) + }; surface .runs() .into_iter() @@ -643,7 +650,7 @@ fn text_runs(surface: &Surface, base: &TextStyle, accent: Hsla) -> Vec base.color }, background_color: if has("highlight") { - Some(hsla(48. / 360., 0.96, 0.75, 1.)) + Some(highlight) } else if has("code") { Some(base.color.opacity(0.08)) } else { @@ -734,7 +741,7 @@ impl Element for EditorElement { let editor = self.editor.read(cx); let text = editor.surface.display_text(); let style = window.text_style(); - let runs = text_runs(&editor.surface, &style, cx.theme().primary); + let runs = text_runs(&editor.surface, &style, cx.theme()); let font_size = style.font_size.to_pixels(window.rem_size()); let line_height = window.line_height(); let mut layout_style = Style::default(); @@ -766,8 +773,7 @@ impl Element for EditorElement { let editor = self.editor.read(cx); let text = editor.surface.display_text(); let style = window.text_style(); - let accent = cx.theme().primary; - let runs = text_runs(&editor.surface, &style, accent); + let runs = text_runs(&editor.surface, &style, cx.theme()); let font_size = style.font_size.to_pixels(window.rem_size()); let line_height = window.line_height(); let lines = Rc::new(shape( From 1e381804689977e30e25a241aa14f4995726ab11 Mon Sep 17 00:00:00 2001 From: Amp Date: Sat, 14 Nov 2026 01:42:58 +0000 Subject: [PATCH 09/10] fix(rust): readable highlight in the Rune playground's dark mode ## 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 --- rust/apps/lexical-playground-rune/src/main.rs | 11 ++++++++--- rust/apps/lexical-playground-rune/src/view.rs | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/rust/apps/lexical-playground-rune/src/main.rs b/rust/apps/lexical-playground-rune/src/main.rs index a6419f1d543..74477c59afd 100644 --- a/rust/apps/lexical-playground-rune/src/main.rs +++ b/rust/apps/lexical-playground-rune/src/main.rs @@ -204,8 +204,9 @@ pub fn mirror(doc: &Document, selection: Option) -> TextEdit { } /// Highlight, subscript and superscript, which the field has no marks for, -/// drawn over it. -pub fn decorations(doc: &Document) -> Vec { +/// drawn over it. `dark` is the window's appearance: highlight is a marker +/// yellow behind dark text and a deep amber behind light text. +pub fn decorations(doc: &Document, dark: bool) -> Vec { let mut spans = Vec::new(); let mut offset = 0; for block in &doc.blocks { @@ -216,7 +217,11 @@ pub fn decorations(doc: &Document) -> Vec { } if run.has_format("highlight") { let mut span = rune::Span::new(start, end); - span.bg = Some(rune::oklch(0.92, 0.12, 95.0)); + span.bg = Some(if dark { + rune::oklch(0.5, 0.1, 75.0) + } else { + rune::oklch(0.92, 0.12, 95.0) + }); spans.push(span); } if run.has_format("subscript") || run.has_format("superscript") { diff --git a/rust/apps/lexical-playground-rune/src/view.rs b/rust/apps/lexical-playground-rune/src/view.rs index 3dab8820e06..ca581e8f916 100644 --- a/rust/apps/lexical-playground-rune/src/view.rs +++ b/rust/apps/lexical-playground-rune/src/view.rs @@ -111,7 +111,7 @@ fn editor(s: &State, k: &Kit) -> El { .key("editor") .w_full() .grow(1.0) - .decorations(decorations(s.playground.document())) + .decorations(decorations(s.playground.document(), k.dark)) .on_edit(|s: &mut State| &mut s.edit) .on_input_key(on_key) .on_input_paste(on_paste) From 04907bc270d1aaaafd23e7f14d0e013eafee6b08 Mon Sep 17 00:00:00 2001 From: Amp Date: Sat, 14 Nov 2026 03:20:26 +0000 Subject: [PATCH 10/10] =?UTF-8?q?fix(rust):=20=E2=8C=98B=20toggles=20off?= =?UTF-8?q?=20in=20one=20press=20in=20the=20desktop=20playgrounds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- rust/apps/lexical-playground-core/src/lib.rs | 21 +++++--- .../apps/lexical-playground-core/src/tests.rs | 35 +++++++++++++ rust/apps/lexical-playground-gpui/src/app.rs | 21 ++++---- rust/apps/lexical-playground-gpui/src/e2e.rs | 51 +++++++++++++++++++ 4 files changed, 110 insertions(+), 18 deletions(-) diff --git a/rust/apps/lexical-playground-core/src/lib.rs b/rust/apps/lexical-playground-core/src/lib.rs index eb480f1d564..39c45fb2ce9 100644 --- a/rust/apps/lexical-playground-core/src/lib.rs +++ b/rust/apps/lexical-playground-core/src/lib.rs @@ -916,18 +916,23 @@ impl Playground { self.run(Value::Object(step)) } + /// The `select` action that gives the editor the app's selection, or + /// `None` when the editor's selection is already at those positions. + /// Positions, not points, decide: the edge between two text nodes is + /// one position but two points (the end of one node, the start of the + /// next), and a browser leaves Lexical's point alone there. Sending the + /// other point moves the selection, as an extra step: after ⌘B bolds a + /// word, its start would move to the end of the plain node before it, + /// the selection would take that node's format, and the next ⌘B, which + /// toggles the selection's format (in JS as here), would set bold again + /// instead of clearing it. fn select_action(&self) -> Option { let sel = self.selection?; - let anchor = self.document.point_at(sel.anchor).to_json(); - let focus = self.document.point_at(sel.focus).to_json(); - let current = self.model_selection.as_array(); - if let Some(c) = current - && c.first().and_then(Value::as_str) == Some("range") - && c.get(1) == Some(&anchor) - && c.get(2) == Some(&focus) - { + if self.selection_from_model() == Some(sel) { return None; } + let anchor = self.document.point_at(sel.anchor).to_json(); + let focus = self.document.point_at(sel.focus).to_json(); let mut action = Object::new(); action.insert("do", "select".into()); action.insert("anchor", anchor); diff --git a/rust/apps/lexical-playground-core/src/tests.rs b/rust/apps/lexical-playground-core/src/tests.rs index 2851464a5b2..73c9b16b175 100644 --- a/rust/apps/lexical-playground-core/src/tests.rs +++ b/rust/apps/lexical-playground-core/src/tests.rs @@ -264,3 +264,38 @@ fn the_surface_draws_formats_and_edits_through_lexical() { assert_eq!(s.edit(|p| p.format_text("italic")).error, None); assert!(s.playground.has_format("italic")); } + +#[test] +fn format_toggles_on_and_off_with_one_press_each() { + // ⌘B over "dogs" splits it into its own bold node and selects that + // node. The selection's start is also the end of "lazy ", and the next + // ⌘B must keep Lexical's point, not send that one, or it bolds again. + let mut p = Playground::new(vec![ + json::parse(r#"{"type":"paragraph","children":[{"type":"text","text":"lazy dogs."}]}"#) + .unwrap(), + ]) + .unwrap(); + let dogs = Sel { + anchor: Pos { block: 0, byte: 5 }, + focus: Pos { block: 0, byte: 9 }, + }; + p.set_selection(Some(dogs)); + let bold_runs = |p: &Playground| -> Vec { + p.document().blocks[0] + .runs + .iter() + .filter(|r| r.has_format("bold")) + .map(|r| r.text.clone()) + .collect() + }; + for (press, bold) in [true, false, true, false].into_iter().enumerate() { + let steps = p.step_count(); + assert_eq!(p.format_text("bold").error, None); + let want: Vec = if bold { vec!["dogs".into()] } else { vec![] }; + assert_eq!(bold_runs(&p), want, "press {press}"); + assert_eq!(p.has_format("bold"), bold, "press {press}"); + assert_eq!(p.selection(), Some(dogs), "press {press}"); + // The first press also gives the editor the selection. + assert_eq!(p.step_count() - steps, if press == 0 { 2 } else { 1 }); + } +} diff --git a/rust/apps/lexical-playground-gpui/src/app.rs b/rust/apps/lexical-playground-gpui/src/app.rs index f03ce5e5e1e..7ac21076036 100644 --- a/rust/apps/lexical-playground-gpui/src/app.rs +++ b/rust/apps/lexical-playground-gpui/src/app.rs @@ -98,16 +98,17 @@ impl PlaygroundApp { ); for (name, label) in FORMATS { let on = playground.has_format(name); - bar = bar.child( - Button::new(SharedString::from(format!("format-{name}"))) - .small() - .label(label) - .selected(on) - .tooltip(format!("Format {name}")) - .on_click(cx.listener(move |this, _, window, cx| { - this.toolbar_edit(window, cx, |p| p.format_text(name)) - })), - ); + let button = Button::new(SharedString::from(format!("format-{name}"))) + .small() + .label(label) + .selected(on) + .tooltip(format!("Format {name}")) + .on_click(cx.listener(move |this, _, window, cx| { + this.toolbar_edit(window, cx, |p| p.format_text(name)) + })); + // A format that's on is filled, as in the Rune app: the theme's + // selected look barely shows in dark mode. + bar = bar.child(if on { button.primary() } else { button }); } bar.child( Button::new("select-all") diff --git a/rust/apps/lexical-playground-gpui/src/e2e.rs b/rust/apps/lexical-playground-gpui/src/e2e.rs index 7c23d0d67b2..63f902e2b0a 100644 --- a/rust/apps/lexical-playground-gpui/src/e2e.rs +++ b/rust/apps/lexical-playground-gpui/src/e2e.rs @@ -166,6 +166,57 @@ async fn drive(window: AnyWindowHandle, app: &Entity, cx: &mut As step!("Typing after the break", typed("next"), expect(2, "next")); step!("⌘Z", key(Box::new(Undo)), expect(2, "")); step!("⌘⇧Z", key(Box::new(Redo)), expect(2, "next")); + // ⌘B over the end of a word: on, then off. "xt" becomes a bold node + // after "ne", so the selection's start is both the end of "ne" and the + // start of "xt"; the second ⌘B keeps Lexical's point (the start of + // "xt") and unbolds in one step. (The first also gives Lexical the new + // selection.) + let next_bold = |want: bool| { + let editor = editor.clone(); + move |cx: &mut AsyncApp| -> Result { + let (format, bold, steps) = editor.read_with(cx, |e, _| { + let p = &e.surface.playground; + let run = p.document().blocks[1] + .runs + .iter() + .find(|r| r.text.ends_with("xt")) + .filter(|r| r.text == if want { "xt" } else { "next" }); + ( + run.map(|r| r.has_format("bold")), + p.has_format("bold"), + p.step_count(), + ) + }); + if format != Some(want) || bold != want { + return Err(format!( + "\"xt\" bold is {format:?}, the toolbar's {bold}, not {want}" + )); + } + Ok(steps) + } + }; + step!("⇧←", key(Box::new(SelectLeft)), expect(2, "next")); + step!( + "⇧← again", + key(Box::new(SelectLeft)), + |cx: &mut AsyncApp| -> Step { next_bold(false)(cx).map(drop) } + ); + step!( + "⌘B over it", + key(Box::new(Bold)), + |cx: &mut AsyncApp| -> Step { next_bold(true)(cx).map(drop) } + ); + let bolded = next_bold(true)(cx)?; + step!( + "⌘B again", + key(Box::new(Bold)), + |cx: &mut AsyncApp| -> Step { + match next_bold(false)(cx)? - bolded { + 1 => Ok(()), + n => Err(format!("{n} steps, not 1")), + } + } + ); step!( "The third paragraph is untouched", |_: &mut Window, _: &mut App| {},