Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions rust/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,12 @@ Names may change as packages land; this table is updated with them.
- **Nodes.** JS's class hierarchy is the `LexicalNode` trait plus the data
each base class adds (`ElementData`, `TextData`). A node is a value behind
an `Rc` in its editor state's node map; `getWritable` clones it unless
nothing else shares it (`utils::with_writable`), so an earlier editor state
never sees a change. Code outside a node reads it through `NodeRef`, a key
nothing else shares it (`utils::get_writable`), so an earlier editor state
never sees a change. The `Rc`'s uniqueness stands in for JS's
`_cloneNotNeeded`. `getWritable` also marks the node dirty, as in JS, and
helpers that take "writable" nodes expect their callers to have called it
in JS's order, because update listeners see nodes in the order they were
marked. Code outside a node reads it through `NodeRef`, a key
that resolves the latest version on every call, as JS node methods call
`getLatest()`. Base class behavior a subclass overrides with a `super`
call is a free function in the base class's module.
Expand All @@ -111,7 +115,20 @@ Names may change as packages land; this table is updated with them.
from Swift (PARITY.md).
- **Errors.** A JS `invariant` a document or a caller can break returns a
`LexicalError` with the same message; one only a bug in a node
implementation can break panics, as `RefCell` does.
implementation can break panics, as `RefCell` does. A failed update
returns its error from `Editor::update` (JS reports it to `onError`) after
discarding the update and notifying listeners, as JS does.
- **The update cycle** (`updates.rs`) follows `$beginUpdate` and
`$commitPendingUpdates` line by line: nested and queued updates,
normalization, transforms (leaves first, the root last), GC, the
selection check, discarding a failed update, and the text content and
update listeners. The dirty sets are insertion-ordered like JS's `Map` and
`Set` (`ordered.rs`), including iteration that sees entries added
meanwhile. Without a DOM nothing reconciles, so mutation listeners and
selection-change notifications never fire, as in JS's headless editor.
JS commits a non-discrete update in a microtask; with no event loop, Rust
commits it at the next `read` or discrete update, which JS also does when
one comes first. Update closures that may be queued are `'static`.

## Conformance

Expand Down Expand Up @@ -153,7 +170,7 @@ 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/*` | |
| 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); next, `RangeSelection` editing and carets |
| 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` | |
| M4 | `html`: a DOM abstraction over an HTML parser, CSS declarations, export and import | `html/*`, `css/*`, `property/documents` (HTML) | |
Expand Down
29 changes: 20 additions & 9 deletions rust/PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ updated with each milestone.
| Export form | `LexicalSerializedExport.ts` | `serialized_export` | `serialization/core` |
| Generated JSON of the core nodes | `LexicalGeneratedJSON.ts` | `generated_json` | `serialization/core`, `property/documents` |
| Core nodes | `nodes/LexicalRootNode.ts`, `LexicalParagraphNode.ts`, `LexicalTextNode.ts`, `LexicalTabNode.ts`, `LexicalLineBreakNode.ts`, `LexicalElementNode.ts` (fields, `getTextContent`, `shouldSerializeTextStyles`) | `nodes::*` | `serialization/core`, `property/documents` |
| Node references and `getWritable` | `LexicalNode.ts` (`getLatest`, `getParent`, siblings, children) | `node`, `utils` | through the nodes |
| Appending children | `ElementNode.splice` (insertion only), `RootNode.splice`, `caret/LexicalCaretTree.ts` | `caret_tree` | through parsing |
| Editor state | `LexicalEditorState.ts` (`toJSON`, `read`, `isEmpty`) | `editor_state` | `serialization/core` |
| Parsing | `parseEditorState`, `$parseSerializedNodeImpl` (`LexicalUpdates.ts`) | `updates` | `serialization/core`, `property/documents` |
| Editor | `createEditor` (node registration), `getEditorState`, `read` | `editor` | |
| Node methods | `LexicalNode.ts` (`getLatest`, `getWritable`, `isAttached`, `getIndexWithinParent`, `remove`/`$removeNode`, `replace`, `insertBefore`, `insertAfter`, `select*`, `markDirty`) | `node`, `utils` | `scripts/updates`, `scripts/transforms` |
| Element methods | `LexicalElementNode.ts` (`splice` with selection repair, `append`, `clear`, `select`, children and descendants, the setters), `RootNode.splice` and its other overrides | `nodes::element`, `node` | `scripts/updates` |
| Text methods | `LexicalTextNode.ts` (`splitText`, `mergeWithSibling`, `spliceText`, `setTextContent`, `select`, the format, detail, mode and style setters and toggles), `toggleTextFormatType`, `TabNode`'s overrides | `nodes::text` | `scripts/updates`, `scripts/listeners`, `scripts/transforms` |
| Tree surgery | `caret/LexicalCaretTree.ts` (`$insertNodeBetween`, `$detachNode`, `$detachSiblingRange`, `$detachNodeWithSelection`, `$insertSibling`, `$selectAdjacentNode`) | `caret_tree` | `scripts/updates` |
| Selection model | `LexicalSelection.ts`: points, `RangeSelection` and `NodeSelection` fields, `$getSelection`, `$setSelection`, `$createRangeSelection`, and the helpers node methods use to keep a selection valid | `selection` | `scripts/updates` |
| 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/*` |
| UTF-16 offsets | `String#length`, `String#slice` | `utf16` | through the text methods; unit tests against Node |
| Upstream version | `meta.json` | `UPSTREAM_VERSION` | `meta` |

## Not ported yet
Expand All @@ -28,10 +33,16 @@ touches:

- **NodeState** (`"$"`) and **slots** (`"$slots"`): a document that carries
either fails to parse with an error naming it, rather than losing them.
- `editor.update`, the commit, normalization (JS's `setEditorState` merges
adjacent simple text nodes; the runner compares text content, which that
doesn't change), selection, transforms, commands and listeners.
- `RootNode.__cachedText`, which only the DOM reconciler fills.
- `RangeSelection` editing (`insertText`, `insertParagraph`,
`deleteCharacter`, `formatText`, ...), `$selectAll`, `$insertNodes`, and
ordering points on different nodes (`isBefore`, which needs carets): a
case that reaches these fails with an error naming the caret port rather
than guessing. `scripts/selection` and `scripts/caret` cases, and the one
case each of `scripts/listeners` and `scripts/transforms` that types
text, are counted as waiting on them.
- Composition, the DOM reconciler, mutation listeners, selection-change
notifications and `$onUpdate` callbacks (none of which a headless editor
runs, except `$onUpdate`), and `RootNode.__cachedText`.

## Documented exceptions

Expand Down
31 changes: 31 additions & 0 deletions rust/conformance/src/suites/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

mod meta;
mod property;
mod scripts;
mod serialization;

use crate::Suite;
Expand All @@ -19,6 +20,36 @@ pub fn all() -> Vec<Suite> {
variant: None,
check: serialization::check,
},
Suite {
file: "scripts/updates",
variant: None,
check: scripts::check,
},
Suite {
file: "scripts/transforms",
variant: None,
check: scripts::check,
},
Suite {
file: "scripts/listeners",
variant: None,
check: scripts::check,
},
Suite {
file: "scripts/commands",
variant: None,
check: scripts::check,
},
Suite {
file: "scripts/selection",
variant: None,
check: scripts::check,
},
Suite {
file: "scripts/caret",
variant: None,
check: scripts::check,
},
Suite {
file: "property/documents",
variant: Some("JSON and text"),
Expand Down
Loading
Loading