From ae9e413081eeeac8e22ec542992d73fd8298670b Mon Sep 17 00:00:00 2001 From: KazariAI <166915487+KazariAI@users.noreply.github.com> Date: Thu, 6 Aug 2026 19:58:16 +0800 Subject: [PATCH 1/5] fix(bridge): preserve auto-import symbol identity --- patches/typescript/0001-tsgo-hooks.patch | 21 ++++-- .../overlay/src/compiler/tsgoChecker.ts | 29 ++++++-- tools/triage-local-autoimport-details.mjs | 71 +++++++++++++++++++ 3 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 tools/triage-local-autoimport-details.mjs diff --git a/patches/typescript/0001-tsgo-hooks.patch b/patches/typescript/0001-tsgo-hooks.patch index c9608a5..8ab6f1e 100644 --- a/patches/typescript/0001-tsgo-hooks.patch +++ b/patches/typescript/0001-tsgo-hooks.patch @@ -609,7 +609,7 @@ index 6034c9dfc3..0c9a6178db 100644 let computedWithoutCacheCount = 0; const fixes = flatMap(exportInfo, (exportInfo, i) => { diff --git a/src/services/completions.ts b/src/services/completions.ts -index 28d29136da..e267aa7847 100644 +index 28d29136da..b15f4b0ea5 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -175,6 +175,7 @@ import { @@ -964,7 +964,7 @@ index 28d29136da..e267aa7847 100644 symbolToOriginInfoMap[symbols.length] = origin; symbolToSortTextMap[symbolId] = importStatementCompletion ? SortText.LocationPriority : SortText.AutoImportSuggestions; symbols.push(symbol); -@@ -5383,7 +5511,7 @@ function getAutoImportSymbolFromCompletionEntryData(name: string, data: Completi +@@ -5383,19 +5511,34 @@ function getAutoImportSymbolFromCompletionEntryData(name: string, data: Completi const containingProgram = data.isPackageJsonImport ? host.getPackageJsonAutoImportProvider!()! : program; const checker = containingProgram.getTypeChecker(); const moduleSymbol = data.ambientModuleName ? checker.tryFindAmbientModule(data.ambientModuleName) : @@ -973,7 +973,20 @@ index 28d29136da..e267aa7847 100644 undefined; if (!moduleSymbol) return undefined; -@@ -5396,6 +5524,16 @@ function getAutoImportSymbolFromCompletionEntryData(name: string, data: Completi +- let symbol = data.exportName === InternalSymbolName.ExportEquals +- ? checker.resolveExternalModuleSymbol(moduleSymbol) +- : checker.tryGetMemberInModuleExportsAndProperties(data.exportName, moduleSymbol); ++ const getBridgeExport = (checker as unknown as { ++ getExportForCompletionEntryData?: (exportName: string, moduleSymbol: Symbol) => Symbol | undefined; ++ }).getExportForCompletionEntryData; ++ let symbol = getBridgeExport ++ ? getBridgeExport(data.exportName, moduleSymbol) ++ : data.exportName === InternalSymbolName.ExportEquals ++ ? checker.resolveExternalModuleSymbol(moduleSymbol) ++ : checker.tryGetMemberInModuleExportsAndProperties(data.exportName, moduleSymbol); + if (!symbol) return undefined; + const isDefaultExport = data.exportName === InternalSymbolName.Default; + symbol = isDefaultExport && getLocalSymbolForExportDefault(symbol) || symbol; return { symbol, origin: completionEntryDataToSymbolOriginInfo(data, name, moduleSymbol) }; } @@ -990,7 +1003,7 @@ index 28d29136da..e267aa7847 100644 interface CompletionEntryDisplayNameForSymbol { readonly name: string; readonly needsConvertPropertyAccess: boolean; -@@ -6089,6 +6227,23 @@ function isDeprecated(symbol: Symbol, checker: TypeChecker) { +@@ -6089,6 +6232,23 @@ function isDeprecated(symbol: Symbol, checker: TypeChecker) { return !!length(declarations) && every(declarations, isDeprecatedDeclaration); } diff --git a/patches/typescript/overlay/src/compiler/tsgoChecker.ts b/patches/typescript/overlay/src/compiler/tsgoChecker.ts index bd220a1..e7f5e93 100644 --- a/patches/typescript/overlay/src/compiler/tsgoChecker.ts +++ b/patches/typescript/overlay/src/compiler/tsgoChecker.ts @@ -19,7 +19,7 @@ import * as ts from "./_namespaces/ts.js"; import { bindSourceFile } from "./binder.js"; import { createSourceFile } from "./parser.js"; -import { SyntaxKind, SymbolFlags, SymbolFormatFlags, NodeFlags, ModifierFlags, JSDocParsingMode, ModuleKind, StructureIsReused, EmitHint, EmitFlags, type Path, type Program, type TypeChecker } from "./types.js"; +import { SyntaxKind, SymbolFlags, SymbolFormatFlags, NodeFlags, ModifierFlags, JSDocParsingMode, ModuleKind, StructureIsReused, EmitHint, EmitFlags, InternalSymbolName, type Path, type Program, type TypeChecker } from "./types.js"; import { getBuildInfoText, getTsBuildInfoEmitOutputFilePath, createPrinterWithRemoveComments } from "./emitter.js"; import { usingSingleLineStringWriter, canHaveJSDoc } from "./utilities.js"; import { getParseTreeNode, isFunctionLike } from "./utilitiesPublic.js"; @@ -10867,10 +10867,10 @@ export function createTsgoChecker(program: any): any { return true; } - function tryGetMemberInModuleExportsImpl(memberName: any, moduleSymbol: any): any { + function getMemberInModuleExportsRaw(memberName: any, moduleSymbol: any): any { if (moduleSymbol?.exports) { const sym = moduleSymbol.exports.get(memberName); - if (sym) return refineNavSymbol(sym); + if (sym) return sym; // Fall through on miss: the raw host table holds direct members only, // while stock's getExportsOfModule (checker.ts:5155) also resolves // `export *` chains — let the Go side answer those. Returning @@ -10881,12 +10881,16 @@ export function createTsgoChecker(program: any): any { } ensureProject(); try { - return refineNavSymbol(rpc().getMemberInModuleExports(moduleSymbol, memberName)); + return rpc().getMemberInModuleExports(moduleSymbol, memberName); } catch { return undefined; } } - function tryGetMemberInModuleExportsAndPropertiesImpl(memberName: any, moduleSymbol: any): any { - const symbol = tryGetMemberInModuleExportsImpl(memberName, moduleSymbol); + function tryGetMemberInModuleExportsImpl(memberName: any, moduleSymbol: any): any { + return refineNavSymbol(getMemberInModuleExportsRaw(memberName, moduleSymbol)); + } + + function getMemberInModuleExportsAndPropertiesRaw(memberName: any, moduleSymbol: any): any { + const symbol = getMemberInModuleExportsRaw(memberName, moduleSymbol); if (symbol) return symbol; ensureProject(); @@ -10897,7 +10901,11 @@ export function createTsgoChecker(program: any): any { const exportEqualsType = getTypeOfSymbolForExportEquals(exportEquals); if (!exportEqualsType || !shouldTreatPropertiesOfExternalModuleAsExports(exportEqualsType)) return undefined; - return refineNavSymbol(resolvePropertyOfType(exportEqualsType, memberName)); + return resolvePropertyOfType(exportEqualsType, memberName); + } + + function tryGetMemberInModuleExportsAndPropertiesImpl(memberName: any, moduleSymbol: any): any { + return refineNavSymbol(getMemberInModuleExportsAndPropertiesRaw(memberName, moduleSymbol)); } function getSymbolFlagsImpl(symbol: any, excludeLocalMeanings?: boolean): number { @@ -13080,6 +13088,13 @@ export function createTsgoChecker(program: any): any { tryGetMemberInModuleExportsAndProperties(memberName: any, moduleSymbol: any): any { return tryGetMemberInModuleExportsAndPropertiesImpl(memberName, moduleSymbol); }, + // Completion details match this result against the batch export map by + // object identity; navigation remapping would replace the registry symbol. + getExportForCompletionEntryData(exportName: any, moduleSymbol: any): any { + return exportName === InternalSymbolName.ExportEquals + ? resolveExternalModuleSymbolImpl(moduleSymbol) + : getMemberInModuleExportsAndPropertiesRaw(exportName, moduleSymbol); + }, resolveExternalModuleSymbol(moduleSymbol: any): any { return refineNavSymbol(resolveExternalModuleSymbolImpl(moduleSymbol)); }, diff --git a/tools/triage-local-autoimport-details.mjs b/tools/triage-local-autoimport-details.mjs new file mode 100644 index 0000000..9563a50 --- /dev/null +++ b/tools/triage-local-autoimport-details.mjs @@ -0,0 +1,71 @@ +#!/usr/bin/env node +/** + * Issue #58: a project-local auto-import completion must resolve to an import + * edit when its entry data is sent back through completionEntryDetails. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { tnbHarnessEnv, withTsserver } from './tsserver-harness.mjs'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const tsserverPath = path.join(repoRoot, 'lib', 'tsserver.js'); +const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'tnb-local-autoimport-')); +const main = path.join(fixture, 'src', 'main.ts'); +const model = path.join(fixture, 'src', 'model.ts'); + +fs.mkdirSync(path.dirname(main), { recursive: true }); +fs.writeFileSync(path.join(fixture, 'tsconfig.json'), JSON.stringify({ include: ['./src/*.ts'] })); +fs.writeFileSync(main, 'ReadRecordModel;\n'); +fs.writeFileSync(model, 'export const ReadRecordModel = {};\n'); + +try { + const result = await withTsserver({ + tsserverPath, + args: ['--disableAutomaticTypingAcquisition', '--suppressDiagnosticEvents'], + env: tnbHarnessEnv(), + }, async ({ send }) => { + await send('configure', { + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsWithInsertText: true, + }, + }); + await send('updateOpen', { + changedFiles: [], + closedFiles: [], + openFiles: [{ file: main, fileContent: 'ReadRecordModel;\n', projectRootPath: fixture }], + }); + + const completion = await send('completionInfo', { + file: main, + line: 1, + offset: 16, + includeExternalModuleExports: true, + includeInsertTextCompletions: true, + }); + const entry = completion.body?.entries?.find(candidate => candidate.name === 'ReadRecordModel' && candidate.source === './model'); + if (!entry) throw new Error('completionInfo did not return ReadRecordModel from ./model'); + + const details = await send('completionEntryDetails', { + file: main, + line: 1, + offset: 16, + entryNames: [{ name: entry.name, source: entry.source, data: entry.data }], + }); + if (!details.success) throw new Error(details.message || 'completionEntryDetails failed'); + const changes = details.body?.[0]?.codeActions?.flatMap(action => action.changes ?? []) ?? []; + const importEdit = changes + .filter(change => change.fileName === main) + .flatMap(change => change.textChanges ?? []) + .find(change => change.newText.includes('ReadRecordModel') && change.newText.includes('./model')); + if (!importEdit) throw new Error('completionEntryDetails returned no ./model import edit'); + return { data: entry.data, edit: importEdit.newText }; + }); + console.log(`ok project-local auto-import details: ${JSON.stringify(result)}`); +} +finally { + fs.rmSync(fixture, { recursive: true, force: true }); +} +process.exit(0); From 12e5716f8b4adac87a6b13103d233a4d1fa9de95 Mon Sep 17 00:00:00 2001 From: KazariAI <166915487+KazariAI@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:15:24 +0800 Subject: [PATCH 2/5] fix(bridge): make completion symbol resolution explicit --- patches/typescript/0001-tsgo-hooks.patch | 56 +++++++++---------- .../overlay/src/compiler/tsgoChecker.ts | 12 ++++ 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/patches/typescript/0001-tsgo-hooks.patch b/patches/typescript/0001-tsgo-hooks.patch index 8ab6f1e..4a62dec 100644 --- a/patches/typescript/0001-tsgo-hooks.patch +++ b/patches/typescript/0001-tsgo-hooks.patch @@ -609,7 +609,7 @@ index 6034c9dfc3..0c9a6178db 100644 let computedWithoutCacheCount = 0; const fixes = flatMap(exportInfo, (exportInfo, i) => { diff --git a/src/services/completions.ts b/src/services/completions.ts -index 28d29136da..b15f4b0ea5 100644 +index 28d29136da..48fa363863 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -175,6 +175,7 @@ import { @@ -964,46 +964,40 @@ index 28d29136da..b15f4b0ea5 100644 symbolToOriginInfoMap[symbols.length] = origin; symbolToSortTextMap[symbolId] = importStatementCompletion ? SortText.LocationPriority : SortText.AutoImportSuggestions; symbols.push(symbol); -@@ -5383,19 +5511,34 @@ function getAutoImportSymbolFromCompletionEntryData(name: string, data: Completi +@@ -5382,14 +5510,25 @@ function getRelevantTokens(position: number, sourceFile: SourceFile): { contextT + function getAutoImportSymbolFromCompletionEntryData(name: string, data: CompletionEntryData, program: Program, host: LanguageServiceHost): { symbol: Symbol; origin: SymbolOriginInfoExport | SymbolOriginInfoResolvedExport; } | undefined { const containingProgram = data.isPackageJsonImport ? host.getPackageJsonAutoImportProvider!()! : program; const checker = containingProgram.getTypeChecker(); - const moduleSymbol = data.ambientModuleName ? checker.tryFindAmbientModule(data.ambientModuleName) : +- const moduleSymbol = data.ambientModuleName ? checker.tryFindAmbientModule(data.ambientModuleName) : - data.fileName ? checker.getMergedSymbol(Debug.checkDefined(containingProgram.getSourceFile(data.fileName)).symbol) : -+ data.fileName ? getModuleSymbolForCompletionEntryData(containingProgram, checker, data.fileName) : - undefined; - +- undefined; +- ++ const tsgoProgram = containingProgram as Program & { ++ readonly isTsgoBackedProgram: true; ++ getCompletionEntryDataSymbols(fileName: string | undefined, ambientModuleName: string | undefined, exportName: string): { moduleSymbol: Symbol; symbol: Symbol; } | undefined; ++ }; ++ let moduleSymbol: Symbol | undefined; ++ let symbol: Symbol | undefined; ++ if (tsgoProgram.isTsgoBackedProgram === true) { ++ const resolved = tsgoProgram.getCompletionEntryDataSymbols(data.fileName, data.ambientModuleName, data.exportName); ++ if (resolved) ({ moduleSymbol, symbol } = resolved); ++ } ++ else { ++ moduleSymbol = data.ambientModuleName ? checker.tryFindAmbientModule(data.ambientModuleName) : ++ data.fileName ? checker.getMergedSymbol(Debug.checkDefined(containingProgram.getSourceFile(data.fileName)).symbol) : ++ undefined; ++ symbol = moduleSymbol && (data.exportName === InternalSymbolName.ExportEquals ++ ? checker.resolveExternalModuleSymbol(moduleSymbol) ++ : checker.tryGetMemberInModuleExportsAndProperties(data.exportName, moduleSymbol)); ++ } if (!moduleSymbol) return undefined; - let symbol = data.exportName === InternalSymbolName.ExportEquals - ? checker.resolveExternalModuleSymbol(moduleSymbol) - : checker.tryGetMemberInModuleExportsAndProperties(data.exportName, moduleSymbol); -+ const getBridgeExport = (checker as unknown as { -+ getExportForCompletionEntryData?: (exportName: string, moduleSymbol: Symbol) => Symbol | undefined; -+ }).getExportForCompletionEntryData; -+ let symbol = getBridgeExport -+ ? getBridgeExport(data.exportName, moduleSymbol) -+ : data.exportName === InternalSymbolName.ExportEquals -+ ? checker.resolveExternalModuleSymbol(moduleSymbol) -+ : checker.tryGetMemberInModuleExportsAndProperties(data.exportName, moduleSymbol); if (!symbol) return undefined; const isDefaultExport = data.exportName === InternalSymbolName.Default; symbol = isDefaultExport && getLocalSymbolForExportDefault(symbol) || symbol; - return { symbol, origin: completionEntryDataToSymbolOriginInfo(data, name, moduleSymbol) }; - } - -+function getModuleSymbolForCompletionEntryData(program: Program, checker: TypeChecker, fileName: string): Symbol { -+ const sourceFile = Debug.checkDefined(program.getSourceFile(fileName)); -+ // tsgo-backed programs hand out a host-binder facade for sf.symbol whose -+ // object identity does not match the registry-proxied symbols inside -+ // export info maps; resolve through the bridged checker when it can answer -+ // (absent on stock checkers — falls back to stock behavior). -+ const forSourceFile = (checker as unknown as { getModuleSymbolForSourceFile?: (sourceFile: SourceFile) => Symbol | undefined; }).getModuleSymbolForSourceFile; -+ return checker.getMergedSymbol(forSourceFile?.(sourceFile) ?? Debug.checkDefined(sourceFile.symbol)); -+} -+ - interface CompletionEntryDisplayNameForSymbol { - readonly name: string; - readonly needsConvertPropertyAccess: boolean; -@@ -6089,6 +6232,23 @@ function isDeprecated(symbol: Symbol, checker: TypeChecker) { +@@ -6089,6 +6228,23 @@ function isDeprecated(symbol: Symbol, checker: TypeChecker) { return !!length(declarations) && every(declarations, isDeprecatedDeclaration); } diff --git a/patches/typescript/overlay/src/compiler/tsgoChecker.ts b/patches/typescript/overlay/src/compiler/tsgoChecker.ts index e7f5e93..6b6e9d1 100644 --- a/patches/typescript/overlay/src/compiler/tsgoChecker.ts +++ b/patches/typescript/overlay/src/compiler/tsgoChecker.ts @@ -8196,6 +8196,18 @@ export function createTsgoProgram( return result; }, getTypeChecker: () => checker, + // Stock completion details compare these against the batch export map + // by object identity, so both symbols stay in the registry domain. + getCompletionEntryDataSymbols: (fileName: string | undefined, ambientModuleName: string | undefined, exportName: string) => { + const moduleSymbol = ambientModuleName + ? checker.tryFindAmbientModule(ambientModuleName) + : fileName + ? checker.getModuleSymbolForSourceFile(thinProgram.getSourceFile(fileName)) + : undefined; + if (!moduleSymbol) return undefined; + const symbol = checker.getExportForCompletionEntryData(exportName, moduleSymbol); + return symbol ? { moduleSymbol, symbol } : undefined; + }, getConfigFileParsingDiagnostics: () => configDiags, getOptionsDiagnostics: () => [], getSemanticDiagnostics: (sourceFile?: any) => { From be5b8d8affe78e3c411bdc011c78564c279eab03 Mon Sep 17 00:00:00 2001 From: KazariAI <166915487+KazariAI@users.noreply.github.com> Date: Thu, 6 Aug 2026 22:25:32 +0800 Subject: [PATCH 3/5] fix(bridge): resolve native auto-import completions --- patches/typescript-go/0004-api-surface.patch | 154 ++++++++++-------- .../overlay/internal/api/arena.go | 36 +++- .../overlay/internal/api/completionresolve.go | 62 +++++++ patches/typescript/0001-tsgo-hooks.patch | 63 ++++++- .../overlay/src/compiler/tsgoChecker.ts | 46 +++++- tools/triage-arena-parity.mjs | 21 ++- tools/triage-local-autoimport-details.mjs | 40 +++-- 7 files changed, 324 insertions(+), 98 deletions(-) create mode 100644 patches/typescript-go/overlay/internal/api/completionresolve.go diff --git a/patches/typescript-go/0004-api-surface.patch b/patches/typescript-go/0004-api-surface.patch index 8850778..581dccf 100644 --- a/patches/typescript-go/0004-api-surface.patch +++ b/patches/typescript-go/0004-api-surface.patch @@ -4424,7 +4424,7 @@ index 6a90030eb..fc58b512a 100644 const docFiles = { "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), diff --git a/internal/api/proto.go b/internal/api/proto.go -index 0917c385d..543e9c90b 100644 +index 0917c385d..f3245b617 100644 --- a/internal/api/proto.go +++ b/internal/api/proto.go @@ -67,37 +67,81 @@ const ( @@ -4703,10 +4703,11 @@ index 0917c385d..543e9c90b 100644 // Reference methods MethodGetReferencesToSymbolInFile Method = "getReferencesToSymbolInFile" -@@ -166,11 +290,18 @@ const ( +@@ -166,11 +290,19 @@ const ( // Language service methods MethodGetCompletionsAtPosition Method = "getCompletionsAtPosition" ++ MethodResolveCompletionItem Method = "resolveCompletionItem" + MethodQuickinfo Method = "quickinfo" + MethodReferences Method = "references" + MethodDefinitionAndBoundSpan Method = "definitionAndBoundSpan" @@ -4722,7 +4723,7 @@ index 0917c385d..543e9c90b 100644 MethodGetSuggestionDiagnostics Method = "getSuggestionDiagnostics" MethodGetDeclarationDiagnostics Method = "getDeclarationDiagnostics" MethodGetProgramDiagnostics Method = "getProgramDiagnostics" -@@ -178,20 +309,30 @@ const ( +@@ -178,20 +310,30 @@ const ( MethodGetConfigFileParsingDiagnostics Method = "getConfigFileParsingDiagnostics" // Emitter methods @@ -4765,7 +4766,7 @@ index 0917c385d..543e9c90b 100644 // Well-known per-checker symbols MethodGetWellKnownSymbols Method = "getWellKnownSymbols" -@@ -208,6 +349,10 @@ type InitializeResponse struct { +@@ -208,6 +350,10 @@ type InitializeResponse struct { UseCaseSensitiveFileNames bool `json:"useCaseSensitiveFileNames"` // CurrentDirectory is the server's current working directory. CurrentDirectory string `json:"currentDirectory"` @@ -4776,7 +4777,7 @@ index 0917c385d..543e9c90b 100644 } // DocumentIdentifier identifies a document by either a file name (plain string) or a URI object. -@@ -217,6 +362,73 @@ type DocumentIdentifier struct { +@@ -217,6 +363,73 @@ type DocumentIdentifier struct { URI lsproto.DocumentUri `json:"uri,omitempty"` } @@ -4850,7 +4851,7 @@ index 0917c385d..543e9c90b 100644 var _ json.UnmarshalerFrom = (*DocumentIdentifier)(nil) func (d *DocumentIdentifier) UnmarshalJSONFrom(dec *json.Decoder) error { -@@ -308,7 +520,9 @@ type APIFileChanges struct { +@@ -308,7 +521,9 @@ type APIFileChanges struct { type UpdateSnapshotParams struct { // OpenProjects lists tsconfig.json files to open/load in the new snapshot. // Opens are ref-counted and persist across snapshots until closed. @@ -4861,7 +4862,7 @@ index 0917c385d..543e9c90b 100644 // CloseProjects lists tsconfig.json files to release in the new snapshot. // A project is only unloaded once every API client that opened it closes it. CloseProjects []DocumentIdentifier `json:"closeProjects,omitempty"` -@@ -324,6 +538,61 @@ type UpdateSnapshotParams struct { +@@ -324,6 +539,61 @@ type UpdateSnapshotParams struct { // CloseFiles lists files to release in the new snapshot. A file is only fully // closed once every API client that opened it closes it. CloseFiles []DocumentIdentifier `json:"closeFiles,omitempty"` @@ -4923,7 +4924,7 @@ index 0917c385d..543e9c90b 100644 } // ProjectFileChanges describes what source files changed within a single project. -@@ -332,6 +601,11 @@ type ProjectFileChanges struct { +@@ -332,6 +602,11 @@ type ProjectFileChanges struct { ChangedFiles []tspath.Path `json:"changedFiles,omitempty"` // DeletedFiles lists source file paths removed from the project's program. DeletedFiles []tspath.Path `json:"deletedFiles,omitempty"` @@ -4935,7 +4936,7 @@ index 0917c385d..543e9c90b 100644 } // SnapshotChanges describes what changed between the previous latest snapshot -@@ -358,36 +632,66 @@ type UpdateSnapshotResponse struct { +@@ -358,36 +633,66 @@ type UpdateSnapshotResponse struct { } var unmarshalers = map[Method]func([]byte) (any, error){ @@ -5024,7 +5025,7 @@ index 0917c385d..543e9c90b 100644 MethodGetFreshTypeOfType: unmarshallerFor[GetTypePropertyParams], MethodGetRegularTypeOfType: unmarshallerFor[GetTypePropertyParams], MethodGetTypesOfType: unmarshallerFor[GetTypePropertyParams], -@@ -410,70 +714,166 @@ var unmarshalers = map[Method]func([]byte) (any, error){ +@@ -410,70 +715,167 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetThisParameterOfSignature: unmarshallerFor[GetSignaturePropertyParams], MethodGetTargetOfSignature: unmarshallerFor[GetSignaturePropertyParams], @@ -5213,6 +5214,7 @@ index 0917c385d..543e9c90b 100644 + MethodGetReferencedSymbolsForNode: unmarshallerFor[GetReferencedSymbolsForNodeParams], + MethodGetSignatureUsages: unmarshallerFor[GetSignatureUsagesParams], + MethodGetCompletionsAtPosition: unmarshallerFor[GetCompletionsAtPositionParams], ++ MethodResolveCompletionItem: unmarshallerFor[ResolveCompletionItemParams], + MethodQuickinfo: unmarshallerFor[QuickinfoParams], + MethodReferences: unmarshallerFor[ReferencesParams], + MethodDefinitionAndBoundSpan: unmarshallerFor[DefinitionAndBoundSpanParams], @@ -5255,7 +5257,7 @@ index 0917c385d..543e9c90b 100644 } type ParseConfigFileParams struct { -@@ -529,6 +929,16 @@ type GetSymbolAtPositionParams struct { +@@ -529,6 +931,16 @@ type GetSymbolAtPositionParams struct { Position uint32 `json:"position"` } @@ -5272,7 +5274,7 @@ index 0917c385d..543e9c90b 100644 type GetSymbolsAtPositionsParams struct { Snapshot SnapshotID `json:"snapshot"` Project ProjectID `json:"project"` -@@ -558,6 +968,11 @@ type SymbolResponse struct { +@@ -558,6 +970,11 @@ type SymbolResponse struct { ValueDeclaration NodeHandle `json:"valueDeclaration,omitempty"` Parent SymbolID `json:"parent,omitzero"` ExportSymbol SymbolID `json:"exportSymbol,omitzero"` @@ -5284,7 +5286,7 @@ index 0917c385d..543e9c90b 100644 } func symbolHandles(symbols []*ast.Symbol) []SymbolID { -@@ -603,6 +1018,15 @@ type TypeResponse struct { +@@ -603,6 +1020,15 @@ type TypeResponse struct { ElementFlags []checker.ElementFlags `json:"elementFlags,omitempty"` FixedLength *int `json:"fixedLength,omitempty"` TupleReadonly *bool `json:"readonly,omitempty"` @@ -5300,7 +5302,7 @@ index 0917c385d..543e9c90b 100644 // IndexedAccessType data ObjectType TypeID `json:"objectType,omitzero"` -@@ -629,6 +1053,9 @@ type TypeResponse struct { +@@ -629,6 +1055,9 @@ type TypeResponse struct { // IntrinsicType data IntrinsicName string `json:"intrinsicName,omitempty"` @@ -5310,7 +5312,7 @@ index 0917c385d..543e9c90b 100644 // TypeAlias data AliasTypeArguments []TypeID `json:"aliasTypeArguments,omitempty"` AliasSymbol SymbolID `json:"aliasSymbol,omitzero"` -@@ -637,6 +1064,21 @@ type TypeResponse struct { +@@ -637,6 +1066,21 @@ type TypeResponse struct { Symbol SymbolID `json:"symbol,omitzero"` } @@ -5332,7 +5334,7 @@ index 0917c385d..543e9c90b 100644 func newTypeResponse(t *checker.Type, id TypeID) *TypeResponse { resp := &TypeResponse{ Id: id, -@@ -660,6 +1102,17 @@ func newTypeResponse(t *checker.Type, id TypeID) *TypeResponse { +@@ -660,6 +1104,17 @@ func newTypeResponse(t *checker.Type, id TypeID) *TypeResponse { if flags&checker.TypeFlagsLiteral != 0 { resp.Value = literalValueToJSON(lit.Value()) } @@ -5350,7 +5352,7 @@ index 0917c385d..543e9c90b 100644 if lit.FreshType() != nil { resp.FreshType = TypeHandle(lit.FreshType()) } -@@ -669,21 +1122,39 @@ func newTypeResponse(t *checker.Type, id TypeID) *TypeResponse { +@@ -669,21 +1124,39 @@ func newTypeResponse(t *checker.Type, id TypeID) *TypeResponse { case flags&checker.TypeFlagsObject != 0: resp.ObjectFlags = uint32(t.ObjectFlags()) objectFlags := t.ObjectFlags() @@ -5404,7 +5406,7 @@ index 0917c385d..543e9c90b 100644 } } if objectFlags&checker.ObjectFlagsClassOrInterface != 0 { -@@ -716,6 +1187,8 @@ func newTypeResponse(t *checker.Type, id TypeID) *TypeResponse { +@@ -716,6 +1189,8 @@ func newTypeResponse(t *checker.Type, id TypeID) *TypeResponse { resp.Target = TypeHandle(t.AsStringMappingType().Target()) case flags&checker.TypeFlagsTypeParameter != 0: resp.IsThisType = t.AsTypeParameter().IsThisType() @@ -5413,7 +5415,7 @@ index 0917c385d..543e9c90b 100644 case flags&checker.TypeFlagsIntrinsic != 0: resp.IntrinsicName = t.AsIntrinsicType().IntrinsicName() } -@@ -781,6 +1254,57 @@ type SourceFileMetadata struct { +@@ -781,6 +1256,57 @@ type SourceFileMetadata struct { ImpliedNodeFormat core.ResolutionMode `json:"impliedNodeFormat"` } @@ -5471,7 +5473,7 @@ index 0917c385d..543e9c90b 100644 type ResolveNameParams struct { Snapshot SnapshotID `json:"snapshot"` Project ProjectID `json:"project"` -@@ -806,6 +1330,13 @@ type GetSymbolPropertyParams struct { +@@ -806,6 +1332,13 @@ type GetSymbolPropertyParams struct { Symbol SymbolID `json:"objectId"` } @@ -5485,7 +5487,7 @@ index 0917c385d..543e9c90b 100644 // GetSignaturePropertyParams is used for all signature sub-property endpoints. type GetSignaturePropertyParams struct { Snapshot SnapshotID `json:"snapshot"` -@@ -815,9 +1346,10 @@ type GetSignaturePropertyParams struct { +@@ -815,9 +1348,10 @@ type GetSignaturePropertyParams struct { // GetContextualTypeParams returns the contextual type for a node. type GetContextualTypeParams struct { @@ -5499,7 +5501,7 @@ index 0917c385d..543e9c90b 100644 } // GetTypeOfSymbolAtLocationParams returns the narrowed type of a symbol at a specific location. -@@ -866,12 +1398,25 @@ type SignatureUsageResponse struct { +@@ -866,12 +1400,25 @@ type SignatureUsageResponse struct { // GetCompletionsAtPositionParams are the parameters for the getCompletionsAtPosition method. type GetCompletionsAtPositionParams struct { @@ -5531,7 +5533,7 @@ index 0917c385d..543e9c90b 100644 } // CompletionEntryLabelDetailsResponse holds additional label display text for a completion entry. -@@ -882,20 +1427,52 @@ type CompletionEntryLabelDetailsResponse struct { +@@ -882,20 +1429,57 @@ type CompletionEntryLabelDetailsResponse struct { // CompletionEntryResponse represents a single completion item. type CompletionEntryResponse struct { @@ -5573,9 +5575,14 @@ index 0917c385d..543e9c90b 100644 +// send it back to getCompletionEntryDetails or key their own metadata on it +// (volar's getAutoImportSuggestions filters on data presence). +type CompletionEntryDataResponse struct { -+ ExportName string `json:"exportName,omitempty"` -+ FileName string `json:"fileName,omitempty"` -+ ModuleSpecifier string `json:"moduleSpecifier,omitempty"` ++ ExportName string `json:"exportName,omitempty"` ++ FileName string `json:"fileName,omitempty"` ++ ModuleSpecifier string `json:"moduleSpecifier,omitempty"` ++ TnbCompletionData *TnbCompletionDataResponse `json:"tnbCompletionData,omitempty"` ++} ++ ++type TnbCompletionDataResponse struct { ++ AutoImport *lsproto.AutoImportFix `json:"autoImport,omitempty"` } // CompletionInfoResponse wraps a list of completion entries. @@ -5594,7 +5601,7 @@ index 0917c385d..543e9c90b 100644 } // GetIntrinsicTypeParams is used for intrinsic type getters (anyType, stringType, etc.). -@@ -904,6 +1481,29 @@ type GetIntrinsicTypeParams struct { +@@ -904,6 +1488,29 @@ type GetIntrinsicTypeParams struct { Project ProjectID `json:"project"` } @@ -5624,7 +5631,7 @@ index 0917c385d..543e9c90b 100644 // WellKnownSymbolsResponse carries the handle ids of the per-checker singleton // symbols (unknown, undefined, arguments) so the client can identify them by id // without a round-trip on every check. -@@ -927,6 +1527,13 @@ type GetNonNullableTypeParams struct { +@@ -927,6 +1534,13 @@ type GetNonNullableTypeParams struct { Type TypeID `json:"type"` } @@ -5638,7 +5645,7 @@ index 0917c385d..543e9c90b 100644 // GetTypeFromTypeNodeParams are the parameters for the getTypeFromTypeNode method. type GetTypeFromTypeNodeParams struct { Snapshot SnapshotID `json:"snapshot"` -@@ -956,6 +1563,74 @@ type IsArrayLikeTypeParams struct { +@@ -956,6 +1570,74 @@ type IsArrayLikeTypeParams struct { Type TypeID `json:"type"` } @@ -5713,7 +5720,7 @@ index 0917c385d..543e9c90b 100644 // IsTypeAssignableToParams checks assignability between two types. type IsTypeAssignableToParams struct { Snapshot SnapshotID `json:"snapshot"` -@@ -977,6 +1652,47 @@ type GetResolvedSignatureParams struct { +@@ -977,6 +1659,47 @@ type GetResolvedSignatureParams struct { Location NodeHandle `json:"location"` } @@ -5761,7 +5768,7 @@ index 0917c385d..543e9c90b 100644 type GetTypeAtLocationParams struct { Snapshot SnapshotID `json:"snapshot"` Project ProjectID `json:"project"` -@@ -1012,6 +1728,24 @@ type TypeToTypeNodeParams struct { +@@ -1012,6 +1735,24 @@ type TypeToTypeNodeParams struct { Flags int32 `json:"flags,omitempty"` } @@ -5786,7 +5793,7 @@ index 0917c385d..543e9c90b 100644 // SignatureToSignatureDeclarationParams are the parameters for the signatureToSignatureDeclaration method. type SignatureToSignatureDeclarationParams struct { Snapshot SnapshotID `json:"snapshot"` -@@ -1022,6 +1756,15 @@ type SignatureToSignatureDeclarationParams struct { +@@ -1022,6 +1763,15 @@ type SignatureToSignatureDeclarationParams struct { Flags int32 `json:"flags,omitempty"` } @@ -5802,7 +5809,7 @@ index 0917c385d..543e9c90b 100644 // PrintNodeParams are the parameters for the printNode method. type PrintNodeParams struct { Data string `json:"data"` // base64-encoded binary AST data -@@ -1060,6 +1803,88 @@ type CheckerNodeParams struct { +@@ -1060,6 +1810,88 @@ type CheckerNodeParams struct { Location NodeHandle `json:"location"` } @@ -5891,7 +5898,7 @@ index 0917c385d..543e9c90b 100644 // CheckerSymbolParams are parameters for checker methods that operate on a symbol. type CheckerSymbolParams struct { Snapshot SnapshotID `json:"snapshot"` -@@ -1074,6 +1899,216 @@ type JSDocTagInfo struct { +@@ -1074,6 +1906,216 @@ type JSDocTagInfo struct { Text string `json:"text,omitempty"` } @@ -6108,7 +6115,7 @@ index 0917c385d..543e9c90b 100644 // CheckerSignatureParams are parameters for checker methods that operate on a signature. type CheckerSignatureParams struct { Snapshot SnapshotID `json:"snapshot"` -@@ -1095,6 +2130,9 @@ type IndexInfoResponse struct { +@@ -1095,6 +2137,9 @@ type IndexInfoResponse struct { ValueType TypeResponse `json:"valueType"` IsReadonly bool `json:"isReadonly,omitempty"` Declaration NodeHandle `json:"declaration,omitempty"` @@ -6118,7 +6125,7 @@ index 0917c385d..543e9c90b 100644 } // SourceFileResponse contains the binary-encoded AST data for a source file. -@@ -1110,6 +2148,68 @@ type GetDiagnosticsParams struct { +@@ -1110,6 +2155,68 @@ type GetDiagnosticsParams struct { File *DocumentIdentifier `json:"file,omitempty"` } @@ -6188,7 +6195,7 @@ index 0917c385d..543e9c90b 100644 type GetProjectDiagnosticsParams struct { Snapshot SnapshotID `json:"snapshot"` diff --git a/internal/api/session.go b/internal/api/session.go -index 7e29c44b6..8f091bbac 100644 +index 7e29c44b6..5b794f286 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -3,6 +3,7 @@ package api @@ -6921,10 +6928,12 @@ index 7e29c44b6..8f091bbac 100644 case string(MethodGetSuggestionDiagnostics): return s.handleGetSuggestionDiagnostics(ctx, parsed.(*GetDiagnosticsParams)) case string(MethodGetDeclarationDiagnostics): -@@ -772,6 +1299,18 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. +@@ -772,6 +1299,20 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetSignatureUsages(ctx, parsed.(*GetSignatureUsagesParams)) case string(MethodGetCompletionsAtPosition): return s.handleGetCompletionsAtPosition(ctx, parsed.(*GetCompletionsAtPositionParams)) ++ case string(MethodResolveCompletionItem): ++ return s.handleResolveCompletionItem(ctx, parsed.(*ResolveCompletionItemParams)) + case string(MethodQuickinfo): + return s.handleQuickinfo(ctx, parsed.(*QuickinfoParams)) + case string(MethodReferences): @@ -6940,7 +6949,7 @@ index 7e29c44b6..8f091bbac 100644 default: return nil, fmt.Errorf("unknown method: %s", method) } -@@ -816,6 +1355,7 @@ func (s *Session) handleInitialize(ctx context.Context) (*InitializeResponse, er +@@ -816,6 +1357,7 @@ func (s *Session) handleInitialize(ctx context.Context) (*InitializeResponse, er return &InitializeResponse{ UseCaseSensitiveFileNames: s.projectSession.FS().UseCaseSensitiveFileNames(), CurrentDirectory: s.projectSession.GetCurrentDirectory(), @@ -6948,7 +6957,7 @@ index 7e29c44b6..8f091bbac 100644 }, nil } -@@ -832,6 +1372,17 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh +@@ -832,6 +1374,17 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh s.updateMu.Lock() defer s.updateMu.Unlock() @@ -6966,7 +6975,7 @@ index 7e29c44b6..8f091bbac 100644 fileChanges := s.toFileChangeSummary(params.FileChanges) apiRequest := &project.APISnapshotRequest{} -@@ -840,6 +1391,15 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh +@@ -840,6 +1393,15 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh var openedProjects []tspath.Path for _, p := range params.OpenProjects { configFileName := p.ToAbsoluteFileName(s.projectSession.GetCurrentDirectory()) @@ -6982,7 +6991,7 @@ index 7e29c44b6..8f091bbac 100644 configPath := s.toPath(configFileName) if s.openProjects.Has(configPath) { continue -@@ -850,6 +1410,18 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh +@@ -850,6 +1412,18 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh apiRequest.OpenProjects.Add(configFileName) openedProjects = append(openedProjects, configPath) } @@ -7001,7 +7010,7 @@ index 7e29c44b6..8f091bbac 100644 // Close projects: only release a ref we currently hold. var closedProjects []tspath.Path -@@ -883,6 +1455,7 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh +@@ -883,6 +1457,7 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh // Close files: only release a ref we currently hold. var closedFiles []tspath.Path @@ -7009,7 +7018,7 @@ index 7e29c44b6..8f091bbac 100644 for _, f := range params.CloseFiles { path := s.toPath(f.ToURI(s.projectSession.GetCurrentDirectory()).FileName()) if !s.openFiles.Has(path) { -@@ -893,6 +1466,92 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh +@@ -893,6 +1468,92 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh } apiRequest.CloseFiles.Add(path) closedFiles = append(closedFiles, path) @@ -7102,7 +7111,7 @@ index 7e29c44b6..8f091bbac 100644 } // Even when nothing is opened or closed, APIUpdate ensures all projects and -@@ -938,12 +1597,22 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh +@@ -938,12 +1599,22 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh refCount: 1, symbolRegistry: make(map[SymbolID]*ast.Symbol), symbolCanonicalProjects: make(map[SymbolID]ProjectID), @@ -7125,7 +7134,7 @@ index 7e29c44b6..8f091bbac 100644 s.snapshotsMu.Unlock() // Build projects list -@@ -962,6 +1631,57 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh +@@ -962,6 +1633,57 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh changes = computeSnapshotChanges(prevSD.snapshot, snapshot) } @@ -7183,7 +7192,7 @@ index 7e29c44b6..8f091bbac 100644 return &UpdateSnapshotResponse{ Snapshot: handle, Projects: projectResponses, -@@ -969,6 +1689,87 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh +@@ -969,6 +1691,87 @@ func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapsh }, nil } @@ -7271,7 +7280,7 @@ index 7e29c44b6..8f091bbac 100644 // handleRelease decrements the ref count for a snapshot. // The snapshot and its registries are only cleaned up when the ref count reaches zero. func (s *Session) handleRelease(ctx context.Context, params *ReleaseParams) (any, error) { -@@ -985,6 +1786,11 @@ func (s *Session) handleRelease(ctx context.Context, params *ReleaseParams) (any +@@ -985,6 +1788,11 @@ func (s *Session) handleRelease(ctx context.Context, params *ReleaseParams) (any sd.refCount-- if sd.refCount <= 0 { delete(s.snapshots, params.Snapshot) @@ -7283,7 +7292,7 @@ index 7e29c44b6..8f091bbac 100644 // Release the API session's ref on the project snapshot. sd.snapshot.Deref(s.projectSession) } -@@ -1061,8 +1867,8 @@ func (s *Session) handleGetSourceFile(ctx context.Context, params *GetSourceFile +@@ -1061,8 +1869,8 @@ func (s *Session) handleGetSourceFile(ctx context.Context, params *GetSourceFile return nil, nil } @@ -7294,7 +7303,7 @@ index 7e29c44b6..8f091bbac 100644 if err != nil { return nil, fmt.Errorf("failed to encode source file: %w", err) } -@@ -1076,6 +1882,42 @@ func (s *Session) handleGetSourceFile(ctx context.Context, params *GetSourceFile +@@ -1076,6 +1884,42 @@ func (s *Session) handleGetSourceFile(ctx context.Context, params *GetSourceFile }, nil } @@ -7337,7 +7346,7 @@ index 7e29c44b6..8f091bbac 100644 // handleGetSourceFileNames returns file names of all source files in a project. func (s *Session) handleGetSourceFileNames(ctx context.Context, params *GetSourceFileNamesParams) ([]string, error) { sd, err := s.getSnapshotData(params.Snapshot) -@@ -1124,311 +1966,373 @@ func (s *Session) handleGetSourceFileMetadata(ctx context.Context, params *GetSo +@@ -1124,311 +1968,373 @@ func (s *Session) handleGetSourceFileMetadata(ctx context.Context, params *GetSo }, nil } @@ -7895,7 +7904,7 @@ index 7e29c44b6..8f091bbac 100644 setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) if err != nil { return nil, err -@@ -1440,22 +2344,23 @@ func (s *Session) handleGetTypeAtPosition(ctx context.Context, params *GetTypeAt +@@ -1440,22 +2346,23 @@ func (s *Session) handleGetTypeAtPosition(ctx context.Context, params *GetTypeAt return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File) } @@ -7924,7 +7933,7 @@ index 7e29c44b6..8f091bbac 100644 setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) if err != nil { return nil, err -@@ -1468,388 +2373,3094 @@ func (s *Session) handleGetTypesAtPositions(ctx context.Context, params *GetType +@@ -1468,388 +2375,3094 @@ func (s *Session) handleGetTypesAtPositions(ctx context.Context, params *GetType } positionMap := sourceFile.GetPositionMap() @@ -11225,7 +11234,7 @@ index 7e29c44b6..8f091bbac 100644 setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) if err != nil { return nil, err -@@ -1860,123 +5471,195 @@ func (s *Session) handleGetNonNullableType(ctx context.Context, params *GetNonNu +@@ -1860,123 +5473,195 @@ func (s *Session) handleGetNonNullableType(ctx context.Context, params *GetNonNu if err != nil { return nil, err } @@ -11479,7 +11488,7 @@ index 7e29c44b6..8f091bbac 100644 setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) if err != nil { return nil, err -@@ -1990,49 +5673,38 @@ func (s *Session) handleGetShorthandAssignmentValueSymbol(ctx context.Context, p +@@ -1990,49 +5675,38 @@ func (s *Session) handleGetShorthandAssignmentValueSymbol(ctx context.Context, p if node == nil { return nil, nil } @@ -11541,7 +11550,7 @@ index 7e29c44b6..8f091bbac 100644 setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) if err != nil { return nil, err -@@ -2044,72 +5716,47 @@ func (s *Session) handleTypeToTypeNode(ctx context.Context, params *TypeToTypeNo +@@ -2044,72 +5718,47 @@ func (s *Session) handleTypeToTypeNode(ctx context.Context, params *TypeToTypeNo return nil, err } @@ -11631,7 +11640,7 @@ index 7e29c44b6..8f091bbac 100644 setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) if err != nil { return nil, err -@@ -2121,302 +5768,266 @@ func (s *Session) handleTypeToString(ctx context.Context, params *TypeToTypeNode +@@ -2121,302 +5770,266 @@ func (s *Session) handleTypeToString(ctx context.Context, params *TypeToTypeNode return nil, err } @@ -12066,7 +12075,7 @@ index 7e29c44b6..8f091bbac 100644 } // handleGetConstraintOfTypeParameter returns the constraint of a type parameter. -@@ -2430,6 +6041,7 @@ func (s *Session) handleGetConstraintOfTypeParameter(ctx context.Context, params +@@ -2430,6 +6043,7 @@ func (s *Session) handleGetConstraintOfTypeParameter(ctx context.Context, params t, err := setup.resolveTypeHandle(params.Type) if err != nil { return nil, err @@ -12074,7 +12083,7 @@ index 7e29c44b6..8f091bbac 100644 } constraint := setup.checker.GetConstraintOfTypeParameter(t) -@@ -2628,6 +6240,73 @@ func (s *Session) handleGetExportsOfModule(ctx context.Context, params *CheckerS +@@ -2628,6 +6242,73 @@ func (s *Session) handleGetExportsOfModule(ctx context.Context, params *CheckerS return results, nil } @@ -12148,7 +12157,7 @@ index 7e29c44b6..8f091bbac 100644 // handleGetMemberInModuleExports returns an export by name from a module symbol. func (s *Session) handleGetMemberInModuleExports(ctx context.Context, params *GetMemberInModuleExportsParams) (*SymbolResponse, error) { setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) -@@ -2830,7 +6509,9 @@ func computeSnapshotChanges(prev *project.Snapshot, next *project.Snapshot) *Sna +@@ -2830,7 +6511,9 @@ func computeSnapshotChanges(prev *project.Snapshot, next *project.Snapshot) *Sna var projectChanges ProjectFileChanges core.DiffMaps( oldFiles, newFiles, @@ -12159,7 +12168,7 @@ index 7e29c44b6..8f091bbac 100644 func(path tspath.Path, _ *ast.SourceFile) { projectChanges.DeletedFiles = append(projectChanges.DeletedFiles, path) }, -@@ -2838,7 +6519,7 @@ func computeSnapshotChanges(prev *project.Snapshot, next *project.Snapshot) *Sna +@@ -2838,7 +6521,7 @@ func computeSnapshotChanges(prev *project.Snapshot, next *project.Snapshot) *Sna projectChanges.ChangedFiles = append(projectChanges.ChangedFiles, path) }, ) @@ -12168,7 +12177,7 @@ index 7e29c44b6..8f091bbac 100644 if changes.ChangedProjects == nil { changes.ChangedProjects = make(map[ProjectID]*ProjectFileChanges) } -@@ -2853,11 +6534,23 @@ func computeSnapshotChanges(prev *project.Snapshot, next *project.Snapshot) *Sna +@@ -2853,11 +6536,23 @@ func computeSnapshotChanges(prev *project.Snapshot, next *project.Snapshot) *Sna // Close closes the session and releases all active snapshots, // regardless of their ref counts. func (s *Session) Close() { @@ -12192,7 +12201,7 @@ index 7e29c44b6..8f091bbac 100644 sd.snapshot.Deref(s.projectSession) delete(s.snapshots, handle) } -@@ -2979,7 +6672,6 @@ func (s *Session) handleGetBindDiagnostics(ctx context.Context, params *GetDiagn +@@ -2979,7 +6674,6 @@ func (s *Session) handleGetBindDiagnostics(ctx context.Context, params *GetDiagn // handleGetSemanticDiagnostics returns semantic diagnostics for a file or all files. func (s *Session) handleGetSemanticDiagnostics(ctx context.Context, params *GetDiagnosticsParams) ([]*DiagnosticResponse, error) { @@ -12200,7 +12209,7 @@ index 7e29c44b6..8f091bbac 100644 sd, err := s.getSnapshotData(params.Snapshot) if err != nil { return nil, err -@@ -2995,10 +6687,162 @@ func (s *Session) handleGetSemanticDiagnostics(ctx context.Context, params *GetD +@@ -2995,10 +6689,162 @@ func (s *Session) handleGetSemanticDiagnostics(ctx context.Context, params *GetD return nil, err } @@ -12363,7 +12372,7 @@ index 7e29c44b6..8f091bbac 100644 // handleGetSuggestionDiagnostics returns suggestion diagnostics for a file or all files. func (s *Session) handleGetSuggestionDiagnostics(ctx context.Context, params *GetDiagnosticsParams) ([]*DiagnosticResponse, error) { ctx = core.WithCheckerLifetime(ctx, core.CheckerLifetimeDiagnostics) -@@ -3077,7 +6921,6 @@ func (s *Session) handleGetProgramDiagnostics(ctx context.Context, params *GetPr +@@ -3077,7 +6923,6 @@ func (s *Session) handleGetProgramDiagnostics(ctx context.Context, params *GetPr // handleGetGlobalDiagnostics returns global (non-file-specific) semantic diagnostics. func (s *Session) handleGetGlobalDiagnostics(ctx context.Context, params *GetProjectDiagnosticsParams) ([]*DiagnosticResponse, error) { @@ -12371,7 +12380,7 @@ index 7e29c44b6..8f091bbac 100644 sd, err := s.getSnapshotData(params.Snapshot) if err != nil { return nil, err -@@ -3094,11 +6937,27 @@ func (s *Session) handleGetGlobalDiagnostics(ctx context.Context, params *GetPro +@@ -3094,11 +6939,27 @@ func (s *Session) handleGetGlobalDiagnostics(ctx context.Context, params *GetPro } // Global diagnostics are accumulated lazily by the project's checker pool as @@ -12404,7 +12413,7 @@ index 7e29c44b6..8f091bbac 100644 diags := core.Filter(proj.GetProjectDiagnostics(ctx), func(d *ast.Diagnostic) bool { return d.File() == nil -@@ -3143,7 +7002,7 @@ func (s *Session) handleGetReferencesToSymbolInFile(ctx context.Context, params +@@ -3143,7 +7004,7 @@ func (s *Session) handleGetReferencesToSymbolInFile(ctx context.Context, params nodes := setup.checker.GetReferencesToSymbolInFile(sourceFile, symbol) result := make([]NodeHandle, len(nodes)) for i, node := range nodes { @@ -12413,7 +12422,7 @@ index 7e29c44b6..8f091bbac 100644 } return result, nil } -@@ -3179,10 +7038,10 @@ func (s *Session) handleGetSignatureUsages(ctx context.Context, params *GetSigna +@@ -3179,10 +7040,10 @@ func (s *Session) handleGetSignatureUsages(ctx context.Context, params *GetSigna result := make([]SignatureUsageResponse, 0, len(usages)) for _, u := range usages { entry := SignatureUsageResponse{ @@ -12426,7 +12435,7 @@ index 7e29c44b6..8f091bbac 100644 } result = append(result, entry) } -@@ -3206,29 +7065,75 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge +@@ -3206,29 +7067,75 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge if sourceFile == nil { return nil, nil } @@ -12514,7 +12523,7 @@ index 7e29c44b6..8f091bbac 100644 entry.LabelDetails = &CompletionEntryLabelDetailsResponse{ Detail: item.LabelDetails.Detail, Description: item.LabelDetails.Description, -@@ -3237,12 +7142,62 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge +@@ -3237,12 +7144,63 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge if item.Symbol != nil { entry.Symbol = sd.newSymbolResponse(item.Symbol, params.Project) } @@ -12542,6 +12551,7 @@ index 7e29c44b6..8f091bbac 100644 + } + if item.Data.AutoImport != nil { + entry.Data.ModuleSpecifier = item.Data.AutoImport.ModuleSpecifier ++ entry.Data.TnbCompletionData = &TnbCompletionDataResponse{AutoImport: item.Data.AutoImport} + } + } + } @@ -12581,7 +12591,7 @@ index 7e29c44b6..8f091bbac 100644 } // handleGetReferencedSymbolsForNode returns node handles for all references found at a node. -@@ -3284,11 +7239,11 @@ func (s *Session) handleGetReferencedSymbolsForNode(ctx context.Context, params +@@ -3284,11 +7242,11 @@ func (s *Session) handleGetReferencedSymbolsForNode(ctx context.Context, params var refs []NodeHandle for _, ref := range entry.References() { if ref.IsNodeEntry() { @@ -12595,7 +12605,7 @@ index 7e29c44b6..8f091bbac 100644 References: refs, } if sym := entry.DefinitionSymbol(); sym != nil { -@@ -3298,3 +7253,24 @@ func (s *Session) handleGetReferencedSymbolsForNode(ctx context.Context, params +@@ -3298,3 +7256,24 @@ func (s *Session) handleGetReferencedSymbolsForNode(ctx context.Context, params } return result, nil } diff --git a/patches/typescript-go/overlay/internal/api/arena.go b/patches/typescript-go/overlay/internal/api/arena.go index 19e849e..529f0c0 100644 --- a/patches/typescript-go/overlay/internal/api/arena.go +++ b/patches/typescript-go/overlay/internal/api/arena.go @@ -19,6 +19,7 @@ import ( "unsafe" "github.com/microsoft/typescript-go/internal/checker" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" ) const ( @@ -818,10 +819,11 @@ func (a *arena) encodeExpandedParameters(v [][]SymbolID) { const ( completionInfoRecordSize = 32 - completionEntryRecordSize = 88 + completionEntryRecordSize = 96 + autoImportFixRecordSize = 36 ) -// encodeCompletionEntry writes a CompletionEntryResponse record (88 bytes fixed). +// encodeCompletionEntry writes a CompletionEntryResponse record (96 bytes fixed). func (a *arena) encodeCompletionEntry(off int, r *CompletionEntryResponse) { a.u32(off+0, a.str(r.Name)) a.u32(off+4, a.str(r.ElementKind)) @@ -872,11 +874,15 @@ func (a *arena) encodeCompletionEntry(off int, r *CompletionEntryResponse) { flags |= 16 } var dataExportName, dataFileName, dataModuleSpecifier string + var autoImportFix *lsproto.AutoImportFix if r.Data != nil { flags |= 32 dataExportName = r.Data.ExportName dataFileName = r.Data.FileName dataModuleSpecifier = r.Data.ModuleSpecifier + if r.Data.TnbCompletionData != nil { + autoImportFix = r.Data.TnbCompletionData.AutoImport + } } if r.IsPackageJsonImport != nil && *r.IsPackageJsonImport { flags |= 64 @@ -889,6 +895,28 @@ func (a *arena) encodeCompletionEntry(off int, r *CompletionEntryResponse) { a.u32(off+72, a.str(dataExportName)) a.u32(off+76, a.str(dataFileName)) a.displayParts(off+80, r.SourceDisplay) + if autoImportFix != nil { + flags |= 128 + a.b(off+64, flags) + fixOff := a.pack(autoImportFixRecordSize) + a.u32(off+88, uint32(fixOff)) + a.u32(fixOff+0, uint32(autoImportFix.Kind)) + a.u32(fixOff+4, a.str(autoImportFix.Name)) + a.u32(fixOff+8, uint32(autoImportFix.ImportKind)) + a.u32(fixOff+12, uint32(autoImportFix.AddAsTypeOnly)) + a.u32(fixOff+16, uint32(autoImportFix.ImportIndex)) + a.u32(fixOff+20, a.str(autoImportFix.NamespacePrefix)) + var fixFlags uint32 + if autoImportFix.UseRequire { + fixFlags |= 1 + } + if autoImportFix.UsagePosition != nil { + fixFlags |= 2 + a.u32(fixOff+24, autoImportFix.UsagePosition.Line) + a.u32(fixOff+28, autoImportFix.UsagePosition.Character) + } + a.u32(fixOff+32, fixFlags) + } // offset map (u32 unless noted): // 0 name / 4 elementKind / 8 kindModifiers / 12 sortText / 16 insertText // 20 filterText / 24 source / 28 detail / 32 labelDetail.detail @@ -896,6 +924,10 @@ func (a *arena) encodeCompletionEntry(off int, r *CompletionEntryResponse) { // 48 commitCharacters (ptr,count) / 56 symbolPtr / 60 kindU32 / 64 flags u8 // 65-67 pad / 68 dataModuleSpecifier / 72 dataExportName / 76 dataFileName // 80 sourceDisplay (ptr,count of {text,kind} records) + // 88 autoImportFix pointer / 92 pad. The packed fix record is: + // 0 kind / 4 name / 8 importKind / 12 addAsTypeOnly / 16 importIndex + // 20 namespacePrefix / 24 usageLine / 28 usageChar / 32 flags + // (useRequire, hasUsagePosition). } // encodeCompletionsResponse writes a CompletionInfoResponse record (32 bytes). diff --git a/patches/typescript-go/overlay/internal/api/completionresolve.go b/patches/typescript-go/overlay/internal/api/completionresolve.go new file mode 100644 index 0000000..b22491d --- /dev/null +++ b/patches/typescript-go/overlay/internal/api/completionresolve.go @@ -0,0 +1,62 @@ +package api + +import ( + "context" + "fmt" + + "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/ls/lsutil" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" +) + +type ResolveCompletionItemParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + Data lsproto.CompletionItemData `json:"data"` + Preferences lsutil.UserPreferences `json:"preferences"` +} + +type ResolvedCompletionItemResponse struct { + Detail string `json:"detail"` + TextChanges []TextChangeResponse `json:"textChanges"` +} + +type TextChangeResponse struct { + Start uint32 `json:"start"` + Length uint32 `json:"length"` + NewText string `json:"newText"` +} + +func (s *Session) handleResolveCompletionItem(ctx context.Context, params *ResolveCompletionItemParams) (*ResolvedCompletionItemResponse, error) { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return nil, err + } + program, err := sd.getProgram(params.Project) + if err != nil { + return nil, err + } + projectName := parseProjectHandle(params.Project) + proj := sd.snapshot.ProjectCollection.GetProjectByPath(projectName) + if proj == nil { + return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectName) + } + langSvc := ls.NewLanguageService(proj.ID(), program, &completionsPrefsHost{Host: sd.snapshot, prefs: params.Preferences}, "") + item, err := langSvc.ResolveCompletionItem(ctx, &lsproto.CompletionItem{Label: params.Data.Name}, ¶ms.Data) + if err != nil { + return nil, err + } + response := &ResolvedCompletionItemResponse{TextChanges: []TextChangeResponse{}} + if item.Detail != nil { + response.Detail = *item.Detail + } + if item.AdditionalTextEdits != nil { + sourceFile := program.GetSourceFile(params.Data.FileName) + response.TextChanges = make([]TextChangeResponse, len(*item.AdditionalTextEdits)) + for i, edit := range *item.AdditionalTextEdits { + start, length := lspRangeToSpan(sourceFile, edit.Range) + response.TextChanges[i] = TextChangeResponse{Start: start, Length: length, NewText: edit.NewText} + } + } + return response, nil +} diff --git a/patches/typescript/0001-tsgo-hooks.patch b/patches/typescript/0001-tsgo-hooks.patch index 4a62dec..c415709 100644 --- a/patches/typescript/0001-tsgo-hooks.patch +++ b/patches/typescript/0001-tsgo-hooks.patch @@ -609,7 +609,7 @@ index 6034c9dfc3..0c9a6178db 100644 let computedWithoutCacheCount = 0; const fixes = flatMap(exportInfo, (exportInfo, i) => { diff --git a/src/services/completions.ts b/src/services/completions.ts -index 28d29136da..48fa363863 100644 +index 28d29136da..d5ba59f622 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -175,6 +175,7 @@ import { @@ -802,7 +802,27 @@ index 28d29136da..48fa363863 100644 // expressions are value space (which includes the value namespaces) return !!(allFlags & SymbolFlags.Value); } -@@ -3588,6 +3638,11 @@ function getCompletionData( +@@ -3147,6 +3197,19 @@ function getCompletionEntryCodeActionsAndSourceDisplay( + return { codeActions: undefined, sourceDisplay: undefined }; + } + ++ if (data?.tnbCompletionData && (program as any).isTsgoBackedProgram === true) { ++ const result = (program as any).resolveCompletionItemTsgo( ++ sourceFile.fileName, ++ position, ++ name, ++ source, ++ data.tnbCompletionData, ++ formatContext.options, ++ preferences, ++ ); ++ return { sourceDisplay: [textPart(data.moduleSpecifier!)], codeActions: [result] }; ++ } ++ + const checker = origin.isFromPackageJson ? host.getPackageJsonAutoImportProvider!()!.getTypeChecker() : program.getTypeChecker(); + const { moduleSymbol } = origin; + const targetSymbol = checker.getMergedSymbol(skipAlias(symbol.exportSymbol || symbol, checker)); +@@ -3588,6 +3651,11 @@ function getCompletionData( let importSpecifierResolver: codefix.ImportSpecifierResolver | undefined; const symbolToOriginInfoMap: SymbolOriginInfoMap = []; const symbolToSortTextMap: SymbolSortTextMap = []; @@ -814,7 +834,7 @@ index 28d29136da..48fa363863 100644 const seenPropertySymbols = new Set(); const isTypeOnlyLocation = isTypeOnlyCompletion(); const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => { -@@ -4028,13 +4083,29 @@ function getCompletionData( +@@ -4028,13 +4096,29 @@ function getCompletionData( symbols = concatenate(symbols, typeChecker.getSymbolsInScope(scopeNode, symbolMeanings)); Debug.assertEachIsDefined(symbols, "getSymbolsInScope() should all be defined"); @@ -845,7 +865,7 @@ index 28d29136da..48fa363863 100644 } if (typeOnlyAliasNeedsPromotion && !(symbol.flags & SymbolFlags.Value)) { const typeOnlyAliasDeclaration = symbol.declarations && find(symbol.declarations, isTypeOnlyImportDeclaration); -@@ -4161,11 +4232,74 @@ function getCompletionData( +@@ -4161,11 +4245,74 @@ function getCompletionData( const exportInfo = getExportInfoMap(sourceFile, host, program, preferences, cancellationToken); const packageJsonAutoImportProvider = host.getPackageJsonAutoImportProvider?.(); const packageJsonFilter = detailsEntryId ? undefined : createPackageJsonImportFilter(sourceFile, preferences, host); @@ -921,7 +941,7 @@ index 28d29136da..48fa363863 100644 position, preferences, !!importStatementCompletion, -@@ -4232,6 +4366,7 @@ function getCompletionData( +@@ -4232,6 +4379,7 @@ function getCompletionData( isDefaultExport, moduleSymbol: exportInfo.moduleSymbol, isFromPackageJson: exportInfo.isFromPackageJson, @@ -929,7 +949,7 @@ index 28d29136da..48fa363863 100644 }); }, ); -@@ -4240,20 +4375,7 @@ function getCompletionData( +@@ -4240,20 +4388,7 @@ function getCompletionData( flags |= context.resolvedAny() ? CompletionInfoFlags.ResolvedModuleSpecifiers : 0; flags |= context.resolvedBeyondLimit() ? CompletionInfoFlags.ResolvedModuleSpecifiersBeyondLimit : 0; }, @@ -951,7 +971,7 @@ index 28d29136da..48fa363863 100644 } function pushAutoImportSymbol(symbol: Symbol, origin: SymbolOriginInfoResolvedExport | SymbolOriginInfoExport) { -@@ -4262,6 +4384,12 @@ function getCompletionData( +@@ -4262,6 +4397,12 @@ function getCompletionData( // If an auto-importable symbol is available as a global, don't add the auto import return; } @@ -964,7 +984,7 @@ index 28d29136da..48fa363863 100644 symbolToOriginInfoMap[symbols.length] = origin; symbolToSortTextMap[symbolId] = importStatementCompletion ? SortText.LocationPriority : SortText.AutoImportSuggestions; symbols.push(symbol); -@@ -5382,14 +5510,25 @@ function getRelevantTokens(position: number, sourceFile: SourceFile): { contextT +@@ -5382,14 +5523,25 @@ function getRelevantTokens(position: number, sourceFile: SourceFile): { contextT function getAutoImportSymbolFromCompletionEntryData(name: string, data: CompletionEntryData, program: Program, host: LanguageServiceHost): { symbol: Symbol; origin: SymbolOriginInfoExport | SymbolOriginInfoResolvedExport; } | undefined { const containingProgram = data.isPackageJsonImport ? host.getPackageJsonAutoImportProvider!()! : program; const checker = containingProgram.getTypeChecker(); @@ -997,7 +1017,7 @@ index 28d29136da..48fa363863 100644 if (!symbol) return undefined; const isDefaultExport = data.exportName === InternalSymbolName.Default; symbol = isDefaultExport && getLocalSymbolForExportDefault(symbol) || symbol; -@@ -6089,6 +6228,23 @@ function isDeprecated(symbol: Symbol, checker: TypeChecker) { +@@ -6089,6 +6241,23 @@ function isDeprecated(symbol: Symbol, checker: TypeChecker) { return !!length(declarations) && every(declarations, isDeprecatedDeclaration); } @@ -2485,6 +2505,31 @@ index 9885093ed6..b0a6e5b278 100644 synchronizeHostData(); return Rename.getRenameInfo(program, getValidSourceFile(fileName), position, preferences || {}); } +diff --git a/src/services/types.ts b/src/services/types.ts +index 5329fe901b..e3f5dde954 100644 +--- a/src/services/types.ts ++++ b/src/services/types.ts +@@ -1465,6 +1465,20 @@ export interface CompletionEntryDataAutoImport { + ambientModuleName?: string; + /** True if the export was found in the package.json AutoImportProvider */ + isPackageJsonImport?: true; ++ /** @internal */ ++ tnbCompletionData?: { ++ autoImport: { ++ kind?: number; ++ name?: string; ++ importKind: number; ++ useRequire?: boolean; ++ addAsTypeOnly: number; ++ moduleSpecifier?: string; ++ importIndex: number; ++ usagePosition?: { line: number; character: number; }; ++ namespacePrefix?: string; ++ }; ++ }; + } + + export interface CompletionEntryDataUnresolved extends CompletionEntryDataAutoImport { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index bec7d4b266..927d0cff0e 100644 --- a/src/services/utilities.ts diff --git a/patches/typescript/overlay/src/compiler/tsgoChecker.ts b/patches/typescript/overlay/src/compiler/tsgoChecker.ts index 6b6e9d1..83980fa 100644 --- a/patches/typescript/overlay/src/compiler/tsgoChecker.ts +++ b/patches/typescript/overlay/src/compiler/tsgoChecker.ts @@ -1693,6 +1693,30 @@ class ArenaClient { if (fileName !== undefined) data.fileName = fileName; const moduleSpecifier = str(v.getUint32(off + 68, true)); if (moduleSpecifier !== undefined) data.moduleSpecifier = moduleSpecifier; + if (flags & 128) { + const fixOff = v.getUint32(off + 88, true); + const fixFlags = v.getUint32(fixOff + 32, true); + const autoImport: any = { + importKind: v.getUint32(fixOff + 8, true), + addAsTypeOnly: v.getUint32(fixOff + 12, true), + importIndex: v.getUint32(fixOff + 16, true), + }; + const kind = v.getUint32(fixOff + 0, true); + if (kind) autoImport.kind = kind; + const fixName = str(v.getUint32(fixOff + 4, true)); + if (fixName) autoImport.name = fixName; + if (fixFlags & 1) autoImport.useRequire = true; + if (moduleSpecifier) autoImport.moduleSpecifier = moduleSpecifier; + const namespacePrefix = str(v.getUint32(fixOff + 20, true)); + if (namespacePrefix) autoImport.namespacePrefix = namespacePrefix; + if (fixFlags & 2) { + autoImport.usagePosition = { + line: v.getUint32(fixOff + 24, true), + character: v.getUint32(fixOff + 28, true), + }; + } + data.tnbCompletionData = { autoImport }; + } d.data = data; } if (flags & 64) d.isPackageJsonImport = true; @@ -1712,7 +1736,7 @@ class ArenaClient { const entries = new Array(count); for (let i = 0; i < count; i++) { entries[i] = this.readCompletionEntry(p); - p += 88; + p += 96; } d.entries = entries; if (f2 & 1) d.flags = v.getUint32(off + 4, true); @@ -8208,6 +8232,26 @@ export function createTsgoProgram( const symbol = checker.getExportForCompletionEntryData(exportName, moduleSymbol); return symbol ? { moduleSymbol, symbol } : undefined; }, + resolveCompletionItemTsgo: (fileName: string, position: number, name: string, source: string | undefined, data: any, formatOptions: any, preferences: any) => { + const proj = liveProject(); + const result = proj.checker.client.apiRequest("resolveCompletionItem", { + snapshot: proj.checker.snapshotId, + project: proj.checker.project.id, + data: { fileName: tsgoFileArg(fileName), position, name, source, ...data }, + preferences: { unstable: { ...formatOptions, ...preferences } }, + }); + if (!result) throw new Error(`resolveCompletionItem: file not found: ${fileName}`); + return { + description: result.detail, + changes: [{ + fileName, + textChanges: result.textChanges.map((change: any) => ({ + span: { start: change.start, length: change.length }, + newText: change.newText, + })), + }], + }; + }, getConfigFileParsingDiagnostics: () => configDiags, getOptionsDiagnostics: () => [], getSemanticDiagnostics: (sourceFile?: any) => { diff --git a/tools/triage-arena-parity.mjs b/tools/triage-arena-parity.mjs index 1dd864b..21de3d9 100644 --- a/tools/triage-arena-parity.mjs +++ b/tools/triage-arena-parity.mjs @@ -465,6 +465,25 @@ function arenaCall(method, params) { if (fileName !== undefined) data.fileName = fileName; const moduleSpecifier = str(view.getUint32(off + 68, true)); if (moduleSpecifier !== undefined) data.moduleSpecifier = moduleSpecifier; + if (flags & 128) { + const fixOff = view.getUint32(off + 88, true); + const fixFlags = view.getUint32(fixOff + 32, true); + const autoImport = { + importKind: view.getUint32(fixOff + 8, true), + addAsTypeOnly: view.getUint32(fixOff + 12, true), + importIndex: view.getUint32(fixOff + 16, true), + }; + const fixKind = view.getUint32(fixOff, true); + if (fixKind) autoImport.kind = fixKind; + const fixName = str(view.getUint32(fixOff + 4, true)); + if (fixName) autoImport.name = fixName; + if (fixFlags & 1) autoImport.useRequire = true; + if (moduleSpecifier) autoImport.moduleSpecifier = moduleSpecifier; + const namespacePrefix = str(view.getUint32(fixOff + 20, true)); + if (namespacePrefix) autoImport.namespacePrefix = namespacePrefix; + if (fixFlags & 2) autoImport.usagePosition = { line: view.getUint32(fixOff + 24, true), character: view.getUint32(fixOff + 28, true) }; + data.tnbCompletionData = { autoImport }; + } d.data = data; } if (flags & 64) d.isPackageJsonImport = true; @@ -480,7 +499,7 @@ function arenaCall(method, params) { const count = view.getUint32(off + 28, true); let p = view.getUint32(off + 24, true); const entries = new Array(count); - for (let i = 0; i < count; i++) { entries[i] = readCompletionEntry(p); p += 88; } + for (let i = 0; i < count; i++) { entries[i] = readCompletionEntry(p); p += 96; } d.entries = entries; if (f2 & 1) d.flags = view.getUint32(off + 4, true); d.isGlobalCompletion = (f1 & 1) !== 0; diff --git a/tools/triage-local-autoimport-details.mjs b/tools/triage-local-autoimport-details.mjs index 9563a50..aba677e 100644 --- a/tools/triage-local-autoimport-details.mjs +++ b/tools/triage-local-autoimport-details.mjs @@ -12,11 +12,21 @@ import { tnbHarnessEnv, withTsserver } from './tsserver-harness.mjs'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const tsserverPath = path.join(repoRoot, 'lib', 'tsserver.js'); const fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'tnb-local-autoimport-')); -const main = path.join(fixture, 'src', 'main.ts'); -const model = path.join(fixture, 'src', 'model.ts'); +const main = path.join(fixture, 'server', 'utils', 'main.ts'); +const model = path.join(fixture, 'server', 'models', 'model.ts'); fs.mkdirSync(path.dirname(main), { recursive: true }); -fs.writeFileSync(path.join(fixture, 'tsconfig.json'), JSON.stringify({ include: ['./src/*.ts'] })); +fs.mkdirSync(path.dirname(model), { recursive: true }); +fs.mkdirSync(path.join(fixture, '.config'), { recursive: true }); +fs.writeFileSync(path.join(fixture, 'tsconfig.json'), JSON.stringify({ files: [], references: [{ path: './.config/tsconfig.app.json' }] })); +fs.writeFileSync(path.join(fixture, '.config', 'tsconfig.app.json'), JSON.stringify({ + compilerOptions: { + module: 'preserve', + moduleResolution: 'bundler', + paths: { '#server/*': ['../server/*'] }, + }, + include: ['../server/**/*.ts'], +})); fs.writeFileSync(main, 'ReadRecordModel;\n'); fs.writeFileSync(model, 'export const ReadRecordModel = {};\n'); @@ -26,11 +36,13 @@ try { args: ['--disableAutomaticTypingAcquisition', '--suppressDiagnosticEvents'], env: tnbHarnessEnv(), }, async ({ send }) => { + const preferences = { + includeCompletionsForModuleExports: true, + includeCompletionsWithInsertText: true, + importModuleSpecifierPreference: 'relative', + }; await send('configure', { - preferences: { - includeCompletionsForModuleExports: true, - includeCompletionsWithInsertText: true, - }, + preferences, }); await send('updateOpen', { changedFiles: [], @@ -45,25 +57,27 @@ try { includeExternalModuleExports: true, includeInsertTextCompletions: true, }); - const entry = completion.body?.entries?.find(candidate => candidate.name === 'ReadRecordModel' && candidate.source === './model'); - if (!entry) throw new Error('completionInfo did not return ReadRecordModel from ./model'); + const entry = completion.body?.entries?.find(candidate => candidate.name === 'ReadRecordModel'); + if (!entry?.source) throw new Error('completionInfo did not return a sourced ReadRecordModel entry'); + if (!entry.data?.tnbCompletionData) throw new Error('completionInfo did not preserve native completion resolve data'); const details = await send('completionEntryDetails', { file: main, line: 1, offset: 16, entryNames: [{ name: entry.name, source: entry.source, data: entry.data }], + preferences, }); if (!details.success) throw new Error(details.message || 'completionEntryDetails failed'); const changes = details.body?.[0]?.codeActions?.flatMap(action => action.changes ?? []) ?? []; const importEdit = changes .filter(change => change.fileName === main) .flatMap(change => change.textChanges ?? []) - .find(change => change.newText.includes('ReadRecordModel') && change.newText.includes('./model')); - if (!importEdit) throw new Error('completionEntryDetails returned no ./model import edit'); - return { data: entry.data, edit: importEdit.newText }; + .find(change => change.newText.includes('ReadRecordModel') && change.newText.includes(entry.source)); + if (!importEdit) throw new Error(`completionEntryDetails returned no ${entry.source} import edit`); + return { source: entry.source, edit: importEdit.newText }; }); - console.log(`ok project-local auto-import details: ${JSON.stringify(result)}`); + console.log(`ok native auto-import completion details: ${JSON.stringify(result)}`); } finally { fs.rmSync(fixture, { recursive: true, force: true }); From c28edf779a683d866dcbc2a5cb8c6769ec84564a Mon Sep 17 00:00:00 2001 From: KazariAI <166915487+KazariAI@users.noreply.github.com> Date: Thu, 6 Aug 2026 22:53:40 +0800 Subject: [PATCH 4/5] ci: require every triage witness to be classified --- .github/workflows/ci.yml | 3 +- tools/ci-witness-groups.mjs | 66 +++++++++++++++---------------------- 2 files changed, 28 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ebcb76..678b3fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,8 @@ jobs: # tools/ci-witness-groups.mjs is the single source of truth for the # witness matrix — it validates the table (dupes, missing scripts, - # count) on every run, so a bad edit fails here before six jobs spawn. + # count, orphans, ownership, baselines) on every run, so a bad edit + # fails here before the witness jobs spawn. - id: groups run: echo "matrix=$(node tools/ci-witness-groups.mjs matrix)" >> "$GITHUB_OUTPUT" diff --git a/tools/ci-witness-groups.mjs b/tools/ci-witness-groups.mjs index 4b99b3a..98c8f0e 100644 --- a/tools/ci-witness-groups.mjs +++ b/tools/ci-witness-groups.mjs @@ -14,7 +14,7 @@ // parent-watch-acceptance / rpcsym-adversarial all run ≤1s; the real cost is // nine ~49-70s witnesses): each of wg1-wg4 carries two heavies (~126-131s), // wg5 carries the ninth plus all ≤11s witnesses (~125s). Rebalance by moving -// names between lists; `all` validates the full table (below). +// names between lists; every invocation validates the full table (below). // // 2026-08-03 convergence audit: 14 witnesses wired (13 in the audit, plus // triage-nuxtui-exportstar 2026-08-04 — unblocked by the bin symlink the @@ -40,7 +40,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; -const TOTAL = 70; +const TOTAL = 71; // Witnesses intentionally NOT in the matrix — run on demand (reasons above). const LOCAL_ONLY = [ @@ -137,6 +137,7 @@ const groups = [ 'check-readme-ledger', 'triage-overlay-delta-sync', 'triage-completion-details-array', + 'triage-local-autoimport-details', 'triage-alias-self-loop', 'triage-alias-nil', 'triage-empty-literal', @@ -170,10 +171,29 @@ const toolsDir = path.dirname(fileURLToPath(import.meta.url)); const flat = groups.flatMap(g => g.witnesses); const dupes = flat.filter((w, i) => flat.indexOf(w) !== i); const missing = flat.filter(w => !fs.existsSync(path.join(toolsDir, `${w}.mjs`))); -if (dupes.length || missing.length || flat.length !== TOTAL) { - if (dupes.length) console.error(`duplicate witnesses: ${[...new Set(dupes)].join(', ')}`); - if (missing.length) console.error(`no tools/.mjs for: ${missing.join(', ')}`); - if (flat.length !== TOTAL) console.error(`witness count ${flat.length} != TOTAL ${TOTAL} — update the table and TOTAL together`); +const onDisk = fs.readdirSync(toolsDir) + .filter(f => f.startsWith('triage-') && f.endsWith('.mjs')) + .map(f => f.slice(0, -'.mjs'.length)); +const accounted = new Set([...flat, ...LOCAL_ONLY, ...OWNED_ELSEWHERE]); +const orphans = onDisk.filter(w => !accounted.has(w)); +const missingLocal = [...LOCAL_ONLY, ...OWNED_ELSEWHERE].filter(w => !fs.existsSync(path.join(toolsDir, `${w}.mjs`))); +const baselineDir = path.join(toolsDir, 'baselines'); +const groupSet = new Set(flat); +const unwired = fs.existsSync(baselineDir) + ? fs.readdirSync(baselineDir) + .filter(f => f.endsWith('.json') && f.startsWith('triage-')) + .map(f => f.slice(0, -'.json'.length)) + .filter(w => !groupSet.has(w)) + : []; +const errors = []; +if (dupes.length) errors.push(`duplicate witnesses: ${[...new Set(dupes)].join(', ')}`); +if (missing.length) errors.push(`no tools/.mjs for: ${missing.join(', ')}`); +if (flat.length !== TOTAL) errors.push(`witness count ${flat.length} != TOTAL ${TOTAL} — update the table and TOTAL together`); +if (orphans.length) errors.push(`orphan triage-*.mjs (no group, not local-only, not owned elsewhere): ${orphans.join(', ')}`); +if (missingLocal.length) errors.push(`no tools/.mjs for: ${missingLocal.join(', ')}`); +if (unwired.length) errors.push(`baseline json without a wired witness: ${unwired.map(w => `${w}.json`).join(', ')}`); +if (errors.length) { + for (const error of errors) console.error(error); process.exit(1); } @@ -199,39 +219,5 @@ if (arg === 'matrix') { for (const w of g.witnesses) console.log(` ${w}`); }); console.log(`total: ${flat.length} witnesses in ${groups.length} groups`); - - // Orphan sweep: every tools/triage-*.mjs must be in a group, LOCAL_ONLY, - // or OWNED_ELSEWHERE. LOCAL_ONLY/OWNED_ELSEWHERE names must exist on - // disk. tools/baselines/triage-.json must map to a wired witness - // (reverse direction — a group witness without a baseline — is fine). - const errors = []; - const onDisk = fs.readdirSync(toolsDir) - .filter(f => f.startsWith('triage-') && f.endsWith('.mjs')) - .map(f => f.slice(0, -'.mjs'.length)); - const accounted = new Set([...flat, ...LOCAL_ONLY, ...OWNED_ELSEWHERE]); - const orphans = onDisk.filter(w => !accounted.has(w)); - if (orphans.length) { - errors.push(`orphan triage-*.mjs (no group, not local-only, not owned elsewhere): ${orphans.join(', ')}`); - } - const missingLocal = [...LOCAL_ONLY, ...OWNED_ELSEWHERE].filter(w => !fs.existsSync(path.join(toolsDir, `${w}.mjs`))); - if (missingLocal.length) { - errors.push(`no tools/.mjs for: ${missingLocal.join(', ')}`); - } - const baselineDir = path.join(toolsDir, 'baselines'); - if (fs.existsSync(baselineDir)) { - const groupSet = new Set(flat); - const unwired = fs.readdirSync(baselineDir) - .filter(f => f.endsWith('.json') && f.startsWith('triage-')) - .map(f => f.slice(0, -'.json'.length)) - .filter(w => !groupSet.has(w)); - if (unwired.length) { - errors.push(`baseline json without a wired witness: ${unwired.map(w => `${w}.json`).join(', ')}`); - } - } - - if (errors.length) { - for (const e of errors) console.error(e); - process.exit(1); - } console.log(`local-only (${LOCAL_ONLY.length}) and owned-elsewhere (${OWNED_ELSEWHERE.length}) files all present; ${onDisk.length} triage-*.mjs accounted, no orphans; baselines all wired`); } From 988b5c3ec01c7a97de8b6491d72ac8b82b520746 Mon Sep 17 00:00:00 2001 From: KazariAI <166915487+KazariAI@users.noreply.github.com> Date: Thu, 6 Aug 2026 22:59:00 +0800 Subject: [PATCH 5/5] ci: keep auto-import witness registration scoped --- .github/workflows/ci.yml | 3 +- tools/ci-witness-groups.mjs | 63 +++++++++++++++++++++++-------------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 678b3fe..7ebcb76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,8 +26,7 @@ jobs: # tools/ci-witness-groups.mjs is the single source of truth for the # witness matrix — it validates the table (dupes, missing scripts, - # count, orphans, ownership, baselines) on every run, so a bad edit - # fails here before the witness jobs spawn. + # count) on every run, so a bad edit fails here before six jobs spawn. - id: groups run: echo "matrix=$(node tools/ci-witness-groups.mjs matrix)" >> "$GITHUB_OUTPUT" diff --git a/tools/ci-witness-groups.mjs b/tools/ci-witness-groups.mjs index 98c8f0e..49a050d 100644 --- a/tools/ci-witness-groups.mjs +++ b/tools/ci-witness-groups.mjs @@ -14,7 +14,7 @@ // parent-watch-acceptance / rpcsym-adversarial all run ≤1s; the real cost is // nine ~49-70s witnesses): each of wg1-wg4 carries two heavies (~126-131s), // wg5 carries the ninth plus all ≤11s witnesses (~125s). Rebalance by moving -// names between lists; every invocation validates the full table (below). +// names between lists; `all` validates the full table (below). // // 2026-08-03 convergence audit: 14 witnesses wired (13 in the audit, plus // triage-nuxtui-exportstar 2026-08-04 — unblocked by the bin symlink the @@ -171,29 +171,10 @@ const toolsDir = path.dirname(fileURLToPath(import.meta.url)); const flat = groups.flatMap(g => g.witnesses); const dupes = flat.filter((w, i) => flat.indexOf(w) !== i); const missing = flat.filter(w => !fs.existsSync(path.join(toolsDir, `${w}.mjs`))); -const onDisk = fs.readdirSync(toolsDir) - .filter(f => f.startsWith('triage-') && f.endsWith('.mjs')) - .map(f => f.slice(0, -'.mjs'.length)); -const accounted = new Set([...flat, ...LOCAL_ONLY, ...OWNED_ELSEWHERE]); -const orphans = onDisk.filter(w => !accounted.has(w)); -const missingLocal = [...LOCAL_ONLY, ...OWNED_ELSEWHERE].filter(w => !fs.existsSync(path.join(toolsDir, `${w}.mjs`))); -const baselineDir = path.join(toolsDir, 'baselines'); -const groupSet = new Set(flat); -const unwired = fs.existsSync(baselineDir) - ? fs.readdirSync(baselineDir) - .filter(f => f.endsWith('.json') && f.startsWith('triage-')) - .map(f => f.slice(0, -'.json'.length)) - .filter(w => !groupSet.has(w)) - : []; -const errors = []; -if (dupes.length) errors.push(`duplicate witnesses: ${[...new Set(dupes)].join(', ')}`); -if (missing.length) errors.push(`no tools/.mjs for: ${missing.join(', ')}`); -if (flat.length !== TOTAL) errors.push(`witness count ${flat.length} != TOTAL ${TOTAL} — update the table and TOTAL together`); -if (orphans.length) errors.push(`orphan triage-*.mjs (no group, not local-only, not owned elsewhere): ${orphans.join(', ')}`); -if (missingLocal.length) errors.push(`no tools/.mjs for: ${missingLocal.join(', ')}`); -if (unwired.length) errors.push(`baseline json without a wired witness: ${unwired.map(w => `${w}.json`).join(', ')}`); -if (errors.length) { - for (const error of errors) console.error(error); +if (dupes.length || missing.length || flat.length !== TOTAL) { + if (dupes.length) console.error(`duplicate witnesses: ${[...new Set(dupes)].join(', ')}`); + if (missing.length) console.error(`no tools/.mjs for: ${missing.join(', ')}`); + if (flat.length !== TOTAL) console.error(`witness count ${flat.length} != TOTAL ${TOTAL} — update the table and TOTAL together`); process.exit(1); } @@ -219,5 +200,39 @@ if (arg === 'matrix') { for (const w of g.witnesses) console.log(` ${w}`); }); console.log(`total: ${flat.length} witnesses in ${groups.length} groups`); + + // Orphan sweep: every tools/triage-*.mjs must be in a group, LOCAL_ONLY, + // or OWNED_ELSEWHERE. LOCAL_ONLY/OWNED_ELSEWHERE names must exist on + // disk. tools/baselines/triage-.json must map to a wired witness + // (reverse direction — a group witness without a baseline — is fine). + const errors = []; + const onDisk = fs.readdirSync(toolsDir) + .filter(f => f.startsWith('triage-') && f.endsWith('.mjs')) + .map(f => f.slice(0, -'.mjs'.length)); + const accounted = new Set([...flat, ...LOCAL_ONLY, ...OWNED_ELSEWHERE]); + const orphans = onDisk.filter(w => !accounted.has(w)); + if (orphans.length) { + errors.push(`orphan triage-*.mjs (no group, not local-only, not owned elsewhere): ${orphans.join(', ')}`); + } + const missingLocal = [...LOCAL_ONLY, ...OWNED_ELSEWHERE].filter(w => !fs.existsSync(path.join(toolsDir, `${w}.mjs`))); + if (missingLocal.length) { + errors.push(`no tools/.mjs for: ${missingLocal.join(', ')}`); + } + const baselineDir = path.join(toolsDir, 'baselines'); + if (fs.existsSync(baselineDir)) { + const groupSet = new Set(flat); + const unwired = fs.readdirSync(baselineDir) + .filter(f => f.endsWith('.json') && f.startsWith('triage-')) + .map(f => f.slice(0, -'.json'.length)) + .filter(w => !groupSet.has(w)); + if (unwired.length) { + errors.push(`baseline json without a wired witness: ${unwired.map(w => `${w}.json`).join(', ')}`); + } + } + + if (errors.length) { + for (const e of errors) console.error(e); + process.exit(1); + } console.log(`local-only (${LOCAL_ONLY.length}) and owned-elsewhere (${OWNED_ELSEWHERE.length}) files all present; ${onDisk.length} triage-*.mjs accounted, no orphans; baselines all wired`); }