export type TextSegment = { text: string; start: number; end: number; }; const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme", }); const pasteMarkerPattern = /\[paste #(\d+)( (\+\d+ lines|\d+ chars))?\]/g; function segmentGraphemes(text: string, offset: number): TextSegment[] { return [...graphemeSegmenter.segment(text)].map((segment) => ({ text: segment.segment, start: offset + segment.index, end: offset + segment.index + segment.segment.length, })); } /** * Segment prompt text into cursor-safe units. Known Pi large-paste markers stay * atomic so vi motions and operators cannot split their rendered placeholder. */ export function segmentPromptText( text: string, pasteMarkerIds: ReadonlySet = new Set(), ): TextSegment[] { if (pasteMarkerIds.size === 0 || !text.includes("[paste #")) { return segmentGraphemes(text, 0); } const segments: TextSegment[] = []; let offset = 0; for (const match of text.matchAll(pasteMarkerPattern)) { const index = match.index; const marker = match[0]; const id = Number(match[1]); if (index === undefined || marker === undefined || !pasteMarkerIds.has(id)) continue; segments.push(...segmentGraphemes(text.slice(offset, index), offset)); segments.push({ text: marker, start: index, end: index + marker.length }); offset = index + marker.length; } segments.push(...segmentGraphemes(text.slice(offset), offset)); return segments; } /** Return whether an offset lies between complete cursor-safe segments. */ export function isSegmentBoundary( segments: readonly TextSegment[], offset: number, ): boolean { if (!Number.isInteger(offset) || offset < 0) return false; if (segments.length === 0) return offset === 0; return offset === 0 || segments.some((segment) => segment.end === offset); }