import { isValidPosition, type Position, type PromptBuffer, } from "../engine/buffer.js"; /** Runtime-only shape used by Pi's current Editor implementation. */ export type PiEditorRuntimeState = { lines: string[]; cursorLine: number; cursorCol: number; }; /** * Deliberately narrow structural interface around Pi's non-public editor state. * No engine module may depend on this type. */ export type PiEditorStateHost = { state: PiEditorRuntimeState; onChange?: (text: string) => void; tui?: { requestRender: () => void; }; }; export type AdapterProbeResult = | { compatible: true } | { compatible: false; reason: string }; function isRuntimeState(value: unknown): value is PiEditorRuntimeState { if (typeof value !== "object" || value === null) return false; const state = value as Record; return ( Array.isArray(state.lines) && state.lines.length > 0 && state.lines.every((line) => typeof line === "string") && Number.isInteger(state.cursorLine) && Number.isInteger(state.cursorCol) ); } /** Check the private Pi editor state shape without mutating it. */ export function probePiEditorAdapter(editor: unknown): AdapterProbeResult { if (typeof editor !== "object" || editor === null) { return { compatible: false, reason: "Editor is not an object" }; } const state = (editor as Record).state; if (!isRuntimeState(state)) { return { compatible: false, reason: "Editor does not expose the expected state shape", }; } const buffer: PromptBuffer = { lines: state.lines, cursor: { line: state.cursorLine, column: state.cursorCol }, }; if (!isValidPosition(buffer, buffer.cursor)) { return { compatible: false, reason: "Editor state cursor is out of bounds", }; } return { compatible: true }; } /** * Applies whole-buffer updates through the one isolated private Pi boundary. * It is intentionally constructed only after a successful capability probe. */ export class RuntimePiEditorAdapter { readonly #editor: PiEditorStateHost; constructor(editor: unknown) { const probe = probePiEditorAdapter(editor); if (!probe.compatible) { throw new Error(`Incompatible Pi editor: ${probe.reason}`); } this.#editor = editor as PiEditorStateHost; } read(): PromptBuffer { const { lines, cursorLine, cursorCol } = this.#editor.state; return { lines: [...lines], cursor: { line: cursorLine, column: cursorCol }, }; } setCursor(cursor: Position): void { const buffer = this.read(); if (!isValidPosition(buffer, cursor)) { throw new RangeError("Cannot move to an invalid cursor position"); } this.#editor.state.cursorLine = cursor.line; this.#editor.state.cursorCol = cursor.column; this.#editor.tui?.requestRender(); } apply(buffer: PromptBuffer): void { if (!isValidPosition(buffer, buffer.cursor)) { throw new RangeError("Cannot apply a buffer with an invalid cursor"); } const lines = [...buffer.lines]; this.#editor.state = { lines, cursorLine: buffer.cursor.line, cursorCol: buffer.cursor.column, }; this.#editor.onChange?.(lines.join("\n")); this.#editor.tui?.requestRender(); } }