Files
autocomplete-model/extensions/vscode/src/logic.ts
T
HarvmasterandClaude Opus 5.5 87d3c3c3bf
ci / build (push) Canceled after 0s
Extension 0.6.0: next-edit, token healing, TS errors context, cursor-move, prewarm
- shared/nextedit.ts: next-edit prompt format (region rewrite or NO_EDIT, Qwen spare special tokens)
- shared/diagnostics.ts: TS errors near the cursor as context; symbols format 2 adds namespace imports
- shared/prompt.ts: token healing (cut the half-typed word, force it via grammar), errors header section
- daemon: model profiles carry contextFormat/errors/members/nextEdit; /v1/next-edit with edit-burst
  tracking; /v1/prewarm; /v1/debug/last; token healing on by default
- extension: next-edit decorations (Tab apply / Esc dismiss), suggest on cursor move, prewarm on focus,
  Show Last Prompt, update check against releases, diagnostics in the edit recorder
- models/README: training-data attribution; release script profiles for stage-d/stage-e
- .gitea/workflows/ci.yml: build + test, publish release on vscode-v* tags (needs a runner + RELEASE_TOKEN)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J39imRDT8L7xYbWzsVWd8A
2026-10-03 17:21:38 +00:00

102 lines
5.0 KiB
TypeScript

/**
* 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<ContextMode, string> = {
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
}