Structured Note Editor
Problem
Design StructuredNoteEditor, an in-memory editor that owns an ordered sequence of blocks, a cursor gap, and linear undo and redo history. Testcases call one editor sequentially from one thread.
Requirements
- Empty start. A new editor has no blocks, cursor position
0, and no undo or redo history. - Valid blocks. Every stored block has a valid ID, one supported type, and valid text.
- Stable identity. Block IDs are unique in the current document; insert creates an identity, replace and move preserve it, and remove deletes it.
- Ordered edits. Insert uses a document position, remove and replace use block identity, and move uses a final document position.
- Valid cursor. The cursor is a gap from
0through the current block count, inclusive, and each primitive edit applies the cursor transition defined in the API. - Atomic batches. A batch evaluates its primitive operations in order against one working state and commits only when every operation succeeds.
- Rejected edit isolation. A rejected direct edit or batch leaves the document, cursor, undo history, and redo history unchanged; a rejected batch reports its first failing operation.
- Command boundaries. Each successful direct mutation is one undoable command, and each successful batch is one command regardless of its operation count or net effect.
- State restoration. Undo and redo restore the complete document and cursor saved at command boundaries in last-in, first-out order.
- Linear history. Every successful direct mutation or batch clears all redo history, including a successful no-op; rejected edits, snapshots, failed undo, and failed redo do not.
- Detached values. Stored blocks and snapshots are detached copies, so later editor changes and caller changes to supplied or returned values cannot affect one another.
- Instance isolation. Distinct editor instances share no document, value, or history state.
API
All names are case-sensitive. The same camelCase callable names and field names apply in every supported runtime.
Value types
| Type | Fields | Contract |
|---|---|---|
Block | id: string, type: string, text: string | A block value. Equality compares all three strings exactly. |
EditOperation | One exact shape from the operation table | A primitive mutation used only by applyBatch. |
DocumentSnapshot | blocks: Block[], cursorPosition: integer, canUndo: boolean, canRedo: boolean | A detached ordered view of the current document and history availability. |
EditorError | code: string, operationIndex: optional integer | The exception for a rejected edit. operationIndex is present only for an invalid batch element or a primitive failure inside a batch. Message text is not contractual. |
EditOperation allows exactly these shapes. A field not shown for a shape is invalid.
type | Other required fields | Primitive behavior |
|---|---|---|
insert | index: integer, block: Block | The behavior of insertBlock. |
remove | blockId: string | The behavior of removeBlock. |
replace | blockId: string, block: Block | The behavior of replaceBlock. |
move | blockId: string, toIndex: integer | The behavior of moveBlock. |
set_cursor | position: integer | The behavior of setCursor. |
Public callables
| Signature | Returns | Behavior |
|---|---|---|
StructuredNoteEditor() | Not applicable | Creates the empty state. |
insertBlock(index: integer, block: Block) | void | Inserts a copy before the block at index; index == blockCount appends. The index is measured before insertion. |
removeBlock(blockId: string) | void | Removes the identified block. |
replaceBlock(blockId: string, replacement: Block) | void | Replaces only the target block's type and text with copied values. replacement.id must equal blockId. |
moveBlock(blockId: string, toIndex: integer) | void | Moves the target so its index after removal and reinsertion is toIndex, where 0 <= toIndex < blockCount before the move. Moving to the current index succeeds as a no-op. |
setCursor(position: integer) | void | Sets the cursor gap, where 0 <= position <= blockCount. Setting the current position succeeds as a no-op. |
applyBatch(operations: EditOperation[]) | void | Applies from 1 through 100 primitive operations. Nested batches, undo, redo, and snapshot operations are invalid. |
undo() | boolean | Returns false without changing state when undo history is empty. Otherwise restores the latest command's before-state, moves that command to redo history, and returns true. |
redo() | boolean | Returns false without changing state when redo history is empty. Otherwise restores the latest undone command's after-state, moves that command to undo history, and returns true. |
snapshot() | DocumentSnapshot | Returns a detached snapshot without creating history or changing either history stack. |
For cursor transitions, use the cursor value at the current primitive step.
| Primitive | Cursor transition |
|---|---|
Insert at index | Increment by one only when index < cursorPosition. Insertion at the cursor leaves the cursor before the inserted block. |
Remove at old index i | Decrement by one only when i < cursorPosition. |
| Replace | Do not change the cursor. |
Move from i to toIndex | If the indices match, do not change the cursor. Otherwise apply the remove rule, then the insert rule with toIndex and the cursor after removal. |
| Set cursor | Replace the cursor with position. |
Every rejected mutation raises EditorError with one of these codes.
| Code | Condition |
|---|---|
INVALID_BLOCK | A block is absent or malformed, its ID is invalid, its type is unsupported, its text is absent, or its text exceeds the byte limit. |
INVALID_BLOCK_ID | A standalone blockId is absent or does not match the ID format. |
INVALID_INDEX | An index or cursor position is outside its valid range for the state at that step. |
DUPLICATE_BLOCK_ID | An insertion uses an ID already present in the working document. |
BLOCK_ID_MISMATCH | A valid replacement block has an ID different from blockId. |
BLOCK_NOT_FOUND | A valid target ID is absent from the working document. |
INVALID_BATCH | The operations array is absent, empty, or longer than 100. |
INVALID_OPERATION | A batch element is absent, has an unsupported type, has a missing or unexpected field, or has a field of the wrong DTO type. |
Validation has observable precedence:
insertBlockvalidatesblock, then the index range, then ID uniqueness.removeBlockvalidatesblockId, then requires the block to exist.replaceBlockvalidatesblockId, thenreplacement, then matching IDs, then target existence.moveBlockvalidatesblockId, then target existence, thentoIndexagainst the current non-empty document.setCursorvalidatespositionagainst the current document.applyBatchvalidates its container first. It then processes elements from index0, validating each exact operation shape before applying the corresponding precedence against the evolving working state. The first failure supplies the error code and zero-basedoperationIndex.
Wrong scalar types that a runtime's typed method signature cannot express are testcase or compile-time errors. Expressible absent DTO values follow the error table.
Examples
| Step | Operation | Result |
|---|---|---|
| 1 | snapshot() | blocks=[], cursorPosition=0, canUndo=false, canRedo=false |
| 2 | insertBlock(0, Block("a", "paragraph", "Alpha")) | Blocks are [a]; cursor is 0. |
| 3 | setCursor(1) | Cursor is 1. |
| 4 | insertBlock(0, Block("b", "heading", "Beta")) | Blocks are [b, a]; cursor is 2. |
| 5 | moveBlock("b", 1) | Blocks are [a, b]; cursor is 1. |
In the following sequence, shorthand names denote the exact EditOperation shapes above.
| Step | Operation | Result |
|---|---|---|
| 1 | applyBatch([remove("a"), insert(1, Block("c", "code", "x")), set_cursor(2)]) from blocks [a, b] and cursor 1 | Blocks are [b, c]; cursor is 2; one command is added. |
| 2 | undo() | Returns true; blocks are [a, b]; cursor is 1. |
| 3 | redo() | Returns true; blocks are [b, c]; cursor is 2. |
Constraints
- A block ID matches
[A-Za-z0-9_-]{1,64}. - A block type is exactly
heading,paragraph,code, orquote. - Block text may be empty and is at most
4096bytes when encoded as UTF-8. - Text is preserved exactly, with no Unicode normalization, case folding, newline conversion, or whitespace trimming.
- Testcase integers are within the signed 32-bit range, and valid live indices are at most
1000. - A testcase keeps at most
1000live blocks, makes at most10000public calls, performs at most10000primitive mutations including batch elements, and retains at most10000successful history commands. - A batch contains from
1through100operations. snapshotmay use time and space linear in the live document size. No tighter complexity bound applies to edits or history operations.
Notes
Concurrent calls on the same editor are outside scope. Methods perform no external I/O and do not wait, time out, support cancellation, or have shutdown behavior. Batch operations observe earlier successful primitive transitions in the same batch. Undo and redo restore saved command boundaries instead of recalculating cursor rules against later state.