ci / build (push) Canceled after 0s
- 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
102 lines
5.0 KiB
TypeScript
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
|
|
}
|