Skip to content

fix: support oh-my-pi hosts via public-API editor internals adapter - #17

Open
taiseii wants to merge 3 commits into
0xKahi:mainfrom
taiseii:fix/omp-editor-internals
Open

taiseii wants to merge 3 commits into
0xKahi:mainfrom
taiseii:fix/omp-editor-internals

Conversation

@taiseii

@taiseii taiseii commented Jul 27, 2026 •

Copy link
Copy Markdown

Closes #16

Problem

On oh-my-pi (omp), every vim motion and text edit silently no-ops, while mode switching still works (Esc → normal mode, block cursor) — masking the failure.

Root cause: the plugin reaches pi-tui's editor buffer/cursor through getEditorInternals(), which relies on pi's state / moveCursor being TypeScript-private (plain runtime properties). omp's @oh-my-pi/pi-tui Editor uses ECMAScript-private #state / #moveCursor, so every internals read returns undefined and setCursor() hits its silent if (!state) return; guard.

Fix

getEditorInternals now probes for native internals (state + moveCursor present) and, when absent, returns a cached OmpEditorInternalsAdapter that reproduces the exact EditorInternals surface on omp's public Editor API. Controllers are untouched — the fix lives entirely behind the seam the codebase already centralized for this.

Adapter mechanics (verified against packages/tui/src/components/editor.ts @ omp 17.1.3):

  • Buffer reads → getLines() / getCursor()
  • Buffer writes → setText() + cursor restore (omp's setText anchors the cursor to the end). state.lines is a write-through proxy: index writes, whole-array assignment, and mutating methods (splice, …) commit via setText.
  • Cursor writes → synthetic legacy arrow keys (\x1b[C / \x1b[D) dispatched to the base Editor.prototype.handleInput (bypassing the modal override, which would re-enter vim dispatch). omp's native step is grapheme-aware and wraps across logical line boundaries, so a left/right walk reaches any (line, col) exactly — no visual-line ambiguity.
  • Vertical arrows are never injected at the buffer edge, where omp's up/down switches to prompt-history navigation or line-end jumps.
  • Deliberately omitted fields (all consumers already degrade): onChange (omp's setText fires it natively — exposing it would double-fire), undoStack/pushUndoSnapshot (plugin's own fallback stack is used), tui (host renders after dispatch), scrollOffset/lastWidth (renderer has fallbacks), segment (Intl.Segmenter fallback).

Tests

  • test/omp-internals-adapter.test.ts: 15 new tests driving an omp-faithful double (public API only, omp's exact arrow/setText/onChange semantics): host detection + adapter caching, state-proxy reads/writes/splice/assignment/clamping, the history-nav edge guard, and end-to-end MovementController (hjkl/w/b/0/$/G) and TextEditController (x, dd, p, u, o) through the adapter, plus single-fire onChange.
  • Full suite: 186 pass / 0 fail (bun test), bun run check (biome + tsc --noEmit) clean. The existing test/editor-internals.test.ts tripwire confirms the pi path still returns native internals.

Limitations (honest list)

  • The base-prototype key-injection branch is exercised in tests via the duck-typed fallback; against the real omp bundle it was verified by source inspection (Editor.prototype.handleInput + tui.editor.cursor* default bindings), not a live TUI. Worst case it degrades back to today's behavior, never worse.
  • On omp, visual-mode highlight uses the renderer's existing scrollOffset = 0 / computed-lastWidth fallbacks — correct for unscrolled prompts, which is the common case.
  • Plugin-owned undo stack on omp (pi's native undo stack is unreachable) — behaviorally equivalent for plugin edits.
  • Companion issue asking omp for a real public cursor API: pi-tui Editor: expose public cursor setter (or runtime-accessible internals) for pi plugin compat can1357/oh-my-pi#6795 — if that lands, this adapter can shrink to a few calls.

Review notes (codex gpt-5.5 high pass)

One finding fixed post-review: the lines-proxy mutator check used prop in RECORD, which also matches inherited Object.prototype names (toString, …) and caused a spurious commit — now Object.hasOwn.

Two findings were confirmed real but intentionally not fixed, documented here as tradeoffs:

  • Wrapped-line j/k: the vertical guard uses logical lines, so within a single long wrapped logical line j/k no-op (pi moves by visual row). The safe alternative doesn't exist plugin-side: omp's up/down at the first/last visual line triggers prompt-history navigation (which replaces the draft) or line-end jumps, and the wrap width (#lastWidth) is private, so we cannot tell when an arrow is safe. Conservative guard stays until omp exposes layout info.
  • Transient intermediate buffers: multi-step edits (e.g. deleteForward at EOL: assign + splice) commit each write, so onChange can observe a transient intermediate buffer before the final one, synchronously, within one input dispatch. omp's change consumers are debounced, and the alternative (deferred commits flushed via tui.requestRender) breaks cursor feedback on direct-setCursor paths like jumpWord/leap — worse than the transient.

Update: visual mode fixed (was crashing under omp)

Live testing surfaced that v broke the editor entirely under omp. Two root causes, both fixed in the latest push:

  1. getPaddingX() is pi-only — omp resolves padding privately (override ?? theme.editorPaddingX ?? 2), so the renderer threw a TypeError on every frame in visual mode. It now probes the host once and uses the theme's editorPaddingX hint (threaded from VimModalEditor) with omp's default 2 as fallback.
  2. Row format divergence — pi text rows are bare padding + text + padding (exactly what the renderer rebuilt); omp rows carry box side chrome (│ + pad … pad + │) with the bottom border fused into the last text row. On omp the renderer now column-slices the original row's chrome off and swaps only the content region — glyph- and style-agnostic across symbol presets — including omp's right-chrome shrink when the EOL cursor overflows one cell.

Three new tests cover the omp row format (fused bottom row, middle-row chrome, width preservation under a width-neutral style). Suite: 189 pass / 0 fail.

taiseii added 3 commits July 27, 2026 13:38
Pi's Editor keeps state/moveCursor TypeScript-private (runtime-accessible),
so getEditorInternals could cast directly. oh-my-pi's Editor uses
ECMAScript-private #state/#moveCursor, making every motion and text edit
silently no-op (mode switching still worked, which masked the failure).

getEditorInternals now probes for native internals and, when absent, returns
a cached adapter reproducing the EditorInternals surface on omp's public API:

- buffer reads via getLines()/getCursor(), writes via setText() + cursor
  restore (setText anchors to end; fires onChange natively, so the adapter
  deliberately omits onChange to avoid double-firing)
- cursor writes via synthetic legacy arrow keys dispatched to the base
  Editor input handler; omp's native step is grapheme-aware and wraps
  across logical lines, so left/right walks reach any (line, col) exactly
- vertical arrows are never injected at the buffer edge, where omp would
  switch to prompt-history navigation
- undo falls back to the plugin-owned stack; visual highlight keeps its
  existing scrollOffset/lastWidth fallbacks

Closes 0xKahi#16
'prop in RECORD' also matches inherited Object.prototype names, so
state.lines.toString (and friends) were routed through the mutating
wrapper, causing a spurious setText + onChange. Found by codex review.
Visual mode crashed on omp for two reasons:

- getPaddingX() is pi-only; omp resolves padding privately
  (override ?? theme.editorPaddingX ?? 2). The renderer now probes once
  and takes the theme hint (threaded from VimModalEditor) with the omp
  default of 2 as fallback.
- pi text rows are bare 'padding + text + padding', which is exactly what
  the renderer rebuilt. omp rows carry box side chrome (│ + pad … pad + │)
  with the bottom border fused into the last text row (╰─…─╯), so the
  pi-style rewrite stripped the frame. On omp the renderer now
  column-slices the original row's chrome (glyph- and style-agnostic)
  and swaps only the content region, mirroring omp's right-chrome shrink
  when the end-of-line cursor overflows by a cell.
@0xKahi

0xKahi commented Sep 10, 2026

Copy link
Copy Markdown
Owner

oh my bad i just saw this, can help resolve the merge conflicts. i just updated pi dependencies to v0.85.1 and simplified some of the editor internals. i think you can move thegetHostPaddingX into the new host object

    const host: EditorHostServices = {
      isFocused: () => this.focused,
      notifyChange: text => this.onChange?.(text),
      requestRender: () => this.tui.requestRender(),
      isHardwareCursorEnabled: () => tui.getShowHardwareCursor(),
    };

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.

Broken on oh-my-pi (omp): editor internals are ECMAScript-private, all motions/edits silently no-op

2 participants