Extension 0.6.0: next-edit, token healing, TS errors context, cursor-move, prewarm
ci / build (push) Canceled after 0s
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
This commit is contained in:
@@ -16,6 +16,7 @@ import ts from 'typescript'
|
||||
import { parse as parseSfc } from '@vue/compiler-sfc'
|
||||
|
||||
import type { SymbolLang } from './symbol-lang.ts'
|
||||
import type { Diagnostic } from './diagnostics.ts'
|
||||
export { symbolLang, type SymbolLang } from './symbol-lang.ts'
|
||||
|
||||
export interface SymbolsRequest {
|
||||
@@ -30,6 +31,11 @@ export interface SymbolsRequest {
|
||||
countTokens: (s: string) => number
|
||||
/** Include member completions when the cursor follows `expr.` (default true, as in training). */
|
||||
members?: boolean
|
||||
/**
|
||||
* Context format version. 1 = Stage C's training format (default, byte-identical to before).
|
||||
* 2 = also namespace imports (`import * as x`): x's members, the ones this file uses first, with signatures.
|
||||
*/
|
||||
format?: 1 | 2
|
||||
}
|
||||
|
||||
export interface SymbolsResult { context: string; items: number; hasMembers: boolean }
|
||||
@@ -181,6 +187,14 @@ export class WorkspaceSymbols {
|
||||
const names: ts.Identifier[] = []
|
||||
if (stmt.importClause.name) names.push(stmt.importClause.name)
|
||||
const bindings = stmt.importClause.namedBindings
|
||||
if ((req.format ?? 1) >= 2 && bindings && ts.isNamespaceImport(bindings)) {
|
||||
const ns = this.namespaceMembers(fileName, tsText, bindings.name.text)
|
||||
if (ns) {
|
||||
const used = new RegExp(`\\b${bindings.name.text.replace(/\$/g, '\\$')}\\.`).test(near)
|
||||
items.push({ priority: used ? 1 : 2, text: `// from '${module}' (namespace ${bindings.name.text})\n${ns}` })
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (bindings && ts.isNamedImports(bindings)) for (const el of bindings.elements) names.push(el.name)
|
||||
for (const id of names) {
|
||||
const pos = id.getStart(sf)
|
||||
@@ -209,6 +223,66 @@ export class WorkspaceSymbols {
|
||||
return chosen.length ? { context: chosen.join('\n'), items: chosen.length, hasMembers: chosen[0].startsWith('// members of') } : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Members of a namespace import, as signature lines. Ranked by how often this file already uses each
|
||||
* member (`ns.member`), then by TypeScript's own completion order; details for the top 12.
|
||||
*/
|
||||
private namespaceMembers(fileName: string, tsText: string, ns: string): string | null {
|
||||
// Probe: ask for completions at `ns.` appended to the file, then restore.
|
||||
const probe = `${tsText}\n;${ns}.`
|
||||
this.overrides.set(fileName, { text: probe, version: ++this.version })
|
||||
try {
|
||||
const pos = probe.length
|
||||
const completions = this.service.getCompletionsAtPosition(fileName, pos, {})
|
||||
if (!completions?.entries.length) return null
|
||||
const usage = new Map<string, number>()
|
||||
for (const m of tsText.matchAll(new RegExp(`\\b${ns.replace(/\$/g, '\\$')}\\.([A-Za-z_$][\\w$]*)`, 'g'))) usage.set(m[1], (usage.get(m[1]) ?? 0) + 1)
|
||||
const entries = [...completions.entries].sort((a, b) => (usage.get(b.name) ?? 0) - (usage.get(a.name) ?? 0) || a.sortText.localeCompare(b.sortText) || a.name.localeCompare(b.name)).slice(0, 40)
|
||||
const detailed = entries.slice(0, 12).map(e => {
|
||||
const d = this.service.getCompletionEntryDetails(fileName, pos, e.name, undefined, undefined, undefined, undefined)
|
||||
return clip(displayText(d?.displayParts).replace(/^\((method|property|function|const|alias|let|var)\) /, ''), 200)
|
||||
})
|
||||
const rest = entries.slice(12).map(e => e.name)
|
||||
return `${detailed.join('\n')}${rest.length ? `\n// also: ${rest.join(', ')}` : ''}`
|
||||
} finally {
|
||||
this.overrides.set(fileName, { text: tsText, version: ++this.version })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntactic + semantic diagnostics of the visible file (Vue: its script blocks), with lines mapped back to the
|
||||
* visible file. Used for "TS errors as context" and to verify generated next-edit data.
|
||||
*/
|
||||
diagnostics(filePath: string, lang: SymbolLang, visible: string): Diagnostic[] {
|
||||
const loaded = this.load(filePath, lang, visible, 0)
|
||||
if (!loaded) return []
|
||||
// Map TS-text offsets back to visible-file lines (Vue script blocks are concatenated with '\n').
|
||||
const segments: { tsStart: number; visStart: number; length: number }[] = []
|
||||
if (lang === 'vue') {
|
||||
let tsPos = 0
|
||||
for (const r of vueScriptRegions(visible)) {
|
||||
segments.push({ tsStart: tsPos, visStart: r.start, length: r.end - r.start })
|
||||
tsPos += r.end - r.start + 1
|
||||
}
|
||||
} else {
|
||||
segments.push({ tsStart: 0, visStart: 0, length: visible.length })
|
||||
}
|
||||
const toVisibleLine = (tsOffset: number) => {
|
||||
const seg = segments.find(g => tsOffset >= g.tsStart && tsOffset <= g.tsStart + g.length) ?? segments[0]
|
||||
const vis = seg.visStart + (tsOffset - seg.tsStart)
|
||||
return visible.slice(0, vis).split('\n').length - 1
|
||||
}
|
||||
const all = [...this.service.getSyntacticDiagnostics(loaded.fileName), ...this.service.getSemanticDiagnostics(loaded.fileName)]
|
||||
return all
|
||||
.filter(d => d.start !== undefined && (d.category === ts.DiagnosticCategory.Error || d.category === ts.DiagnosticCategory.Warning))
|
||||
.map(d => ({
|
||||
line: toVisibleLine(d.start!),
|
||||
code: d.code,
|
||||
message: ts.flattenDiagnosticMessageText(d.messageText, '\n'),
|
||||
category: d.category === ts.DiagnosticCategory.Error ? 'error' as const : 'warning' as const,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* If the identifier being typed at the cursor only resolves as a member of
|
||||
* the enclosing class (the user forgot `this.`), returns the member name.
|
||||
|
||||
Reference in New Issue
Block a user