diff --git a/.github/workflows/rust-playground.yml b/.github/workflows/rust-playground.yml new file mode 100644 index 000000000000..95e71c23b45c --- /dev/null +++ b/.github/workflows/rust-playground.yml @@ -0,0 +1,182 @@ +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. 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: + 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" + # 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 + ${{ runner.temp }}/lexical-playground-rune-parity.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 + + 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/packages/lexical-conformance/src/ops.ts b/packages/lexical-conformance/src/ops.ts index 6e5a34c4ba23..9e0d472a7f8c 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/replay.test.ts b/packages/lexical-conformance/src/replay.test.ts new file mode 100644 index 000000000000..e72d5811b108 --- /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/packages/lexical-conformance/src/scripts.test.ts b/packages/lexical-conformance/src/scripts.test.ts index 36919e9d2f53..9906bcc39309 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 000000000000..f54eaa4201f1 --- /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/Cargo.lock b/rust/Cargo.lock index b8a922beecbf..64578cd37b80 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 6f04ed712a3d..1fc2df242479 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 310b0cb29367..9e0b905fe971 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 @@ -58,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` | | @@ -170,16 +173,56 @@ 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), 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-*`, `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) | | | 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 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. ## Releases (planned for M7) diff --git a/rust/PARITY.md b/rust/PARITY.md index 04ecf87fa72d..ccefbe8f1c0b 100644 --- a/rust/PARITY.md +++ b/rust/PARITY.md @@ -22,11 +22,12 @@ 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` | | 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` | @@ -37,11 +38,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`, @@ -50,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/apps/lexical-playground-core/Cargo.toml b/rust/apps/lexical-playground-core/Cargo.toml new file mode 100644 index 000000000000..15d7a00e1754 --- /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 000000000000..39c45fb2ce9c --- /dev/null +++ b/rust/apps/lexical-playground-core/src/lib.rs @@ -0,0 +1,1118 @@ +//! 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, 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 +//! `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; +use std::rc::Rc; +use std::time::Instant; + +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 { + 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, +} + +/// 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, + /// 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), + /// 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 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(), + 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()]) + } + + /// ⌘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(); + 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 `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 { + 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(actions)); + 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?; + 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); + action.insert("focus", focus); + Some(Value::Object(action)) + } + + 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) => { + 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(); + 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()); + 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 = self.script.clone(); + 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 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)?; + 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. +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/surface.rs b/rust/apps/lexical-playground-core/src/surface.rs new file mode 100644 index 000000000000..016b5e70ced3 --- /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 new file mode 100644 index 000000000000..73c9b16b175c --- /dev/null +++ b/rust/apps/lexical-playground-core/src/tests.rs @@ -0,0 +1,301 @@ +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 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(); + 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"); +} + +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")); +} + +#[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/.cargo/config.toml b/rust/apps/lexical-playground-gpui/.cargo/config.toml new file mode 100644 index 000000000000..395ed6c813a5 --- /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 000000000000..b83d22266ac8 --- /dev/null +++ b/rust/apps/lexical-playground-gpui/.gitignore @@ -0,0 +1 @@ +/target/ 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 000000000000..9a3a0fe2a26a --- /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 000000000000..7bbffd8b0971 --- /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 000000000000..7b6d55ba6b96 --- /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 000000000000..2ef288b274a2 --- /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 000000000000..d699b85c26bb --- /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/Cargo.lock b/rust/apps/lexical-playground-gpui/Cargo.lock new file mode 100644 index 000000000000..14b857ed2e81 --- /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 000000000000..fe22e173eadb --- /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 000000000000..4dc58c020b6b --- /dev/null +++ b/rust/apps/lexical-playground-gpui/README.md @@ -0,0 +1,85 @@ +# 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. ⌘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. + - 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. +- **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. + - **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 + ``` + +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 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 + +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, 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 +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 000000000000..565f0286ae7d --- /dev/null +++ b/rust/apps/lexical-playground-gpui/bundle.sh @@ -0,0 +1,61 @@ +#!/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 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 +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" +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' + + + + + CFBundleExecutable + lexical-playground-gpui + CFBundleIdentifier + dev.lexical.playground-gpui + CFBundleName + Lexical Playground (GPUI) + CFBundlePackageType + APPL + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon + 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" +# 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/rust-toolchain.toml b/rust/apps/lexical-playground-gpui/rust-toolchain.toml new file mode 100644 index 000000000000..7ffd41dc286f --- /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 000000000000..7ac210760361 --- /dev/null +++ b/rust/apps/lexical-playground-gpui/src/app.rs @@ -0,0 +1,330 @@ +//! 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, + _appearance: 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"), + window, + 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(); + }); + window.focus(&editor.focus_handle(cx), cx); + PlaygroundApp { + editor, + panel: Panel::Tree, + report: None, + status: None, + _subscription: subscription, + _appearance: appearance, + } + } + + /// 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); + 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") + .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(), window, 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 000000000000..63f902e2b0ab --- /dev/null +++ b/rust/apps/lexical-playground-gpui/src/e2e.rs @@ -0,0 +1,229 @@ +//! 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, 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 +//! 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!("⌘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| {}, + 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 000000000000..d479ee916b3f --- /dev/null +++ b/rust/apps/lexical-playground-gpui/src/editor.rs @@ -0,0 +1,890 @@ +//! 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, Theme}; +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!( + 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, + Undo, + Redo, + Copy, + Cut, + Paste, + ShowCharacterPalette, + ] +); + +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); + 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-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), + 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, + /// 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, 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, + 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 { + 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; + self.blink(cx); + 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); + self.blink(cx); + 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(|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)) + .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, 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() + .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(highlight) + } 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()); + 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 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( + 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, 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()), + 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) + && window.is_window_active() + && caret_on + && 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 000000000000..e57c8a9cbcd0 --- /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/.cargo/config.toml b/rust/apps/lexical-playground-rune/.cargo/config.toml new file mode 100644 index 000000000000..c01e660664b9 --- /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 000000000000..b83d22266ac8 --- /dev/null +++ b/rust/apps/lexical-playground-rune/.gitignore @@ -0,0 +1 @@ +/target/ 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 000000000000..9a3a0fe2a26a --- /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 000000000000..7bbffd8b0971 --- /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 000000000000..7b6d55ba6b96 --- /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 000000000000..2d6b518d22c7 --- /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 000000000000..c0af340dc44f --- /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/Cargo.lock b/rust/apps/lexical-playground-rune/Cargo.lock new file mode 100644 index 000000000000..fd995030ca27 --- /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 000000000000..b7ed7c50a140 --- /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 000000000000..8724606c351c --- /dev/null +++ b/rust/apps/lexical-playground-rune/README.md @@ -0,0 +1,69 @@ +# 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. ⌘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. + - 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 + ``` + +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 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 +`--tree`, `--script`; see Rune's README). The scenes are `typed`, `json` and `parity` (Replay vectors, run). diff --git a/rust/apps/lexical-playground-rune/bundle.sh b/rust/apps/lexical-playground-rune/bundle.sh new file mode 100755 index 000000000000..86db7a3babc7 --- /dev/null +++ b/rust/apps/lexical-playground-rune/bundle.sh @@ -0,0 +1,61 @@ +#!/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 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 +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" +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' + + + + + CFBundleExecutable + lexical-playground-rune + CFBundleIdentifier + dev.lexical.playground-rune + CFBundleName + Lexical Playground (Rune) + CFBundlePackageType + APPL + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon + 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" +# 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/rust-toolchain.toml b/rust/apps/lexical-playground-rune/rust-toolchain.toml new file mode 100644 index 000000000000..2bf148648b2c --- /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 000000000000..74477c59afd6 --- /dev/null +++ b/rust/apps/lexical-playground-rune/src/main.rs @@ -0,0 +1,316 @@ +//! 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, + /// 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 { + 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, + blink: Blink { + since: 0.0, + selection: (0, 0), + window_focused: true, + on: true, + }, + }; + 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), + // 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, + } + 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. `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 { + 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(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") { + 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, tx: &mut Tx) -> bool { + s.blink.restart(tx.clock()); + s.key(ev) +} + +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, 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(); + 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; + }), + ("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). + families: ("Inter", "ui-monospace"), + poll: Some(poll), + ..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 000000000000..ff834017262c --- /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 000000000000..ca581e8f9160 --- /dev/null +++ b/rust/apps/lexical-playground-rune/src/view.rs @@ -0,0 +1,262 @@ +//! 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, 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}; +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) + .on_window_focus(on_window_focus) + .child(toolbar(s, k, cx.chrome_insets().leading)) + .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)) +} + +/// `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); + 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 { + // 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( + if s.blink.on { + k.c.text_base + } else { + Color::TRANSPARENT + }, + k.selection(), + ) + .key("editor") + .w_full() + .grow(1.0) + .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) + .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) + .wrap() + .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) + .wrap() + .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 +} + +pub 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) +} diff --git a/rust/conformance/Cargo.toml b/rust/conformance/Cargo.toml index 003cb58958c3..d5d5c8f301cc 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 72b52f7a9e16..46262ee088fe 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 3229b104ccfe..e25fa60f03e6 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 5961b8edf066..e844f32b874e 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 000000000000..463f8035fb86 --- /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 3f1128654afa..7d938c7a9f2c 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, @@ -64,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 { @@ -108,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 { @@ -123,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 @@ -140,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, } } @@ -209,6 +253,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 +287,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 +308,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/history.rs b/rust/lexical-editor/src/history.rs new file mode 100644 index 000000000000..de32a0a4f975 --- /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 7035fd852eae..6dd141147af0 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; @@ -101,6 +109,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 4534a1d2c90c..32c84d538a37 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 5a5b3785ec5f..43f57e1ed54f 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) }