Skip to content

compose: WYSIWYG spike — visual paragraph editing + markup op contract - #302

Open
BeauBoorman wants to merge 2 commits into
drawmeanelephant:mainfrom
BeauBoorman:agent/wysiwyg-compose
Open

compose: WYSIWYG spike — visual paragraph editing + markup op contract#302
BeauBoorman wants to merge 2 commits into
drawmeanelephant:mainfrom
BeauBoorman:agent/wysiwyg-compose

Conversation

@BeauBoorman

Copy link
Copy Markdown
Collaborator

Implements the WYSIWYG spike plan: survey → one node type → op contract.

Scope (deliberately small): visual editing for plain single-line
paragraphs over Oliver's own render, plus the contract every further node
type reuses. Not a full WYSIWYG editor — headings/lists/markers/paste are
follow-up cards in docs/WYSIWYG-DESIGN.md.

  • ComposeBlockMap — conservative line-shape classification (the
    highlighter's sniffing posture, never a parse) mapping source blocks to
    rendered blocks; alignment with the DOM is verified, not assumed —
    mismatch disables visual mode instead of guessing offsets.
  • ComposeMarkupOp — the closed op set (insert/replace/delete as pure
    buffer splices, surrogate-pair and ZWJ-family-aware collapsed deletes);
    unmappable DOM edits are never guessed (paste/IME-composition payload
    rides dataTransfer/half-composed runs — nil host-side +
    preventDefault DOM-side), the next reconcile snaps the DOM back to the
    buffer.
  • ComposeVisualDocumentCompose Preview: Replace NSAttributedString with WKWebView #230 document assembly + the bridge script
    (data-block numbering, plaintext-only editable blocks, beforeinput
    forwarding, Enter + non-op-set interception, caret/scroll restore).
  • ComposeVisualEditorView — WKWebView host under the Compose Preview: Replace NSAttributedString with WKWebView #230 sandbox; edits
    splice ComposeDocument.text (the single source of truth); reconcile
    re-renders through Oliver after edit silence.
  • ComposeEditorView — Visual toolbar toggle (Markdown buffers only).
  • GoToLineSheet extracted to its own file (ComposeEditorView was over
    the lint file budget).

The DOM is ephemeral paint like the highlighter's attributes; Oliver stays
the only renderer; no parallel document model. Marker verbs (Cmd-B etc.)
are designed-in as existing ComposeFormat transforms at mapped ranges —
second node-type card, no new contract.

Tests: 4 new suites — block map classification/alignment (18), op
derivation + application incl. surrogate/ZWJ collapsed deletes and
never-guess nil paths (29), document assembly + bridge guards (11), and a
real-WKWebView E2E round trip: synthetic beforeinput → buffer mutated +
dirty. Full ContractTests run: 610 executed / 0 failures / 5 pre-existing
skips. swiftlint --strict 0; swiftformat 0.

Notes for review:

  • Commit is unsigned — this repo's history mixes signed and unsigned
    (recent upstream merges are unsigned); happy to re-sign if preferred.
  • Independent gate record (two rounds, including one FAIL→amend cycle that
    fixed a selection-splice offset bug and a ZWJ backward-delete split):
    org/ops/reviews/dme-2026-09-06/adversarial/solipsist-wysiwyg-gate-cw2*.md.

Visual editing for plain single-line paragraphs over Oliver's own render,
with the contract every further node type reuses (docs/WYSIWYG-DESIGN.md):

- ComposeBlockMap: conservative line-shape classification (the highlighter's
  sniffing posture, never a parse mapping source blocks to rendered blocks;
  alignment with the DOM is verified, not assumed — mismatch disables visual
  mode instead of guessing offsets.
- ComposeMarkupOp: the closed op set (insert/replace/delete as pure buffer
  splices, surrogate-pair-aware collapsed deletes); unmappable DOM edits are
  never guessed, the next reconcile snaps the DOM back to the buffer.
- ComposeVisualDocument: drawmeanelephant#230 document assembly + the bridge script
  (data-block numbering, plaintext-only editable blocks, beforeinput
  forwarding, Enter interception, caret/scroll restore).
- ComposeVisualEditorView: WKWebView host under the drawmeanelephant#230 sandbox; edits
  splice ComposeDocument.text (the single source of truth); reconcile
  re-renders through Oliver after edit silence.
- ComposeEditorView: Visual toolbar toggle (Markdown only).
- GoToLineSheet extracted to its own file (ComposeEditorView was over the
  lint file budget).

The DOM is ephemeral paint like the highlighter's attributes; Oliver stays
the only renderer; no parallel document model.

Tests: 4 new suites (block map classification/alignment, op derivation +
application, document assembly + block-child counter, and a real-WKWebView
E2E round trip proving a synthetic beforeinput lands in the buffer and
marks it dirty). Suite: 580 tests, 0 failures. SwiftLint --strict: 0.
EOF
)
…osition, cluster-delete fixes

Addresses the independent gate FAIL (dme-fjt, cw2 verdict org 4defea8):

- ComposeMarkupOpTests.swift was committed empty at 8a71084 (the "4 new
  suites" claim was 3 real + 1 empty — retracted then). This restores the
  full op-contract coverage: 29 tests across insert/replace/delete
  derivation (incl. offset clamping), collapsed surrogate-pair and ZWJ
  family deletes, never-guess nil paths (unmapped input types, nil data,
  non-editable blocks, out-of-range blocks), apply() splice + caret math,
  and the derive-then-apply convenience round trip.

- Two source bugs the new suite exposed, both fixed:
  - insertText over a selection double-translated the buffer offset
    (rendered offset passed back through bufferOffset, so splices landed
    one block-anchor too far right).
  - Collapsed backward delete did not walk continuation units leftward
    (ZWJ families split); backwardClusterExtent now mirrors the forward
    isContinuationScalar walk.

- Paste/composition are now deliberately unmapped, host AND DOM side:
  insertFromPaste payload rides dataTransfer (never data), IME
  composition commits half-composed text — both return nil (buffer
  untouched) and the bridge preventDefaults the same set so paint never
  desyncs awaiting reconcile. insertText with nil data returns nil
  (never guess an empty splice over a selection). Documented in
  WYSIWYG-DESIGN.md derivation section as the spike limitation, paste
  as a follow-up card.

- ComposeVisualDocumentTests gains the unmapped-inputType guard test.

Fresh self-check: ContractTests 610 executed / 0 failures / 5 skipped
(pre-existing); suite presence verified per-file (non-zero blobs) AND
per xcresult (29 MarkupOp test entries, all Passed); swiftlint strict 0;
swiftformat 0; Solipsist app build SUCCEEDED (env-overridden embed).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant