From ca8ae3c3787db336843004b7961ab208e01837da Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Wed, 15 Oct 2025 11:39:17 +0200 Subject: [PATCH 1/9] Include inline completions in @vscode/chat-lib --- chat-lib/tsconfig.json | 16 ++- script/build/extractChatLib.ts | 217 ++++++++++++++++++++++++++++++++- src/lib/node/chatLibMain.ts | 3 + 3 files changed, 233 insertions(+), 3 deletions(-) diff --git a/chat-lib/tsconfig.json b/chat-lib/tsconfig.json index 46f43958cb..2c0dfdbfba 100644 --- a/chat-lib/tsconfig.json +++ b/chat-lib/tsconfig.json @@ -10,7 +10,21 @@ "declarationMap": true, "types": [ "node" - ] + ], + "paths": { + "#lib/*": [ + "./src/_internal/extension/completions-core/lib/src/*" + ], + "#prompt/*": [ + "./src/_internal/extension/completions-core/prompt/src/*" + ], + "#bridge/*": [ + "./src/_internal/extension/completions-core/bridge/src/*" + ], + "#types": [ + "./src/_internal/extension/completions-core/types/src" + ] + } }, "include": [ "src", diff --git a/script/build/extractChatLib.ts b/script/build/extractChatLib.ts index ec4efd257c..c63fdd9f99 100644 --- a/script/build/extractChatLib.ts +++ b/script/build/extractChatLib.ts @@ -6,6 +6,7 @@ import { exec } from 'child_process'; import * as fs from 'fs'; import { glob } from 'glob'; +import * as jsonc from 'jsonc-parser'; import * as path from 'path'; import { promisify } from 'util'; @@ -38,8 +39,11 @@ interface FileInfo { class ChatLibExtractor { private processedFiles = new Set(); private allFiles = new Map(); + private pathMappings: Map = new Map(); async extract(): Promise { + // Load path mappings from tsconfig.json + await this.loadPathMappings(); console.log('Starting chat-lib extraction...'); // Clean target directory @@ -63,6 +67,33 @@ class ChatLibExtractor { console.log('Chat-lib extraction completed successfully!'); } + private async loadPathMappings(): Promise { + const tsconfigPath = path.join(REPO_ROOT, 'tsconfig.json'); + const tsconfigContent = await fs.promises.readFile(tsconfigPath, 'utf-8'); + const tsconfig = jsonc.parse(tsconfigContent); + + if (tsconfig.compilerOptions?.paths) { + for (const [alias, targets] of Object.entries(tsconfig.compilerOptions.paths)) { + // Skip the 'vscode' mapping as it's handled separately + if (alias === 'vscode') { + continue; + } + + // Handle path mappings like "#lib/*" -> ["./src/extension/completions-core/lib/src/*"] + // and "#types" -> ["./src/extension/completions-core/types/src"] + if (Array.isArray(targets) && targets.length > 0) { + const target = targets[0]; // Use the first target + // Remove leading './' and trailing '/*' if present + const cleanTarget = target.replace(/^\.\//, '').replace(/\/\*$/, ''); + const cleanAlias = alias.replace(/\/\*$/, ''); + this.pathMappings.set(cleanAlias, cleanTarget); + } + } + } + + console.log('Loaded path mappings:', Array.from(this.pathMappings.entries())); + } + private async cleanTargetDir(): Promise { // Remove and recreate the src directory if (fs.existsSync(TARGET_DIR)) { @@ -113,16 +144,65 @@ class ChatLibExtractor { const content = await fs.promises.readFile(filePath, 'utf-8'); const dependencies: string[] = []; + // Remove single-line comments and process line by line to avoid matching commented imports + // We need to be careful not to remove strings that contain '//' + const lines = content.split('\n'); + const activeLines: string[] = []; + let inBlockComment = false; + + for (const line of lines) { + // Track block comments + if (line.trim().startsWith('/*')) { + inBlockComment = true; + } + if (inBlockComment) { + if (line.includes('*/')) { + inBlockComment = false; + } + continue; + } + + // Skip single-line comments + const trimmedLine = line.trim(); + if (trimmedLine.startsWith('//')) { + continue; + } + + // For lines that might have inline comments, we need to preserve string content + // Remove comments that are not inside strings + let processedLine = line; + // Simple heuristic: if the line contains import/export, keep everything up to // + // that's outside of string literals + if (trimmedLine.includes('import') || trimmedLine.includes('export')) { + // Remove inline comments (this is a simple approach - could be improved) + const commentIndex = line.indexOf('//'); + if (commentIndex !== -1) { + // Check if // is inside a string by counting quotes before it + const beforeComment = line.substring(0, commentIndex); + const singleQuotes = (beforeComment.match(/'/g) || []).length; + const doubleQuotes = (beforeComment.match(/"/g) || []).length; + // If even number of quotes, the comment is outside strings + if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0) { + processedLine = beforeComment; + } + } + } + + activeLines.push(processedLine); + } + + const activeContent = activeLines.join('\n'); + // Extract both import and export statements using regex // Matches: // - import ... from './path' // - export ... from './path' // - export { ... } from './path' // Updated regex to match all relative imports (including multiple ../ segments) - const importExportRegex = /(?:import|export)\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"](\.\.?\/[^'"]*)['"]/g; + const relativeImportRegex = /(?:import|export)\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"](\.\.?\/[^'"]*)['"]/g; let match; - while ((match = importExportRegex.exec(content)) !== null) { + while ((match = relativeImportRegex.exec(activeContent)) !== null) { const importPath = match[1]; const resolvedPath = this.resolveImportPath(filePath, importPath); @@ -131,9 +211,81 @@ class ChatLibExtractor { } } + // Also match path alias imports like: import ... from '#lib/...' or '#types' + // We need to resolve these to follow their dependencies + const aliasImportRegex = /(?:import|export)\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([#][^'"]*)['"]/g; + + while ((match = aliasImportRegex.exec(activeContent)) !== null) { + const importPath = match[1]; + const resolvedPath = this.resolvePathAlias(importPath); + + if (resolvedPath) { + dependencies.push(resolvedPath); + } + } + return dependencies; } + private resolvePathAlias(importPath: string): string | null { + // Handle path alias imports like '#lib/foo' or '#types' + // Find the matching alias by checking if the import starts with any registered alias + for (const [alias, targetPath] of this.pathMappings.entries()) { + if (importPath === alias) { + // Exact match for aliases without wildcards (e.g., '#types') + return this.resolveFileWithExtensions(path.join(REPO_ROOT, targetPath)); + } else if (importPath.startsWith(alias + '/')) { + // Wildcard match for aliases with /* (e.g., '#lib/foo' matches '#lib') + const remainder = importPath.substring(alias.length + 1); // +1 to skip the '/' + const fullPath = path.join(REPO_ROOT, targetPath, remainder); + return this.resolveFileWithExtensions(fullPath); + } + } + + // If no alias matched, return null + console.warn(`Warning: Path alias not found for: ${importPath}`); + return null; + } + + private resolveFileWithExtensions(basePath: string): string | null { + // Try with .ts extension + if (fs.existsSync(basePath + '.ts')) { + return this.normalizePath(path.relative(REPO_ROOT, basePath + '.ts')); + } + + // Try with .tsx extension + if (fs.existsSync(basePath + '.tsx')) { + return this.normalizePath(path.relative(REPO_ROOT, basePath + '.tsx')); + } + + // Try with .d.ts extension + if (fs.existsSync(basePath + '.d.ts')) { + return this.normalizePath(path.relative(REPO_ROOT, basePath + '.d.ts')); + } + + // Try with index.ts + if (fs.existsSync(path.join(basePath, 'index.ts'))) { + return this.normalizePath(path.relative(REPO_ROOT, path.join(basePath, 'index.ts'))); + } + + // Try with index.tsx + if (fs.existsSync(path.join(basePath, 'index.tsx'))) { + return this.normalizePath(path.relative(REPO_ROOT, path.join(basePath, 'index.tsx'))); + } + + // Try with index.d.ts + if (fs.existsSync(path.join(basePath, 'index.d.ts'))) { + return this.normalizePath(path.relative(REPO_ROOT, path.join(basePath, 'index.d.ts'))); + } + + // Try as-is + if (fs.existsSync(basePath)) { + return this.normalizePath(path.relative(REPO_ROOT, basePath)); + } + + return null; + } + private resolveImportPath(fromFile: string, importPath: string): string | null { const fromDir = path.dirname(fromFile); const resolved = path.resolve(fromDir, importPath); @@ -344,6 +496,67 @@ class ChatLibExtractor { // Copy all tiktoken files await this.copyTikTokenFiles(); + + // Update chat-lib tsconfig.json with path mappings + await this.updateChatLibTsConfig(); + } + + private async updateChatLibTsConfig(): Promise { + console.log('Updating chat-lib tsconfig.json with path mappings...'); + + const chatLibTsconfigPath = path.join(CHAT_LIB_DIR, 'tsconfig.json'); + const tsconfigContent = await fs.promises.readFile(chatLibTsconfigPath, 'utf-8'); + const tsconfig = jsonc.parse(tsconfigContent); + + // Ensure compilerOptions exists + if (!tsconfig.compilerOptions) { + tsconfig.compilerOptions = {}; + } + + // Ensure paths exists + if (!tsconfig.compilerOptions.paths) { + tsconfig.compilerOptions.paths = {}; + } + + // Read the root tsconfig once to check for wildcards + const rootTsconfigPath = path.join(REPO_ROOT, 'tsconfig.json'); + const rootTsconfigContent = await fs.promises.readFile(rootTsconfigPath, 'utf-8'); + const rootTsconfig = jsonc.parse(rootTsconfigContent); + + // Add path mappings from the root tsconfig, adjusted for chat-lib structure + // The files are in src/_internal/... structure + for (const [alias, targetPath] of this.pathMappings.entries()) { + // Convert from root paths like "src/extension/completions-core/lib/src" + // to chat-lib paths like "./src/_internal/extension/completions-core/lib/src" + // Remove the "src/" prefix from targetPath since it's already part of the _internal structure + const pathWithoutSrc = targetPath.replace(/^src\//, ''); + const chatLibPath = `./src/_internal/${pathWithoutSrc}`; + + let aliasWithWildcard = alias; + let pathWithWildcard = chatLibPath; + + // Check if the original mapping had a wildcard + if (rootTsconfig.compilerOptions?.paths) { + for (const key of Object.keys(rootTsconfig.compilerOptions.paths)) { + const keyWithoutWildcard = key.replace(/\/\*$/, ''); + if (keyWithoutWildcard === alias && key.endsWith('/*')) { + aliasWithWildcard = alias + '/*'; + pathWithWildcard = chatLibPath + '/*'; + break; + } + } + } + + tsconfig.compilerOptions.paths[aliasWithWildcard] = [pathWithWildcard]; + } + + // Write the updated tsconfig back + await fs.promises.writeFile( + chatLibTsconfigPath, + JSON.stringify(tsconfig, null, '\t') + '\n' + ); + + console.log('Chat-lib tsconfig.json updated with path mappings:', Object.keys(tsconfig.compilerOptions.paths)); } private async validateModule(): Promise { diff --git a/src/lib/node/chatLibMain.ts b/src/lib/node/chatLibMain.ts index 8d5719e801..0da8eeeb69 100644 --- a/src/lib/node/chatLibMain.ts +++ b/src/lib/node/chatLibMain.ts @@ -62,6 +62,9 @@ import { generateUuid } from '../../util/vs/base/common/uuid'; import { SyncDescriptor } from '../../util/vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from '../../util/vs/platform/instantiation/common/instantiation'; +import { getInlineCompletions } from '../../extension/completions-core/lib/src/inlineCompletion'; +export { getInlineCompletions }; + /** * Log levels (taken from vscode.d.ts) */ From ef04ba938e1ef70c236a9d92befe63d6fa479197 Mon Sep 17 00:00:00 2001 From: Jeff Hunter Date: Thu, 16 Oct 2025 11:48:07 -0400 Subject: [PATCH 2/9] Follow type imports, * exports without "as", and jsxImportSource pragmas for dependency extraction --- script/build/extractChatLib.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/script/build/extractChatLib.ts b/script/build/extractChatLib.ts index c63fdd9f99..2d1f0325bd 100644 --- a/script/build/extractChatLib.ts +++ b/script/build/extractChatLib.ts @@ -153,7 +153,10 @@ class ChatLibExtractor { for (const line of lines) { // Track block comments if (line.trim().startsWith('/*')) { - inBlockComment = true; + // preserve pragmas in tsx files + if (!(filePath.endsWith('.tsx') && line.match(/\/\*\*\s+@jsxImportSource\s+\S+/))) { + inBlockComment = true; + } } if (inBlockComment) { if (line.includes('*/')) { @@ -199,7 +202,7 @@ class ChatLibExtractor { // - export ... from './path' // - export { ... } from './path' // Updated regex to match all relative imports (including multiple ../ segments) - const relativeImportRegex = /(?:import|export)\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"](\.\.?\/[^'"]*)['"]/g; + const relativeImportRegex = /(?:import(?:\s+type)?|export)\s+(?:(?:\{[^}]*\}|\*(?:\s+as\s+\w+)?|\w+)\s+from\s+)?['"](\.\.?\/[^'"]*)['"]/g; let match; while ((match = relativeImportRegex.exec(activeContent)) !== null) { @@ -213,7 +216,7 @@ class ChatLibExtractor { // Also match path alias imports like: import ... from '#lib/...' or '#types' // We need to resolve these to follow their dependencies - const aliasImportRegex = /(?:import|export)\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([#][^'"]*)['"]/g; + const aliasImportRegex = /(?:import(?:\s+type)?|export)\s+(?:(?:\{[^}]*\}|\*(?:\s+as\s+\w+)?|\w+)\s+from\s+)?['"]([#][^'"]*)['"]/g; while ((match = aliasImportRegex.exec(activeContent)) !== null) { const importPath = match[1]; @@ -224,6 +227,20 @@ class ChatLibExtractor { } } + // For tsx files process JSX imports as well + if (filePath.endsWith('.tsx')) { + const jsxRelativeImportRegex = /\/\*\*\s+@jsxImportSource\s+(\.\.?\/\S+)\s+\*\//g; + + while ((match = jsxRelativeImportRegex.exec(activeContent)) !== null) { + const importPath = match[1]; + const resolvedPath = this.resolveImportPath(filePath, path.join(importPath, 'jsx-runtime')); + + if (resolvedPath) { + dependencies.push(resolvedPath); + } + } + } + return dependencies; } From d2ce128d83b587a0a602be1192c40520fb99d480 Mon Sep 17 00:00:00 2001 From: Jeff Hunter Date: Mon, 20 Oct 2025 17:28:51 -0400 Subject: [PATCH 3/9] update @vscode/chat-lib test configuration --- chat-lib/package-lock.json | 49 ++++++++++++++++++++++++++++++++++++++ chat-lib/package.json | 1 + chat-lib/vitest.config.ts | 5 +++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/chat-lib/package-lock.json b/chat-lib/package-lock.json index 20aca63f07..98eb303ca6 100644 --- a/chat-lib/package-lock.json +++ b/chat-lib/package-lock.json @@ -28,6 +28,7 @@ "outdent": "^0.8.0", "rimraf": "^6.0.1", "typescript": "^5.8.3", + "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.5" }, "engines": { @@ -2032,6 +2033,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4008,6 +4016,27 @@ "node": ">=14.0.0" } }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsx": { "version": "4.20.3", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", @@ -4272,6 +4301,26 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vitest": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", diff --git a/chat-lib/package.json b/chat-lib/package.json index af9567d839..ddc81f0bf4 100644 --- a/chat-lib/package.json +++ b/chat-lib/package.json @@ -33,6 +33,7 @@ "outdent": "^0.8.0", "rimraf": "^6.0.1", "typescript": "^5.8.3", + "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.5" }, "keywords": [ diff --git a/chat-lib/vitest.config.ts b/chat-lib/vitest.config.ts index 12e95e2be8..54697912e6 100644 --- a/chat-lib/vitest.config.ts +++ b/chat-lib/vitest.config.ts @@ -3,11 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; import { loadEnv } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; +const plugin = tsconfigPaths(); + export default defineConfig(({ mode }) => ({ + plugins: [plugin], test: { include: ['**/*.spec.ts', '**/*.spec.tsx'], exclude: [ From ac2f72321f4b67c85c71eee3314dff43c0cc1c63 Mon Sep 17 00:00:00 2001 From: Jeff Hunter Date: Thu, 23 Oct 2025 17:18:59 -0400 Subject: [PATCH 4/9] update chat lib extraction with new path and add context setup for lib --- chat-lib/package.json | 1 - chat-lib/test/getInlineCompletions.spec.ts | 40 +++ chat-lib/tsconfig.json | 8 +- eslint.config.mjs | 6 + script/build/extractChatLib.ts | 1 + .../vscode-node/completionsServiceBridges.ts | 2 +- .../vscode-node/lib/src/config.ts | 4 +- .../vscode-node/lib/src/notificationSender.ts | 8 +- src/lib/node/chatLibMain.ts | 299 +++++++++++++++++- 9 files changed, 356 insertions(+), 13 deletions(-) create mode 100644 chat-lib/test/getInlineCompletions.spec.ts diff --git a/chat-lib/package.json b/chat-lib/package.json index ddc81f0bf4..af9567d839 100644 --- a/chat-lib/package.json +++ b/chat-lib/package.json @@ -33,7 +33,6 @@ "outdent": "^0.8.0", "rimraf": "^6.0.1", "typescript": "^5.8.3", - "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.5" }, "keywords": [ diff --git a/chat-lib/test/getInlineCompletions.spec.ts b/chat-lib/test/getInlineCompletions.spec.ts new file mode 100644 index 0000000000..e5af4f0337 --- /dev/null +++ b/chat-lib/test/getInlineCompletions.spec.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Load env +import * as dotenv from 'dotenv'; +dotenv.config({ path: '../.env' }); + +import { createTextDocument } from '#lib/test/textDocument'; +import { assert, describe, it } from 'vitest'; +import { createInlineCompletionsProvider } from '../src/main'; + +describe('getInlineCompletions', () => { + it('should return completions for a document and position', async () => { + const provider = createInlineCompletionsProvider({ + fetcher: undefined as any, + authService: undefined as any, + telemetrySender: undefined as any, + isRunningInTest: true, + contextProviderMatch: undefined as any, + statusHandler: undefined as any, + documentManager: undefined as any, + workspace: undefined as any, + urlOpener: undefined as any, + editorInfo: undefined as any, + editorPluginInfo: undefined as any, + relatedPluginInfo: undefined as any, + editorSession: undefined as any, + notificationSender: undefined as any, + endpointProvider: undefined as any, + capiClientService: undefined as any, + }); + const doc = createTextDocument('file:///test.txt', 'javascript', 1, 'function main() {\n\n\n}\n'); + + const result = await provider.getInlineCompletions(doc, { line: 1, character: 0 }); + + assert(result); + }); +}); diff --git a/chat-lib/tsconfig.json b/chat-lib/tsconfig.json index 2c0dfdbfba..a4e292726c 100644 --- a/chat-lib/tsconfig.json +++ b/chat-lib/tsconfig.json @@ -13,16 +13,16 @@ ], "paths": { "#lib/*": [ - "./src/_internal/extension/completions-core/lib/src/*" + "./src/_internal/extension/completions-core/vscode-node/lib/src/*" ], "#prompt/*": [ - "./src/_internal/extension/completions-core/prompt/src/*" + "./src/_internal/extension/completions-core/vscode-node/prompt/src/*" ], "#bridge/*": [ - "./src/_internal/extension/completions-core/bridge/src/*" + "./src/_internal/extension/completions-core/vscode-node/bridge/src/*" ], "#types": [ - "./src/_internal/extension/completions-core/types/src" + "./src/_internal/extension/completions-core/vscode-node/types/src" ] } }, diff --git a/eslint.config.mjs b/eslint.config.mjs index be237c1df4..c6639db591 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -366,5 +366,11 @@ export default tseslint.config( 'local/no-unlayered-files': 'off', 'no-restricted-imports': 'off' } + }, + { + files: ['./src/lib/node/chatLibMain.ts'], + rules: { + 'import/no-restricted-paths': 'off' + } } ); diff --git a/script/build/extractChatLib.ts b/script/build/extractChatLib.ts index 2d1f0325bd..de2ea796ce 100644 --- a/script/build/extractChatLib.ts +++ b/script/build/extractChatLib.ts @@ -27,6 +27,7 @@ const entryPoints = [ 'src/platform/tokenizer/node/tikTokenizerWorker.ts', // For tests: 'src/platform/authentication/test/node/simulationTestCopilotTokenManager.ts', + 'src/extension/completions-core/vscode-node/lib/src/test/textDocument.ts', ]; interface FileInfo { diff --git a/src/extension/completions-core/vscode-node/completionsServiceBridges.ts b/src/extension/completions-core/vscode-node/completionsServiceBridges.ts index 7f6a880d7d..a857d6048a 100644 --- a/src/extension/completions-core/vscode-node/completionsServiceBridges.ts +++ b/src/extension/completions-core/vscode-node/completionsServiceBridges.ts @@ -143,7 +143,7 @@ export function createContext(serviceAccessor: ServicesAccessor): Context { } }); - ctx.set(NotificationSender, new ExtensionNotificationSender()); + ctx.set(NotificationSender, instaService.createInstance(ExtensionNotificationSender)); ctx.set(EditorAndPluginInfo, new VSCodeEditorInfo()); ctx.set(EditorSession, new EditorSession(env.sessionId, env.machineId, env.remoteName, uiKindToString(env.uiKind))); ctx.set(CopilotExtensionStatus, new CopilotExtensionStatus()); diff --git a/src/extension/completions-core/vscode-node/lib/src/config.ts b/src/extension/completions-core/vscode-node/lib/src/config.ts index c88c16d1ad..671c99f27e 100644 --- a/src/extension/completions-core/vscode-node/lib/src/config.ts +++ b/src/extension/completions-core/vscode-node/lib/src/config.ts @@ -380,14 +380,14 @@ type NameAndVersion = { version: string; }; -type EditorInfo = NameAndVersion & { +export type EditorInfo = NameAndVersion & { // The root directory of the installation, currently only used to simplify stack traces. root?: string; // A programmatic name, used for error reporting. devName?: string; }; -type EditorPluginInfo = NameAndVersion; +export type EditorPluginInfo = NameAndVersion; export type EditorPluginFilter = { filter: Filter; value: string; isVersion?: boolean }; diff --git a/src/extension/completions-core/vscode-node/lib/src/notificationSender.ts b/src/extension/completions-core/vscode-node/lib/src/notificationSender.ts index a4695aef82..c76db2d971 100644 --- a/src/extension/completions-core/vscode-node/lib/src/notificationSender.ts +++ b/src/extension/completions-core/vscode-node/lib/src/notificationSender.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window } from 'vscode'; +import { INotificationService } from '../../../../../platform/notification/common/notificationService'; export interface ActionItem { title: string; @@ -13,8 +13,12 @@ export abstract class NotificationSender { } export class ExtensionNotificationSender extends NotificationSender { + constructor(@INotificationService private readonly notificationService: INotificationService) { + super(); + } + async showWarningMessage(message: string, ...actions: ActionItem[]): Promise { - const response = await window.showWarningMessage(message, ...actions.map(action => action.title)); + const response = await this.notificationService.showWarningMessage(message, ...actions.map(action => action.title)); if (response === undefined) { return; } return { title: response }; } diff --git a/src/lib/node/chatLibMain.ts b/src/lib/node/chatLibMain.ts index 0da8eeeb69..decb8c63f4 100644 --- a/src/lib/node/chatLibMain.ts +++ b/src/lib/node/chatLibMain.ts @@ -4,6 +4,56 @@ *--------------------------------------------------------------------------------------------*/ import type * as vscode from 'vscode'; +import { DocumentSelector, Position } from 'vscode-languageserver-protocol'; +import { CompletionsAuthenticationServiceBridge } from '../../extension/completions-core/vscode-node/bridge/src/completionsAuthenticationServiceBridge'; +import { CompletionsCapiBridge } from '../../extension/completions-core/vscode-node/bridge/src/completionsCapiBridge'; +import { CompletionsEndpointProviderBridge } from '../../extension/completions-core/vscode-node/bridge/src/completionsEndpointProviderBridge'; +import { CompletionsExperimentationServiceBridge } from '../../extension/completions-core/vscode-node/bridge/src/completionsExperimentationServiceBridge'; +import { CompletionsIgnoreServiceBridge } from '../../extension/completions-core/vscode-node/bridge/src/completionsIgnoreServiceBridge'; +import { CompletionsTelemetryServiceBridge } from '../../extension/completions-core/vscode-node/bridge/src/completionsTelemetryServiceBridge'; +import { CopilotExtensionStatus } from '../../extension/completions-core/vscode-node/extension/src/extensionStatus'; +import { CopilotTokenManager } from '../../extension/completions-core/vscode-node/lib/src/auth/copilotTokenManager'; +import { CompletionNotifier } from '../../extension/completions-core/vscode-node/lib/src/completionNotifier'; +import { BuildInfo, ConfigProvider, DefaultsOnlyConfigProvider, EditorAndPluginInfo, EditorInfo, EditorPluginInfo, EditorSession, InMemoryConfigProvider } from '../../extension/completions-core/vscode-node/lib/src/config'; +import { CopilotContentExclusionManager } from '../../extension/completions-core/vscode-node/lib/src/contentExclusion/contentExclusionManager'; +import { Context } from '../../extension/completions-core/vscode-node/lib/src/context'; +import { UserErrorNotifier } from '../../extension/completions-core/vscode-node/lib/src/error/userErrorNotifier'; +import { Features } from '../../extension/completions-core/vscode-node/lib/src/experiments/features'; +import { FileReader } from '../../extension/completions-core/vscode-node/lib/src/fileReader'; +import { FileSystem } from '../../extension/completions-core/vscode-node/lib/src/fileSystem'; +import { AsyncCompletionManager } from '../../extension/completions-core/vscode-node/lib/src/ghostText/asyncCompletions'; +import { CompletionsCache } from '../../extension/completions-core/vscode-node/lib/src/ghostText/completionsCache'; +import { BlockModeConfig, ConfigBlockModeConfig } from '../../extension/completions-core/vscode-node/lib/src/ghostText/configBlockMode'; +import { CopilotCompletion } from '../../extension/completions-core/vscode-node/lib/src/ghostText/copilotCompletion'; +import { CurrentGhostText } from '../../extension/completions-core/vscode-node/lib/src/ghostText/current'; +import { ForceMultiLine, GetGhostTextOptions } from '../../extension/completions-core/vscode-node/lib/src/ghostText/ghostText'; +import { LastGhostText } from '../../extension/completions-core/vscode-node/lib/src/ghostText/last'; +import { ITextEditorOptions } from '../../extension/completions-core/vscode-node/lib/src/ghostText/normalizeIndent'; +import { SpeculativeRequestCache } from '../../extension/completions-core/vscode-node/lib/src/ghostText/speculativeRequestCache'; +import { getInlineCompletions } from '../../extension/completions-core/vscode-node/lib/src/inlineCompletion'; +import { LocalFileSystem } from '../../extension/completions-core/vscode-node/lib/src/localFileSystem'; +import { LogLevel as CompletionsLogLevel, LogTarget, TelemetryLogSender } from '../../extension/completions-core/vscode-node/lib/src/logger'; +import { TelemetryLogSenderImpl } from '../../extension/completions-core/vscode-node/lib/src/logging/telemetryLogSender'; +import { Fetcher } from '../../extension/completions-core/vscode-node/lib/src/networking'; +import { ActionItem, NotificationSender } from '../../extension/completions-core/vscode-node/lib/src/notificationSender'; +import { LiveOpenAIFetcher, OpenAIFetcher } from '../../extension/completions-core/vscode-node/lib/src/openai/fetch'; +import { AvailableModelsManager } from '../../extension/completions-core/vscode-node/lib/src/openai/model'; +import { StatusChangedEvent, StatusReporter } from '../../extension/completions-core/vscode-node/lib/src/progress'; +import { CompletionsPromptFactory, createCompletionsPromptFactory } from '../../extension/completions-core/vscode-node/lib/src/prompt/completionsPromptFactory/completionsPromptFactory'; +import { ContextProviderBridge } from '../../extension/completions-core/vscode-node/lib/src/prompt/components/contextProviderBridge'; +import { ContextProviderRegistry, DefaultContextProviders, DefaultContextProvidersContainer, getContextProviderRegistry } from '../../extension/completions-core/vscode-node/lib/src/prompt/contextProviderRegistry'; +import { ContextProviderStatistics } from '../../extension/completions-core/vscode-node/lib/src/prompt/contextProviderStatistics'; +import { FullRecentEditsProvider, RecentEditsProvider } from '../../extension/completions-core/vscode-node/lib/src/prompt/recentEdits/recentEditsProvider'; +import { CompositeRelatedFilesProvider } from '../../extension/completions-core/vscode-node/lib/src/prompt/similarFiles/compositeRelatedFilesProvider'; +import { RelatedFilesProvider } from '../../extension/completions-core/vscode-node/lib/src/prompt/similarFiles/relatedFiles'; +import { TelemetryUserConfig } from '../../extension/completions-core/vscode-node/lib/src/telemetry'; +import { INotebookDocument, ITextDocument, TextDocumentIdentifier } from '../../extension/completions-core/vscode-node/lib/src/textDocument'; +import { TextDocumentChangeEvent, TextDocumentCloseEvent, TextDocumentFocusedEvent, TextDocumentManager, TextDocumentOpenEvent, WorkspaceFoldersChangeEvent } from '../../extension/completions-core/vscode-node/lib/src/textDocumentManager'; +import { Event } from '../../extension/completions-core/vscode-node/lib/src/util/event'; +import { UrlOpener } from '../../extension/completions-core/vscode-node/lib/src/util/opener'; +import { PromiseQueue } from '../../extension/completions-core/vscode-node/lib/src/util/promiseQueue'; +import { RuntimeMode } from '../../extension/completions-core/vscode-node/lib/src/util/runtimeMode'; +import { DocumentContext, WorkspaceFolder } from '../../extension/completions-core/vscode-node/types/src'; import { DebugRecorder } from '../../extension/inlineEdits/node/debugRecorder'; import { INextEditProvider, NextEditProvider } from '../../extension/inlineEdits/node/nextEditProvider'; import { LlmNESTelemetryBuilder, NextEditProviderTelemetryBuilder, TelemetrySender } from '../../extension/inlineEdits/node/nextEditProviderTelemetry'; @@ -26,6 +76,7 @@ import { IDiffService } from '../../platform/diff/common/diffService'; import { DiffServiceImpl } from '../../platform/diff/node/diffServiceImpl'; import { ICAPIClientService } from '../../platform/endpoint/common/capiClient'; import { IDomainService } from '../../platform/endpoint/common/domainService'; +import { IEndpointProvider } from '../../platform/endpoint/common/endpointProvider'; import { CAPIClientImpl } from '../../platform/endpoint/node/capiClientImpl'; import { DomainService } from '../../platform/endpoint/node/domainServiceImpl'; import { IEnvService } from '../../platform/env/common/envService'; @@ -61,9 +112,9 @@ import { Disposable } from '../../util/vs/base/common/lifecycle'; import { generateUuid } from '../../util/vs/base/common/uuid'; import { SyncDescriptor } from '../../util/vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from '../../util/vs/platform/instantiation/common/instantiation'; - -import { getInlineCompletions } from '../../extension/completions-core/lib/src/inlineCompletion'; -export { getInlineCompletions }; +export { + IAuthenticationService, ICAPIClientService, IEndpointProvider, IExperimentationService, IIgnoreService, ILanguageContextProviderService +}; /** * Log levels (taken from vscode.d.ts) @@ -391,3 +442,245 @@ class SimpleTelemetryService implements ITelemetryService { return; } } + +export type IDocumentContext = DocumentContext; + +export type CompletionsContextProviderMatchFunction = (documentSelector: DocumentSelector, documentContext: IDocumentContext) => Promise; + +export type ICompletionsStatusChangedEvent = StatusChangedEvent; + +export interface ICompletionsStatusHandler { + didChange(event: ICompletionsStatusChangedEvent): void; +} + +export type ICompletionsTextDocumentChangeEvent = Event; +export type ICompletionsTextDocumentOpenEvent = Event; +export type ICompletionsTextDocumentCloseEvent = Event; +export type ICompletionsTextDocumentFocusedEvent = Event; +export type ICompletionsWorkspaceFoldersChangeEvent = Event; +export type ICompletionsTextDocumentIdentifier = TextDocumentIdentifier; +export type ICompletionsNotebookDocument = INotebookDocument; +export type ICompletionsWorkspaceFolder = WorkspaceFolder; + +export interface ICompletionsTextDocumentManager { + onDidChangeTextDocument: ICompletionsTextDocumentChangeEvent; + onDidOpenTextDocument: ICompletionsTextDocumentOpenEvent; + onDidCloseTextDocument: ICompletionsTextDocumentCloseEvent; + + onDidFocusTextDocument: ICompletionsTextDocumentFocusedEvent; + onDidChangeWorkspaceFolders: ICompletionsWorkspaceFoldersChangeEvent; + + /** + * Get all open text documents, skipping content exclusions and other validations. + */ + getTextDocumentsUnsafe(): ITextDocument[]; + + /** + * If `TextDocument` represents notebook returns `INotebookDocument` instance, otherwise returns `undefined` + */ + findNotebook(doc: TextDocumentIdentifier): ICompletionsNotebookDocument | undefined; + + getWorkspaceFolders(): WorkspaceFolder[]; +} + +export interface IURLOpener { + open(url: string): Promise; +} + +export type IEditorInfo = EditorInfo; +export type IEditorPluginInfo = EditorPluginInfo; + +export interface IEditorSession { + readonly sessionId: string; + readonly machineId: string; + readonly remoteName?: string; + readonly uiKind?: string; +} + +export type IActionItem = ActionItem +export interface INotificationSender { + showWarningMessage(message: string, ...actions: IActionItem[]): Promise; +} + + +export interface IInlineCompletionsProviderOptions { + readonly fetcher: IFetcher; + readonly authService: IAuthenticationService; + readonly telemetrySender: ITelemetrySender; + readonly logTarget?: ILogTarget; + readonly isRunningInTest?: boolean; + readonly contextProviderMatch: CompletionsContextProviderMatchFunction; + readonly languageContextProvider?: ILanguageContextProviderService; + readonly statusHandler: ICompletionsStatusHandler; + readonly documentManager: ICompletionsTextDocumentManager; + readonly workspace: ObservableWorkspace; + readonly urlOpener: IURLOpener; + readonly editorInfo: IEditorInfo; + readonly editorPluginInfo: IEditorPluginInfo; + readonly relatedPluginInfo: IEditorPluginInfo[]; + readonly editorSession: IEditorSession; + readonly notificationSender: INotificationSender; + readonly ignoreService?: IIgnoreService; + readonly experimentationService?: IExperimentationService; + readonly endpointProvider: IEndpointProvider; + readonly capiClientService: ICAPIClientService; +} + +export type IGetInlineCompletionsOptions = Exclude, 'promptOnly'> & { + formattingOptions?: ITextEditorOptions; +}; + +export interface IInlineCompletionsProvider { + getInlineCompletions(textDocument: ITextDocument, position: Position, token?: CancellationToken, options?: IGetInlineCompletionsOptions): Promise; + dispose(): void; +} + +export function createInlineCompletionsProvider(options: IInlineCompletionsProviderOptions): IInlineCompletionsProvider { + const ctx = createContext(options); + return new InlineCompletionsProvider(ctx); +} + +class InlineCompletionsProvider extends Disposable implements IInlineCompletionsProvider { + + constructor(private _ctx: Context) { + super(); + } + + async getInlineCompletions(textDocument: ITextDocument, position: Position, token?: CancellationToken, options?: IGetInlineCompletionsOptions): Promise { + return await getInlineCompletions(this._ctx, textDocument, position, token, options); + } +} + +function createContext(options: IInlineCompletionsProviderOptions): Context { + const { fetcher, authService, statusHandler, documentManager, workspace, telemetrySender, urlOpener, editorSession } = options; + const logTarget = options.logTarget || new ConsoleLog(undefined, InternalLogLevel.Trace); + + const builder = new InstantiationServiceBuilder(); + builder.define(IAuthenticationService, authService); + builder.define(IIgnoreService, options.ignoreService || new NullIgnoreService()); + builder.define(ITelemetryService, new SyncDescriptor(SimpleTelemetryService, [telemetrySender])); + builder.define(IExperimentationService, options.experimentationService || new NullExperimentationService()); + builder.define(IEndpointProvider, options.endpointProvider); + builder.define(ICAPIClientService, options.capiClientService); + const instaService = builder.seal(); + + const ctx = new Context(); + ctx.set(CompletionsIgnoreServiceBridge, instaService.createInstance(CompletionsIgnoreServiceBridge)); + ctx.set(CompletionsTelemetryServiceBridge, instaService.createInstance(CompletionsTelemetryServiceBridge)); + ctx.set(CompletionsAuthenticationServiceBridge, instaService.createInstance(CompletionsAuthenticationServiceBridge)); + ctx.set(CompletionsExperimentationServiceBridge, instaService.createInstance(CompletionsExperimentationServiceBridge)); + ctx.set(CompletionsEndpointProviderBridge, instaService.createInstance(CompletionsEndpointProviderBridge)); + ctx.set(CompletionsCapiBridge, instaService.createInstance(CompletionsCapiBridge)); + ctx.set(ConfigProvider, new InMemoryConfigProvider(new DefaultsOnlyConfigProvider(), new Map())); + ctx.set(CopilotContentExclusionManager, new CopilotContentExclusionManager(ctx)); + ctx.set(RuntimeMode, RuntimeMode.fromEnvironment(options.isRunningInTest ?? false)); + ctx.set(BuildInfo, new BuildInfo()); + ctx.set(CompletionsCache, new CompletionsCache()); + ctx.set(Features, new Features(ctx)); + ctx.set(TelemetryLogSender, new TelemetryLogSenderImpl()); + ctx.set(TelemetryUserConfig, new TelemetryUserConfig(ctx)); + ctx.set(UserErrorNotifier, new UserErrorNotifier()); + ctx.set(OpenAIFetcher, new LiveOpenAIFetcher()); + ctx.set(BlockModeConfig, new ConfigBlockModeConfig()); + ctx.set(PromiseQueue, new PromiseQueue()); + ctx.set(CompletionNotifier, new CompletionNotifier(ctx)); + ctx.set(FileReader, new FileReader(ctx)); + try { + ctx.set(CompletionsPromptFactory, createCompletionsPromptFactory(ctx)); + } catch (e) { + console.log(e); + } + ctx.set(LastGhostText, new LastGhostText()); + ctx.set(CurrentGhostText, new CurrentGhostText()); + ctx.set(AvailableModelsManager, new AvailableModelsManager(ctx)); + ctx.set(AsyncCompletionManager, new AsyncCompletionManager(ctx)); + ctx.set(SpeculativeRequestCache, new SpeculativeRequestCache()); + + ctx.set(Fetcher, new class extends Fetcher { + override get name(): string { + return (fetcher as any).name || fetcher.constructor.name; + } + override fetch(url: string, options: FetchOptions) { + return fetcher.fetch(url, options); + } + override disconnectAll(): Promise { + return fetcher.disconnectAll(); + } + }); + + ctx.set(NotificationSender, new class extends NotificationSender { + async showWarningMessage(message: string, ...actions: IActionItem[]): Promise { + return await options.notificationSender.showWarningMessage(message, ...actions); + } + }); + ctx.set(EditorAndPluginInfo, new class extends EditorAndPluginInfo { + override getEditorInfo(): EditorInfo { + return options.editorInfo; + } + override getEditorPluginInfo(): EditorPluginInfo { + return options.editorPluginInfo; + } + override getRelatedPluginInfo(): EditorPluginInfo[] { + return options.relatedPluginInfo; + } + }); + ctx.set(EditorSession, new EditorSession(editorSession.sessionId, editorSession.machineId, editorSession.remoteName, editorSession.uiKind)); + ctx.set(CopilotExtensionStatus, new CopilotExtensionStatus()); + ctx.set(CopilotTokenManager, new CopilotTokenManager(ctx)); + ctx.set(StatusReporter, new class extends StatusReporter { + didChange(event: StatusChangedEvent): void { + statusHandler.didChange(event); + } + }); + ctx.set(TextDocumentManager, new class extends TextDocumentManager { + onDidChangeTextDocument = documentManager.onDidChangeTextDocument; + onDidOpenTextDocument = documentManager.onDidOpenTextDocument; + onDidCloseTextDocument = documentManager.onDidCloseTextDocument; + onDidFocusTextDocument = documentManager.onDidFocusTextDocument; + onDidChangeWorkspaceFolders = documentManager.onDidChangeWorkspaceFolders; + getTextDocumentsUnsafe(): ITextDocument[] { + return documentManager.getTextDocumentsUnsafe(); + } + findNotebook(doc: TextDocumentIdentifier): INotebookDocument | undefined { + return documentManager.findNotebook(doc); + } + getWorkspaceFolders(): WorkspaceFolder[] { + return documentManager.getWorkspaceFolders(); + } + }(ctx)); + ctx.set(ObservableWorkspace, workspace); + ctx.set(RecentEditsProvider, new FullRecentEditsProvider(ctx)); + ctx.set(FileSystem, new LocalFileSystem()); + ctx.set(RelatedFilesProvider, new CompositeRelatedFilesProvider(ctx)); + ctx.set(ContextProviderStatistics, new ContextProviderStatistics()); + ctx.set(ContextProviderRegistry, getContextProviderRegistry( + ctx, + (_, sel, docCtx) => options.contextProviderMatch(sel, docCtx), + options.languageContextProvider ?? new NullLanguageContextProviderService() + )); + ctx.set(ContextProviderBridge, new ContextProviderBridge(ctx)); + ctx.set(DefaultContextProviders, new DefaultContextProvidersContainer()); + ctx.set(ForceMultiLine, ForceMultiLine.default); + ctx.set(UrlOpener, new class extends UrlOpener { + async open(target: string) { + await urlOpener.open(target); + } + }); + + ctx.set(LogTarget, new class extends LogTarget { + override logIt(ctx: Context, level: CompletionsLogLevel, category: string, ...extra: unknown[]): void { + logTarget.logIt(this.toExternalLogLevel(level), category, ...extra); + } + toExternalLogLevel(level: CompletionsLogLevel): LogLevel { + switch (level) { + case CompletionsLogLevel.DEBUG: return LogLevel.Debug; + case CompletionsLogLevel.INFO: return LogLevel.Info; + case CompletionsLogLevel.WARN: return LogLevel.Warning; + case CompletionsLogLevel.ERROR: return LogLevel.Error; + default: return LogLevel.Info; + } + } + }); + + return ctx; +} From 17fc7c70a9f60ab0dea0e598d7ce3408afbf00f6 Mon Sep 17 00:00:00 2001 From: Jeff Hunter Date: Fri, 24 Oct 2025 17:21:00 -0400 Subject: [PATCH 5/9] initial stubs for inline completions test --- chat-lib/package-lock.json | 9 +- chat-lib/package.json | 2 +- chat-lib/test/getInlineCompletions.spec.ts | 215 +++++++++++++++++++-- src/lib/node/chatLibMain.ts | 2 +- 4 files changed, 207 insertions(+), 21 deletions(-) diff --git a/chat-lib/package-lock.json b/chat-lib/package-lock.json index 98eb303ca6..263fffb452 100644 --- a/chat-lib/package-lock.json +++ b/chat-lib/package-lock.json @@ -10,7 +10,7 @@ "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@microsoft/tiktokenizer": "^1.0.10", - "@vscode/copilot-api": "^0.1.12", + "@vscode/copilot-api": "^0.1.13", "@vscode/l10n": "^0.0.18", "@vscode/prompt-tsx": "^0.4.0-alpha.5", "jsonc-parser": "^3.3.1", @@ -28,7 +28,6 @@ "outdent": "^0.8.0", "rimraf": "^6.0.1", "typescript": "^5.8.3", - "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.5" }, "engines": { @@ -1002,9 +1001,9 @@ } }, "node_modules/@vscode/copilot-api": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/@vscode/copilot-api/-/copilot-api-0.1.12.tgz", - "integrity": "sha512-Jr9MfQDXeyLMY7pHN8XP/UxZUe9scIpsQic8Wosy3VeUYLZrUmS9QzYSDxSN7S3DxIL5YXNTcSW5PU/C6N2LbQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@vscode/copilot-api/-/copilot-api-0.1.13.tgz", + "integrity": "sha512-bVNAtC9y2nqF5LV7HpDd9BbuV81hstV+oIovo5MgJw1NWWNgeGpyBzcRJP0u6Dz6stRjka5UtEvC5dxqSWowyA==", "license": "SEE LICENSE" }, "node_modules/@vscode/l10n": { diff --git a/chat-lib/package.json b/chat-lib/package.json index af9567d839..78fe27af58 100644 --- a/chat-lib/package.json +++ b/chat-lib/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@microsoft/tiktokenizer": "^1.0.10", - "@vscode/copilot-api": "^0.1.12", + "@vscode/copilot-api": "^0.1.13", "@vscode/l10n": "^0.0.18", "@vscode/prompt-tsx": "^0.4.0-alpha.5", "jsonc-parser": "^3.3.1", diff --git a/chat-lib/test/getInlineCompletions.spec.ts b/chat-lib/test/getInlineCompletions.spec.ts index e5af4f0337..1769607235 100644 --- a/chat-lib/test/getInlineCompletions.spec.ts +++ b/chat-lib/test/getInlineCompletions.spec.ts @@ -8,28 +8,215 @@ import * as dotenv from 'dotenv'; dotenv.config({ path: '../.env' }); import { createTextDocument } from '#lib/test/textDocument'; +import { TextDocumentIdentifier } from '#lib/textDocument'; +import { TextDocumentChangeEvent, TextDocumentCloseEvent, TextDocumentFocusedEvent, TextDocumentOpenEvent, WorkspaceFoldersChangeEvent } from '#lib/textDocumentManager'; +import { CAPIClient } from '@vscode/copilot-api'; +import * as stream from 'stream'; import { assert, describe, it } from 'vitest'; -import { createInlineCompletionsProvider } from '../src/main'; +import { AuthenticationGetSessionOptions, AuthenticationSession, LanguageModelChat } from 'vscode'; +import { CopilotToken, TokenEnvelope } from '../src/_internal/platform/authentication/common/copilotToken'; +import { ChatEndpointFamily, EmbeddingsEndpointFamily } from '../src/_internal/platform/endpoint/common/endpointProvider'; +import { MutableObservableWorkspace } from '../src/_internal/platform/inlineEdits/common/observableWorkspace'; +import { FetchOptions, IAbortController, IHeaders, Response } from '../src/_internal/platform/networking/common/fetcherService'; +import { IChatEndpoint, IEmbeddingsEndpoint, IFetcher } from '../src/_internal/platform/networking/common/networking'; +import { Emitter, Event } from '../src/_internal/util/vs/base/common/event'; +import { Disposable } from '../src/_internal/util/vs/base/common/lifecycle'; +import { URI } from '../src/_internal/util/vs/base/common/uri'; +import { ChatRequest } from '../src/_internal/vscodeTypes'; +import { createInlineCompletionsProvider, IAuthenticationService, ICAPIClientService, ICompletionsStatusChangedEvent, ICompletionsTextDocumentManager, IEndpointProvider, ITelemetrySender } from '../src/main'; + +class TestFetcher implements IFetcher { + constructor(private readonly responses: Record) { } + + getUserAgentLibrary(): string { + return 'TestFetcher'; // matches the naming convention inside of completions + } + + async fetch(url: string, options: FetchOptions): Promise { + const uri = URI.parse(url); + const responseText = this.responses[uri.path]; + console.error(`${options.method ?? 'GET'} ${url}`); + + const headers = new class implements IHeaders { + get(name: string): string | null { + return null; + } + *[Symbol.iterator](): Iterator<[string, string]> { + // Empty headers for test + } + }; + + const found = typeof responseText === 'string'; + return new Response( + found ? 200 : 404, + found ? 'OK' : 'Not Found', + headers, + async () => responseText || '', + async () => JSON.parse(responseText || ''), + async () => stream.Readable.from([responseText || '']) + ); + } + + async disconnectAll(): Promise { + return Promise.resolve(); + } + + makeAbortController(): IAbortController { + return new AbortController(); + } + + isAbortError(e: any): boolean { + return e && e.name === 'AbortError'; + } + + isInternetDisconnectedError(e: any): boolean { + return false; + } + + isFetcherError(e: any): boolean { + return false; + } + + getUserMessageForFetcherError(err: any): string { + return `Test fetcher error: ${err.message}`; + } +} + +function createTestCopilotToken(envelope?: Partial>): CopilotToken { + const REFRESH_BUFFER_SECONDS = 60; + const expires_at = Date.now() + ((envelope?.refresh_in ?? 0) + REFRESH_BUFFER_SECONDS) * 1000; + return new CopilotToken({ + token: `test token ${Math.ceil(Math.random() * 100)}`, + refresh_in: 0, + expires_at, + username: 'testuser', + isVscodeTeamMember: false, + copilot_plan: 'testsku', + ...envelope + }); +} + +class TestAuthService extends Disposable implements IAuthenticationService { + readonly _serviceBrand: undefined; + readonly isMinimalMode = true; + readonly anyGitHubSession = undefined; + readonly permissiveGitHubSession = undefined; + readonly copilotToken = createTestCopilotToken(); + speculativeDecodingEndpointToken: string | undefined; + + private readonly _onDidAuthenticationChange = this._register(new Emitter()); + readonly onDidAuthenticationChange: Event = this._onDidAuthenticationChange.event; + + private readonly _onDidAccessTokenChange = this._register(new Emitter()); + readonly onDidAccessTokenChange = this._onDidAccessTokenChange.event; + + private readonly _onDidAdoAuthenticationChange = this._register(new Emitter()); + readonly onDidAdoAuthenticationChange = this._onDidAdoAuthenticationChange.event; + + async getAnyGitHubSession(options?: AuthenticationGetSessionOptions): Promise { + return undefined; + } + + async getPermissiveGitHubSession(options: AuthenticationGetSessionOptions): Promise { + return undefined; + } + + async getCopilotToken(force?: boolean): Promise { + return this.copilotToken; + } + + resetCopilotToken(httpError?: number): void { } + + async getAdoAccessTokenBase64(options?: AuthenticationGetSessionOptions): Promise { + return undefined; + } +} + +class TestTelemetrySender implements ITelemetrySender { + events: { eventName: string; properties?: Record; measurements?: Record }[] = []; + sendTelemetryEvent(eventName: string, properties?: Record, measurements?: Record): void { + this.events.push({ eventName, properties, measurements }); + } +} + +class TestEndpointProvider implements IEndpointProvider { + readonly _serviceBrand: undefined; + + async getAllCompletionModels(forceRefresh?: boolean) { + return []; + } + + async getAllChatEndpoints() { + return []; + } + + async getChatEndpoint(requestOrFamily: LanguageModelChat | ChatRequest | ChatEndpointFamily): Promise { + throw new Error('Method not implemented.'); + } + + async getEmbeddingsEndpoint(family?: EmbeddingsEndpointFamily): Promise { + throw new Error('Method not implemented.'); + } +} + +class TestCAPIClientService extends CAPIClient implements ICAPIClientService { + readonly _serviceBrand: undefined; + constructor() { + super({} as any, undefined, undefined as any /* IFetcherService */, '-'); + } +} + +class TestDocumentManager extends Disposable implements ICompletionsTextDocumentManager { + private readonly _onDidChangeTextDocument = this._register(new Emitter()); + readonly onDidChangeTextDocument = this._onDidChangeTextDocument.event; + + private readonly _onDidOpenTextDocument = this._register(new Emitter()); + readonly onDidOpenTextDocument = this._onDidOpenTextDocument.event; + + private readonly _onDidCloseTextDocument = this._register(new Emitter()); + readonly onDidCloseTextDocument = this._onDidCloseTextDocument.event; + + private readonly _onDidFocusTextDocument = this._register(new Emitter()); + readonly onDidFocusTextDocument = this._onDidFocusTextDocument.event; + + private readonly _onDidChangeWorkspaceFolders = this._register(new Emitter()); + readonly onDidChangeWorkspaceFolders = this._onDidChangeWorkspaceFolders.event; + + getTextDocumentsUnsafe() { + return []; + } + + findNotebook(doc: TextDocumentIdentifier) { + return undefined; + } + + getWorkspaceFolders() { + return []; + } +} describe('getInlineCompletions', () => { it('should return completions for a document and position', async () => { const provider = createInlineCompletionsProvider({ - fetcher: undefined as any, - authService: undefined as any, - telemetrySender: undefined as any, + fetcher: new TestFetcher({}), + authService: new TestAuthService(), + telemetrySender: new TestTelemetrySender(), isRunningInTest: true, - contextProviderMatch: undefined as any, - statusHandler: undefined as any, - documentManager: undefined as any, - workspace: undefined as any, + contextProviderMatch: async () => 0, + statusHandler: new class { didChange(_: ICompletionsStatusChangedEvent) { } }, + documentManager: new TestDocumentManager(), + workspace: new MutableObservableWorkspace(), urlOpener: undefined as any, - editorInfo: undefined as any, - editorPluginInfo: undefined as any, - relatedPluginInfo: undefined as any, - editorSession: undefined as any, + editorInfo: { name: 'test-editor', version: '1.0.0' }, + editorPluginInfo: { name: 'test-plugin', version: '1.0.0' }, + relatedPluginInfo: [], + editorSession: { + sessionId: 'test-session-id', + machineId: 'test-machine-id', + }, notificationSender: undefined as any, - endpointProvider: undefined as any, - capiClientService: undefined as any, + endpointProvider: new TestEndpointProvider(), + capiClientService: new TestCAPIClientService(), }); const doc = createTextDocument('file:///test.txt', 'javascript', 1, 'function main() {\n\n\n}\n'); diff --git a/src/lib/node/chatLibMain.ts b/src/lib/node/chatLibMain.ts index decb8c63f4..d9c72d59d6 100644 --- a/src/lib/node/chatLibMain.ts +++ b/src/lib/node/chatLibMain.ts @@ -598,7 +598,7 @@ function createContext(options: IInlineCompletionsProviderOptions): Context { ctx.set(Fetcher, new class extends Fetcher { override get name(): string { - return (fetcher as any).name || fetcher.constructor.name; + return fetcher.getUserAgentLibrary(); } override fetch(url: string, options: FetchOptions) { return fetcher.fetch(url, options); From 325830c67e6c7f02d49feff2cd53a1d03cc3d85a Mon Sep 17 00:00:00 2001 From: Jeff Hunter Date: Mon, 27 Oct 2025 16:20:41 -0400 Subject: [PATCH 6/9] round trip test for getInlineCompletions --- chat-lib/test/getInlineCompletions.reply.txt | 22 +++++++++++++++ chat-lib/test/getInlineCompletions.spec.ts | 28 +++++++++++++++----- 2 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 chat-lib/test/getInlineCompletions.reply.txt diff --git a/chat-lib/test/getInlineCompletions.reply.txt b/chat-lib/test/getInlineCompletions.reply.txt new file mode 100644 index 0000000000..a405c9ef31 --- /dev/null +++ b/chat-lib/test/getInlineCompletions.reply.txt @@ -0,0 +1,22 @@ +data: {"choices":[{"index":0,"finish_reason":null}]} + +data: {"choices":[{"text":" ","index":0,"finish_reason":null}]} + +data: {"choices":[{"text":" console","index":0,"finish_reason":null}]} + +data: {"choices":[{"text":".log","index":0,"finish_reason":null}]} + +data: {"choices":[{"text":"(\"","index":0,"finish_reason":null}]} + +data: {"choices":[{"text":"Hello","index":0,"finish_reason":null}]} + +data: {"choices":[{"text":",","index":0,"finish_reason":null}]} + +data: {"choices":[{"text":" World","index":0,"finish_reason":null}]} + +data: {"choices":[{"text":"!\");","index":0,"finish_reason":null}]} + +data: {"choices":[{"index":0,"finish_reason":"stop"}]} + +data: [DONE] + diff --git a/chat-lib/test/getInlineCompletions.spec.ts b/chat-lib/test/getInlineCompletions.spec.ts index 1769607235..b43c5c679b 100644 --- a/chat-lib/test/getInlineCompletions.spec.ts +++ b/chat-lib/test/getInlineCompletions.spec.ts @@ -7,12 +7,15 @@ import * as dotenv from 'dotenv'; dotenv.config({ path: '../.env' }); +import { ResultType } from '#lib/ghostText/ghostText'; import { createTextDocument } from '#lib/test/textDocument'; import { TextDocumentIdentifier } from '#lib/textDocument'; import { TextDocumentChangeEvent, TextDocumentCloseEvent, TextDocumentFocusedEvent, TextDocumentOpenEvent, WorkspaceFoldersChangeEvent } from '#lib/textDocumentManager'; import { CAPIClient } from '@vscode/copilot-api'; +import { readFile } from 'fs/promises'; +import { join } from 'path'; import * as stream from 'stream'; -import { assert, describe, it } from 'vitest'; +import { assert, describe, expect, it } from 'vitest'; import { AuthenticationGetSessionOptions, AuthenticationSession, LanguageModelChat } from 'vscode'; import { CopilotToken, TokenEnvelope } from '../src/_internal/platform/authentication/common/copilotToken'; import { ChatEndpointFamily, EmbeddingsEndpointFamily } from '../src/_internal/platform/endpoint/common/endpointProvider'; @@ -23,7 +26,7 @@ import { Emitter, Event } from '../src/_internal/util/vs/base/common/event'; import { Disposable } from '../src/_internal/util/vs/base/common/lifecycle'; import { URI } from '../src/_internal/util/vs/base/common/uri'; import { ChatRequest } from '../src/_internal/vscodeTypes'; -import { createInlineCompletionsProvider, IAuthenticationService, ICAPIClientService, ICompletionsStatusChangedEvent, ICompletionsTextDocumentManager, IEndpointProvider, ITelemetrySender } from '../src/main'; +import { createInlineCompletionsProvider, IActionItem, IAuthenticationService, ICAPIClientService, ICompletionsStatusChangedEvent, ICompletionsTextDocumentManager, IEndpointProvider, ILogTarget, ITelemetrySender, LogLevel } from '../src/main'; class TestFetcher implements IFetcher { constructor(private readonly responses: Record) { } @@ -35,7 +38,6 @@ class TestFetcher implements IFetcher { async fetch(url: string, options: FetchOptions): Promise { const uri = URI.parse(url); const responseText = this.responses[uri.path]; - console.error(`${options.method ?? 'GET'} ${url}`); const headers = new class implements IHeaders { get(name: string): string | null { @@ -195,18 +197,25 @@ class TestDocumentManager extends Disposable implements ICompletionsTextDocument } } +class NullLogTarget implements ILogTarget { + logIt(level: LogLevel, metadataStr: string, ...extra: any[]): void { } +} + describe('getInlineCompletions', () => { it('should return completions for a document and position', async () => { const provider = createInlineCompletionsProvider({ - fetcher: new TestFetcher({}), + fetcher: new TestFetcher({ '/v1/engines/gpt-4o-copilot/completions': await readFile(join(__dirname, 'getInlineCompletions.reply.txt'), 'utf8') }), authService: new TestAuthService(), telemetrySender: new TestTelemetrySender(), + logTarget: new NullLogTarget(), isRunningInTest: true, contextProviderMatch: async () => 0, statusHandler: new class { didChange(_: ICompletionsStatusChangedEvent) { } }, documentManager: new TestDocumentManager(), workspace: new MutableObservableWorkspace(), - urlOpener: undefined as any, + urlOpener: new class { + async open(_url: string) { } + }, editorInfo: { name: 'test-editor', version: '1.0.0' }, editorPluginInfo: { name: 'test-plugin', version: '1.0.0' }, relatedPluginInfo: [], @@ -214,14 +223,19 @@ describe('getInlineCompletions', () => { sessionId: 'test-session-id', machineId: 'test-machine-id', }, - notificationSender: undefined as any, + notificationSender: new class { + async showWarningMessage(_message: string, ..._items: IActionItem[]) { return undefined; } + }, endpointProvider: new TestEndpointProvider(), capiClientService: new TestCAPIClientService(), }); - const doc = createTextDocument('file:///test.txt', 'javascript', 1, 'function main() {\n\n\n}\n'); + const doc = createTextDocument('file:///test.txt', 'javascript', 1, 'function main() {\n\n}\n'); const result = await provider.getInlineCompletions(doc, { line: 1, character: 0 }); assert(result); + expect(result.length).toBe(1); + expect(result[0].resultType).toBe(ResultType.Async); + expect(result[0].displayText).toBe(' console.log("Hello, World!");'); }); }); From db961c4a7f16da9023acbecbe464ed4a7cb63fe5 Mon Sep 17 00:00:00 2001 From: Jeff Hunter Date: Mon, 27 Oct 2025 16:28:30 -0400 Subject: [PATCH 7/9] remove unused path mappings --- chat-lib/package-lock.json | 48 ---------------------- chat-lib/test/getInlineCompletions.spec.ts | 8 ++-- chat-lib/tsconfig.json | 15 +------ chat-lib/vitest.config.ts | 4 -- 4 files changed, 5 insertions(+), 70 deletions(-) diff --git a/chat-lib/package-lock.json b/chat-lib/package-lock.json index 263fffb452..c1de36daf0 100644 --- a/chat-lib/package-lock.json +++ b/chat-lib/package-lock.json @@ -2032,13 +2032,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true, - "license": "MIT" - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4015,27 +4008,6 @@ "node": ">=14.0.0" } }, - "node_modules/tsconfck": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", - "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", - "dev": true, - "license": "MIT", - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/tsx": { "version": "4.20.3", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", @@ -4300,26 +4272,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vite-tsconfig-paths": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", - "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "globrex": "^0.1.2", - "tsconfck": "^3.0.3" - }, - "peerDependencies": { - "vite": "*" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, "node_modules/vitest": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", diff --git a/chat-lib/test/getInlineCompletions.spec.ts b/chat-lib/test/getInlineCompletions.spec.ts index b43c5c679b..efd2065fb6 100644 --- a/chat-lib/test/getInlineCompletions.spec.ts +++ b/chat-lib/test/getInlineCompletions.spec.ts @@ -7,16 +7,16 @@ import * as dotenv from 'dotenv'; dotenv.config({ path: '../.env' }); -import { ResultType } from '#lib/ghostText/ghostText'; -import { createTextDocument } from '#lib/test/textDocument'; -import { TextDocumentIdentifier } from '#lib/textDocument'; -import { TextDocumentChangeEvent, TextDocumentCloseEvent, TextDocumentFocusedEvent, TextDocumentOpenEvent, WorkspaceFoldersChangeEvent } from '#lib/textDocumentManager'; import { CAPIClient } from '@vscode/copilot-api'; import { readFile } from 'fs/promises'; import { join } from 'path'; import * as stream from 'stream'; import { assert, describe, expect, it } from 'vitest'; import { AuthenticationGetSessionOptions, AuthenticationSession, LanguageModelChat } from 'vscode'; +import { ResultType } from '../src/_internal/extension/completions-core/vscode-node/lib/src/ghostText/ghostText'; +import { createTextDocument } from '../src/_internal/extension/completions-core/vscode-node/lib/src/test/textDocument'; +import { TextDocumentIdentifier } from '../src/_internal/extension/completions-core/vscode-node/lib/src/textDocument'; +import { TextDocumentChangeEvent, TextDocumentCloseEvent, TextDocumentFocusedEvent, TextDocumentOpenEvent, WorkspaceFoldersChangeEvent } from '../src/_internal/extension/completions-core/vscode-node/lib/src/textDocumentManager'; import { CopilotToken, TokenEnvelope } from '../src/_internal/platform/authentication/common/copilotToken'; import { ChatEndpointFamily, EmbeddingsEndpointFamily } from '../src/_internal/platform/endpoint/common/endpointProvider'; import { MutableObservableWorkspace } from '../src/_internal/platform/inlineEdits/common/observableWorkspace'; diff --git a/chat-lib/tsconfig.json b/chat-lib/tsconfig.json index a4e292726c..8816134dc7 100644 --- a/chat-lib/tsconfig.json +++ b/chat-lib/tsconfig.json @@ -11,20 +11,7 @@ "types": [ "node" ], - "paths": { - "#lib/*": [ - "./src/_internal/extension/completions-core/vscode-node/lib/src/*" - ], - "#prompt/*": [ - "./src/_internal/extension/completions-core/vscode-node/prompt/src/*" - ], - "#bridge/*": [ - "./src/_internal/extension/completions-core/vscode-node/bridge/src/*" - ], - "#types": [ - "./src/_internal/extension/completions-core/vscode-node/types/src" - ] - } + "paths": {} }, "include": [ "src", diff --git a/chat-lib/vitest.config.ts b/chat-lib/vitest.config.ts index 54697912e6..f0b0c8d68b 100644 --- a/chat-lib/vitest.config.ts +++ b/chat-lib/vitest.config.ts @@ -4,13 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { loadEnv } from 'vite'; -import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; -const plugin = tsconfigPaths(); - export default defineConfig(({ mode }) => ({ - plugins: [plugin], test: { include: ['**/*.spec.ts', '**/*.spec.tsx'], exclude: [ From 877c0c86da1d6768ed9111c20102c504064891f4 Mon Sep 17 00:00:00 2001 From: Jeff Hunter Date: Mon, 27 Oct 2025 16:43:35 -0400 Subject: [PATCH 8/9] fix type import --- chat-lib/test/getInlineCompletions.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-lib/test/getInlineCompletions.spec.ts b/chat-lib/test/getInlineCompletions.spec.ts index efd2065fb6..54afc4f59c 100644 --- a/chat-lib/test/getInlineCompletions.spec.ts +++ b/chat-lib/test/getInlineCompletions.spec.ts @@ -12,7 +12,7 @@ import { readFile } from 'fs/promises'; import { join } from 'path'; import * as stream from 'stream'; import { assert, describe, expect, it } from 'vitest'; -import { AuthenticationGetSessionOptions, AuthenticationSession, LanguageModelChat } from 'vscode'; +import type { AuthenticationGetSessionOptions, AuthenticationSession, LanguageModelChat } from 'vscode'; import { ResultType } from '../src/_internal/extension/completions-core/vscode-node/lib/src/ghostText/ghostText'; import { createTextDocument } from '../src/_internal/extension/completions-core/vscode-node/lib/src/test/textDocument'; import { TextDocumentIdentifier } from '../src/_internal/extension/completions-core/vscode-node/lib/src/textDocument'; From 28d71fa63faaf2ee20de13afb1cca3d9b3b11320 Mon Sep 17 00:00:00 2001 From: Jeff Hunter Date: Mon, 3 Nov 2025 17:16:08 -0500 Subject: [PATCH 9/9] send only original event names for chat-lib telemetry --- src/lib/node/chatLibMain.ts | 17 ++++++++++++++++- .../telemetry/node/azureInsightsReporter.ts | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/lib/node/chatLibMain.ts b/src/lib/node/chatLibMain.ts index d9c72d59d6..de378da7eb 100644 --- a/src/lib/node/chatLibMain.ts +++ b/src/lib/node/chatLibMain.ts @@ -104,6 +104,7 @@ import { ISnippyService, NullSnippyService } from '../../platform/snippy/common/ import { IExperimentationService, NullExperimentationService } from '../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService, TelemetryDestination, TelemetryEventMeasurements, TelemetryEventProperties } from '../../platform/telemetry/common/telemetry'; import { eventPropertiesToSimpleObject } from '../../platform/telemetry/common/telemetryData'; +import { unwrapEventNameFromPrefix } from '../../platform/telemetry/node/azureInsightsReporter'; import { ITokenizerProvider, TokenizerProvider } from '../../platform/tokenizer/node/tokenizer'; import { IWorkspaceService, NullWorkspaceService } from '../../platform/workspace/common/workspaceService'; import { InstantiationServiceBuilder } from '../../util/common/services'; @@ -551,6 +552,20 @@ class InlineCompletionsProvider extends Disposable implements IInlineCompletions } } +class UnwrappingTelemetrySender implements ITelemetrySender { + constructor(private readonly sender: ITelemetrySender) { } + + sendTelemetryEvent(eventName: string, properties?: Record, measurements?: Record): void { + this.sender.sendTelemetryEvent(this.normalizeEventName(eventName), properties, measurements); + } + + private normalizeEventName(eventName: string): string { + const unwrapped = unwrapEventNameFromPrefix(eventName); + const withoutPrefix = unwrapped.match(/^[^/]+\/(.*)/); + return withoutPrefix ? withoutPrefix[1] : unwrapped; + } +} + function createContext(options: IInlineCompletionsProviderOptions): Context { const { fetcher, authService, statusHandler, documentManager, workspace, telemetrySender, urlOpener, editorSession } = options; const logTarget = options.logTarget || new ConsoleLog(undefined, InternalLogLevel.Trace); @@ -558,7 +573,7 @@ function createContext(options: IInlineCompletionsProviderOptions): Context { const builder = new InstantiationServiceBuilder(); builder.define(IAuthenticationService, authService); builder.define(IIgnoreService, options.ignoreService || new NullIgnoreService()); - builder.define(ITelemetryService, new SyncDescriptor(SimpleTelemetryService, [telemetrySender])); + builder.define(ITelemetryService, new SyncDescriptor(SimpleTelemetryService, [new UnwrappingTelemetrySender(telemetrySender)])); builder.define(IExperimentationService, options.experimentationService || new NullExperimentationService()); builder.define(IEndpointProvider, options.endpointProvider); builder.define(ICAPIClientService, options.capiClientService); diff --git a/src/platform/telemetry/node/azureInsightsReporter.ts b/src/platform/telemetry/node/azureInsightsReporter.ts index c3a1b7802f..c16531348a 100644 --- a/src/platform/telemetry/node/azureInsightsReporter.ts +++ b/src/platform/telemetry/node/azureInsightsReporter.ts @@ -20,7 +20,7 @@ export function wrapEventNameForPrefixRemoval(eventName: string): string { function isWrappedEventName(eventName: string): boolean { return eventName.includes('wrapped-telemetry-event-name-') && eventName.endsWith('-wrapped-telemetry-event-name'); } -function unwrapEventNameFromPrefix(eventName: string): string { +export function unwrapEventNameFromPrefix(eventName: string): string { const match = eventName.match(/wrapped-telemetry-event-name-(.*?)-wrapped-telemetry-event-name/); return match ? match[1] : eventName; }