From b53ad49ecdc535047fccb90167ef0427e32941b6 Mon Sep 17 00:00:00 2001 From: luca cappa Date: Mon, 7 Oct 2024 10:39:59 -0700 Subject: [PATCH] add custom snippets PoC --- Extension/.vscode/launch.json | 9 +- Extension/Extension.code-workspace | 13 +++ Extension/src/LanguageServer/client.ts | 21 ++++- .../src/LanguageServer/copilotProviders.ts | 18 +++- Extension/src/LanguageServer/extension.ts | 85 +++++++++++++++++-- .../tests/copilotProviders.test.ts | 7 +- 6 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 Extension/Extension.code-workspace diff --git a/Extension/.vscode/launch.json b/Extension/.vscode/launch.json index 4323f133dd..759193d510 100644 --- a/Extension/.vscode/launch.json +++ b/Extension/.vscode/launch.json @@ -14,10 +14,17 @@ "--skip-release-notes", "--disable-workspace-trust", "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionDevelopmentPath=C:/Users/lucappa/.vscode-insiders/extensions/copilot-client", + "--disable-extension=ms.vscode.cpptools", + "--disable-extension=github.synth-lab", + "--disable-extension=github.copilot", + "--disable-extension=github.copilot-nightly", + "--log=github.copilot:debug", ], "sourceMaps": true, "outFiles": [ - "${workspaceFolder}/dist/**" + "${workspaceFolder}/dist/**", + "C:/Users/lucappa/.vscode-insiders/extensions/copilot-client/dist/**" ], // you can use a watch task as a prelaunch task and it works like you'd want it to. "preLaunchTask": "watch" diff --git a/Extension/Extension.code-workspace b/Extension/Extension.code-workspace new file mode 100644 index 0000000000..01855a711f --- /dev/null +++ b/Extension/Extension.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../../Users/lucappa/.vscode-insiders/extensions/copilot-client" + } + ], + "settings": { + "typescript.tsdk": "./node_modules/typescript/lib" + } +} diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index e46743815e..6e2e5ed7f2 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -55,7 +55,7 @@ import { Location, TextEdit, WorkspaceEdit } from './commonTypes'; import * as configs from './configurations'; import { DataBinding } from './dataBinding'; import { cachedEditorConfigSettings, getEditorConfigSettings } from './editorConfig'; -import { CppSourceStr, clients, configPrefix, updateLanguageConfigurations, usesCrashHandler, watchForCrashes } from './extension'; +import { CppSourceStr, SnippetEntry, clients, configPrefix, updateLanguageConfigurations, usesCrashHandler, watchForCrashes } from './extension'; import { LocalizeStringParams, getLocaleId, getLocalizedString } from './localization'; import { PersistentFolderState, PersistentWorkspaceState } from './persistentState'; import { RequestCancelled, ServerCancelled, createProtocolFilter } from './protocolFilter'; @@ -541,6 +541,15 @@ export interface ChatContextResult { targetArchitecture: string; } +export interface CompletionContextsResult { + context: SnippetEntry[]; +} + +export interface CompletionContextParams { + file: string; + caretOffset: number; +} + // Requests const PreInitializationRequest: RequestType = new RequestType('cpptools/preinitialize'); const InitializationRequest: RequestType = new RequestType('cpptools/initialize'); @@ -561,7 +570,7 @@ const GenerateDoxygenCommentRequest: RequestType = new RequestType('cpptools/didChangeCppProperties'); const IncludesRequest: RequestType = new RequestType('cpptools/getIncludes'); const CppContextRequest: RequestType = new RequestType('cpptools/getChatContext'); - +const CompletionContextRequest: RequestType = new RequestType('cpptools/getCompletionContext'); // Notifications to the server const DidOpenNotification: NotificationType = new NotificationType('textDocument/didOpen'); const FileCreatedNotification: NotificationType = new NotificationType('cpptools/fileCreated'); @@ -792,6 +801,7 @@ export interface Client { addTrustedCompiler(path: string): Promise; getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise; getChatContext(token: vscode.CancellationToken): Promise; + getCompletionContext(fileName: vscode.Uri, caretOffset: number, token: vscode.CancellationToken): Promise; } export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client { @@ -2220,6 +2230,12 @@ export class DefaultClient implements Client { () => this.languageClient.sendRequest(CppContextRequest, null, token), token); } + public async getCompletionContext(file: vscode.Uri, caretOffset: number, token: vscode.CancellationToken): Promise { + await withCancellation(this.ready, token); + return DefaultClient.withLspCancellationHandling( + () => this.languageClient.sendRequest(CompletionContextRequest, { file: file.toString(), caretOffset }, token), token); + } + /** * a Promise that can be awaited to know when it's ok to proceed. * @@ -4123,4 +4139,5 @@ class NullClient implements Client { addTrustedCompiler(path: string): Promise { return Promise.resolve(); } getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise { return Promise.resolve({} as GetIncludesResult); } getChatContext(token: vscode.CancellationToken): Promise { return Promise.resolve({} as ChatContextResult); } + getCompletionContext(file: vscode.Uri, caretOffset: number, token: vscode.CancellationToken): Promise { return Promise.resolve({} as CompletionContextsResult); } } diff --git a/Extension/src/LanguageServer/copilotProviders.ts b/Extension/src/LanguageServer/copilotProviders.ts index f23554f76d..d3173a813e 100644 --- a/Extension/src/LanguageServer/copilotProviders.ts +++ b/Extension/src/LanguageServer/copilotProviders.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import * as util from '../common'; import { ChatContextResult, GetIncludesResult } from './client'; -import { getActiveClient } from './extension'; +import { getActiveClient, SnippetEntry } from './extension'; export interface CopilotTrait { name: string; @@ -25,6 +25,22 @@ export interface CopilotApi { cancellationToken: vscode.CancellationToken ) => Promise<{ entries: vscode.Uri[]; traits?: CopilotTrait[] }> ): Disposable; + registerRelatedFilesProvider( + providerId: { extensionId: string; languageId: string }, + callback: ( + uri: vscode.Uri, + context: { flags: Record }, + cancellationToken: vscode.CancellationToken + ) => Promise<{ entries: vscode.Uri[]; traits?: CopilotTrait[] }> + ): Disposable; + registerSnippetsProvider( + providerId: { extensionId: string; languageId: string }, + callback: ( + uri: vscode.Uri, + context: { flags: Record }, + cancellationToken: vscode.CancellationToken + ) => Promise<{ entries: SnippetEntry[] }> + ): Disposable; } export async function registerRelatedFilesProvider(): Promise { diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 02dd3e8861..0874616f2e 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -20,10 +20,10 @@ import * as util from '../common'; import { getCrashCallStacksChannel } from '../logger'; import { PlatformInformation } from '../platform'; import * as telemetry from '../telemetry'; -import { Client, DefaultClient, DoxygenCodeActionCommandArguments, openFileVersions } from './client'; +import { Client, CompletionContextsResult, DefaultClient, DoxygenCodeActionCommandArguments, GetIncludesResult, openFileVersions } from './client'; import { ClientCollection } from './clientCollection'; import { CodeActionDiagnosticInfo, CodeAnalysisDiagnosticIdentifiersAndUri, codeAnalysisAllFixes, codeAnalysisCodeToFixes, codeAnalysisFileToCodeActions } from './codeAnalysis'; -import { registerRelatedFilesProvider } from './copilotProviders'; +import { getCopilotApi, registerRelatedFilesProvider } from './copilotProviders'; import { CppBuildTaskProvider } from './cppBuildTaskProvider'; import { getCustomConfigProviders } from './customProviders'; import { getLanguageConfig } from './languageConfig'; @@ -34,6 +34,21 @@ import { CppSettings } from './settings'; import { LanguageStatusUI, getUI } from './ui'; import { makeLspRange, rangeEquals, showInstallCompilerWalkthrough } from './utils'; +/* +interface CopilotTrait { + name: string; + value: string; + includeInPrompt?: boolean; + promptTextOverride?: string; +}*/ + +export interface SnippetEntry { + uri: string; + text: string; + startLine: number; + endLine: number; +} + nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); export const CppSourceStr: string = "C/C++"; @@ -264,6 +279,26 @@ export async function activate(): Promise { } await registerRelatedFilesProvider(); + + const isCustomSnippetProviderApiEnabled = await telemetry.isExperimentEnabled("CppToolsCustomSnippetsApi"); + if (isCustomSnippetProviderApiEnabled) { + const api = await getCopilotApi(); + if (util.extensionContext && api) { + try { + for (const languageId of ['c', 'cpp', 'cuda-cpp']) { + api.registerSnippetsProvider( + { extensionId: util.extensionContext.extension.id, languageId }, + async (uri: vscode.Uri, context: { flags: Record }, token: vscode.CancellationToken) => { + const result = await getCompletionContextWithCancellation(context.flags['caretOffset'] as number, token); + return { entries: result.context }; + } + ); + } + } catch { + console.log("Failed to register Copilot related files provider."); + } + } + } } export function updateLanguageConfigurations(): void { @@ -276,8 +311,8 @@ export function updateLanguageConfigurations(): void { } /** - * workspace events - */ + * workspace events + */ async function onDidChangeSettings(event: vscode.ConfigurationChangeEvent): Promise { const client: Client = clients.getDefaultClient(); if (client instanceof DefaultClient) { @@ -488,9 +523,9 @@ async function onSwitchHeaderSource(): Promise { } /** - * Allow the user to select a workspace when multiple workspaces exist and get the corresponding Client back. - * The resulting client is used to handle some command that was previously invoked. - */ + * Allow the user to select a workspace when multiple workspaces exist and get the corresponding Client back. + * The resulting client is used to handle some command that was previously invoked. + */ async function selectClient(): Promise { if (clients.Count === 1) { return clients.ActiveClient; @@ -1387,3 +1422,39 @@ export async function preReleaseCheck(): Promise { } } } + +export async function getIncludesWithCancellation(maxDepth: number, token: vscode.CancellationToken): Promise { + const includes = await clients.ActiveClient.getIncludes(maxDepth, token); + const wksFolder = clients.ActiveClient.RootUri?.toString(); + + if (!wksFolder) { + return includes; + } + + includes.includedFiles = includes.includedFiles.filter(header => vscode.Uri.file(header).toString().startsWith(wksFolder)); + return includes; +} + +export async function getCompletionContextWithCancellation(caretOffset: number, token: vscode.CancellationToken): Promise { + try { + const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!activeEditor) { + return { context: [] }; + } + + const snippets = await clients.ActiveClient.getCompletionContext(activeEditor.document.uri, caretOffset, token); + const wksFolder = clients.ActiveClient.RootUri?.toString(); + + if (!wksFolder) { + return snippets; + } + + // Fix up URIs to be relative to the workspace folder. + // //?? TODO Fix the check, the uri do not start with wksFolder whew. + //snippets.context = snippets.context.filter(snippet => + // vscode.Uri.file(snippet.uri).toString().startsWith(wksFolder)); + return snippets; + } catch (e) { + return { context: [] }; + } +} diff --git a/Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts b/Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts index 54052e122d..6e451977e0 100644 --- a/Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts +++ b/Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts @@ -39,6 +39,9 @@ describe('registerRelatedFilesProvider', () => { sinon.stub(util, 'extensionContext').value({ extension: { id: 'test-extension-id' } }); class MockCopilotApi implements CopilotApi { + registerSnippetsProvider(_providerId: { extensionId: string; languageId: string }, _callback: (uri: vscode.Uri, context: { flags: Record }, cancellationToken: vscode.CancellationToken) => Promise<{ entries: extension.SnippetEntry[] }>): Disposable { + throw new Error('Method not implemented.'); + } public registerRelatedFilesProvider( _providerId: { extensionId: string; languageId: string }, _callback: ( @@ -75,8 +78,8 @@ describe('registerRelatedFilesProvider', () => { }); const arrange = ({ vscodeExtension, getIncludeFiles, chatContext, rootUri, flags }: - { vscodeExtension?: vscode.Extension; getIncludeFiles?: GetIncludesResult; chatContext?: ChatContextResult; rootUri?: vscode.Uri; flags?: Record } = - { vscodeExtension: undefined, getIncludeFiles: undefined, chatContext: undefined, rootUri: undefined, flags: {} } + { vscodeExtension?: vscode.Extension; getIncludeFiles?: GetIncludesResult; chatContext?: ChatContextResult; rootUri?: vscode.Uri; flags?: Record } = + { vscodeExtension: undefined, getIncludeFiles: undefined, chatContext: undefined, rootUri: undefined, flags: {} } ) => { activeClientStub.getIncludes.resolves(getIncludeFiles); activeClientStub.getChatContext.resolves(chatContext);