/** A logical prompt position expressed as a UTF-16 offset within one line. */ export type Position = { line: number; column: number; }; /** * The pure editing engine's prompt representation. * * Lines are always non-empty as a collection (an empty prompt is `[""]`), * while individual logical lines may be empty. Grapheme-boundary validation is * added by the segmentation phase; this phase only establishes line bounds. */ export type PromptBuffer = { readonly lines: readonly string[]; readonly cursor: Position; /** Pi large-paste marker ids that must remain atomic during edits. */ readonly pasteMarkerIds?: ReadonlySet; }; export type PromptBufferOptions = { pasteMarkerIds?: ReadonlySet; }; /** Normalize the line endings Pi accepts before constructing a logical buffer. */ export function normalizePromptText(text: string): string { return text.replaceAll("\r\n", "\n").replaceAll("\r", "\n"); } /** Create a valid immutable logical prompt buffer with the cursor at its end. */ export function createPromptBuffer( text: string, options: PromptBufferOptions = {}, ): PromptBuffer { const lines = normalizePromptText(text).split("\n"); const line = lines.length - 1; const lastLine = lines[line]; if (lastLine === undefined) { throw new Error("Prompt buffer must contain at least one line"); } return { lines, cursor: { line, column: lastLine.length }, ...(options.pasteMarkerIds === undefined ? {} : { pasteMarkerIds: new Set(options.pasteMarkerIds) }), }; } /** Return whether a position is within the inclusive cursor bounds of a buffer. */ export function isValidPosition( buffer: PromptBuffer, position: Position, ): boolean { if (!Number.isInteger(position.line) || !Number.isInteger(position.column)) return false; if ( position.line < 0 || position.line >= buffer.lines.length || position.column < 0 ) return false; const line = buffer.lines[position.line]; return line !== undefined && position.column <= line.length; }