/** * Pure request-building logic (no vscode import), unit-tested in test/. */ export interface DocumentSlice { prefix: string suffix: string /** Absolute line number where `prefix` starts (non-zero only when truncated). */ prefixLineOffset: number } /** * Splits the document at the cursor. Very large prefixes are cut at a line * whose number is a multiple of `lineChunk`, so the cut point (and the daemon's * KV cache) stays stable while typing. */ export function sliceDocument(text: string, offset: number, maxPrefixChars: number, maxSuffixChars: number, lineChunk = 256): DocumentSlice { let prefix = text.slice(0, offset) let prefixLineOffset = 0 if (prefix.length > maxPrefixChars) { const lineStarts = [0] for (let i = prefix.indexOf('\n'); i !== -1; i = prefix.indexOf('\n', i + 1)) lineStarts.push(i + 1) let line = 0 while (line + lineChunk < lineStarts.length && prefix.length - lineStarts[line] > maxPrefixChars) line += lineChunk prefix = prefix.slice(lineStarts[line]) prefixLineOffset = line } let suffix = text.slice(offset, offset + maxSuffixChars) if (offset + maxSuffixChars < text.length) suffix = suffix.slice(0, suffix.lastIndexOf('\n') + 1) return { prefix, suffix, prefixLineOffset } } /** * When VS Code's suggest widget is open, an inline completion must extend the * selected suggestion. We pretend the selected text was already typed: the * prefix ends at the replace range's start plus the selected text. */ export function applySelectedSuggestion(text: string, rangeStartOffset: number, cursorOffset: number, selectedText: string): { text: string; cursorOffset: number } { return { text: text.slice(0, rangeStartOffset) + selectedText + text.slice(cursorOffset), cursorOffset: rangeStartOffset + selectedText.length, } } /** Repo name and '/'-separated relative path, as the model saw them in training. */ export function repoAndPath(workspaceFolderName: string | undefined, relativePath: string, fileName: string): { repo: string; path: string } { if (!workspaceFolderName) return { repo: 'untitled', path: fileName.split(/[\\/]/).pop() || 'untitled' } return { repo: workspaceFolderName, path: relativePath.replace(/\\/g, '/') } } export type ContextMode = 'symbols' | 'simple' | 'raw' export const MODE_DESCRIPTIONS: Record = { symbols: 'repo header + available symbols from the TypeScript language service', simple: 'repo name + file path header only (fast, no language service)', raw: 'no header: plain suffix-first FIM (for models trained without it)', } export function statusText(cfg: { enabled: boolean; mode: ContextMode; effectiveMode?: ContextMode; model: string; llama?: { loading: boolean; running: boolean } } | null, lastMs: number | null): string { if (!cfg) return '$(debug-disconnect) AC: no daemon' if (!cfg.enabled) return '$(circle-slash) AC: off' if (cfg.llama?.loading) return '$(loading~spin) AC: loading' const mode = cfg.effectiveMode && cfg.effectiveMode !== cfg.mode ? `${cfg.mode}→${cfg.effectiveMode}` : cfg.mode return `$(sparkle) AC: ${mode}${lastMs !== null ? ` · ${Math.round(lastMs)} ms` : ''}` } /** Line-block diff of a region: the first/last changed lines (old and new) after trimming common lines. */ export function lineBlockDiff(oldText: string, newText: string): { oldStart: number; oldEnd: number; newLines: string[]; oldLines: string[] } | null { const a = oldText.split('\n'), b = newText.split('\n') let p = 0 while (p < a.length && p < b.length && a[p] === b[p]) p++ if (p === a.length && p === b.length) return null let s = 0 while (s < a.length - p && s < b.length - p && a[a.length - 1 - s] === b[b.length - 1 - s]) s++ return { oldStart: p, oldEnd: a.length - s, oldLines: a.slice(p, a.length - s), newLines: b.slice(p, b.length - s) } } /** Character diff of one line: deleted [start, end) in the old line and the text inserted there. */ export function charDiff(oldLine: string, newLine: string): { start: number; deleteEnd: number; insert: string } { let p = 0 while (p < oldLine.length && p < newLine.length && oldLine[p] === newLine[p]) p++ let s = 0 while (s < oldLine.length - p && s < newLine.length - p && oldLine[oldLine.length - 1 - s] === newLine[newLine.length - 1 - s]) s++ return { start: p, deleteEnd: oldLine.length - s, insert: newLine.slice(p, newLine.length - s) } } /** Where cursor-move triggers make sense for plain FIM: empty line, end of line, or on a line with an error. */ export function insertionMakesSense(lineText: string, column: number, lineHasError: boolean): boolean { if (!lineText.trim()) return true if (!lineText.slice(column).trim()) return true return lineHasError } /** Semver-ish comparison of "a.b.c" strings. */ export function newerVersion(candidate: string, current: string): boolean { const pa = candidate.split('.').map(Number), pb = current.split('.').map(Number) for (let i = 0; i < Math.max(pa.length, pb.length); i++) { if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0) } return false }