import { detectComposerTrigger as detectComposerTriggerBase, type ComposerTrigger, type ComposerTriggerKind, type ComposerSlashCommand, } from "@t3tools/shared/composerTrigger"; export { replaceTextRange, parseStandaloneComposerSlashCommand, } from "@t3tools/shared/composerTrigger"; export type { ComposerTrigger, ComposerTriggerKind, ComposerSlashCommand }; import { splitPromptIntoComposerSegments } from "./composer-editor-mentions"; import { INLINE_TERMINAL_CONTEXT_PLACEHOLDER } from "./lib/terminalContext"; const isInlineTokenSegment = ( segment: | { type: "text"; text: string } | { type: "mention" } | { type: "skill" } | { type: "terminal-context" }, ): boolean => segment.type !== "text"; function clampCursor(text: string, cursor: number): number { if (!Number.isFinite(cursor)) return text.length; return Math.max(0, Math.min(text.length, Math.floor(cursor))); } function isWhitespace(char: string): boolean { return ( char === " " || char === "\n" || char === "\t" || char === "\r" || char === INLINE_TERMINAL_CONTEXT_PLACEHOLDER ); } export function expandCollapsedComposerCursor(text: string, cursorInput: number): number { const collapsedCursor = clampCursor(text, cursorInput); const segments = splitPromptIntoComposerSegments(text); if (segments.length === 0) { return collapsedCursor; } let remaining = collapsedCursor; let expandedCursor = 0; for (const segment of segments) { if (segment.type === "mention") { const expandedLength = segment.path.length + 1; if (remaining <= 1) { return expandedCursor + (remaining === 0 ? 0 : expandedLength); } remaining -= 1; expandedCursor += expandedLength; continue; } if (segment.type === "skill") { const expandedLength = segment.name.length + 1; if (remaining <= 1) { return expandedCursor + (remaining === 0 ? 0 : expandedLength); } remaining -= 1; expandedCursor += expandedLength; continue; } if (segment.type === "terminal-context") { if (remaining <= 1) { return expandedCursor + remaining; } remaining -= 1; expandedCursor += 1; continue; } const segmentLength = segment.text.length; if (remaining <= segmentLength) { return expandedCursor + remaining; } remaining -= segmentLength; expandedCursor += segmentLength; } return expandedCursor; } function collapsedSegmentLength( segment: | { type: "text"; text: string } | { type: "mention" } | { type: "skill" } | { type: "terminal-context" }, ): number { if (segment.type === "text") { return segment.text.length; } return 1; } function clampCollapsedComposerCursorForSegments( segments: ReadonlyArray< | { type: "text"; text: string } | { type: "mention" } | { type: "skill" } | { type: "terminal-context" } >, cursorInput: number, ): number { const collapsedLength = segments.reduce( (total, segment) => total + collapsedSegmentLength(segment), 0, ); if (!Number.isFinite(cursorInput)) { return collapsedLength; } return Math.max(0, Math.min(collapsedLength, Math.floor(cursorInput))); } export function clampCollapsedComposerCursor(text: string, cursorInput: number): number { return clampCollapsedComposerCursorForSegments( splitPromptIntoComposerSegments(text), cursorInput, ); } export function collapseExpandedComposerCursor(text: string, cursorInput: number): number { const expandedCursor = clampCursor(text, cursorInput); const segments = splitPromptIntoComposerSegments(text); if (segments.length === 0) { return expandedCursor; } let remaining = expandedCursor; let collapsedCursor = 0; for (const segment of segments) { if (segment.type === "mention") { const expandedLength = segment.path.length + 1; if (remaining === 0) { return collapsedCursor; } if (remaining <= expandedLength) { return collapsedCursor + 1; } remaining -= expandedLength; collapsedCursor += 1; continue; } if (segment.type === "skill") { const expandedLength = segment.name.length + 1; if (remaining === 0) { return collapsedCursor; } if (remaining <= expandedLength) { return collapsedCursor + 1; } remaining -= expandedLength; collapsedCursor += 1; continue; } if (segment.type === "terminal-context") { if (remaining <= 1) { return collapsedCursor + remaining; } remaining -= 1; collapsedCursor += 1; continue; } const segmentLength = segment.text.length; if (remaining <= segmentLength) { return collapsedCursor + remaining; } remaining -= segmentLength; collapsedCursor += segmentLength; } return collapsedCursor; } export function isCollapsedCursorAdjacentToInlineToken( text: string, cursorInput: number, direction: "left" | "right", ): boolean { const segments = splitPromptIntoComposerSegments(text); if (!segments.some(isInlineTokenSegment)) { return false; } const cursor = clampCollapsedComposerCursorForSegments(segments, cursorInput); let collapsedOffset = 0; for (const segment of segments) { if (isInlineTokenSegment(segment)) { if (direction === "left" && cursor === collapsedOffset + 1) { return true; } if (direction === "right" && cursor === collapsedOffset) { return true; } } collapsedOffset += collapsedSegmentLength(segment); } return false; } export const isCollapsedCursorAdjacentToMention = isCollapsedCursorAdjacentToInlineToken; export function detectComposerTrigger(text: string, cursorInput: number): ComposerTrigger | null { return detectComposerTriggerBase(text, cursorInput, isWhitespace); }