Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Snippets PoC #12820

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Extension/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions Extension/Extension.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"folders": [
{
"path": "."
},
{
"path": "../../Users/lucappa/.vscode-insiders/extensions/copilot-client"
}
],
"settings": {
"typescript.tsdk": "./node_modules/typescript/lib"
}
}
21 changes: 19 additions & 2 deletions Extension/src/LanguageServer/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<void, string, void> = new RequestType<void, string, void>('cpptools/preinitialize');
const InitializationRequest: RequestType<CppInitializationParams, void, void> = new RequestType<CppInitializationParams, void, void>('cpptools/initialize');
Expand All @@ -561,7 +570,7 @@ const GenerateDoxygenCommentRequest: RequestType<GenerateDoxygenCommentParams, G
const ChangeCppPropertiesRequest: RequestType<CppPropertiesParams, void, void> = new RequestType<CppPropertiesParams, void, void>('cpptools/didChangeCppProperties');
const IncludesRequest: RequestType<GetIncludesParams, GetIncludesResult, void> = new RequestType<GetIncludesParams, GetIncludesResult, void>('cpptools/getIncludes');
const CppContextRequest: RequestType<void, ChatContextResult, void> = new RequestType<void, ChatContextResult, void>('cpptools/getChatContext');

const CompletionContextRequest: RequestType<CompletionContextParams, CompletionContextsResult, void> = new RequestType<CompletionContextParams, CompletionContextsResult, void>('cpptools/getCompletionContext');
// Notifications to the server
const DidOpenNotification: NotificationType<DidOpenTextDocumentParams> = new NotificationType<DidOpenTextDocumentParams>('textDocument/didOpen');
const FileCreatedNotification: NotificationType<FileChangedParams> = new NotificationType<FileChangedParams>('cpptools/fileCreated');
Expand Down Expand Up @@ -792,6 +801,7 @@ export interface Client {
addTrustedCompiler(path: string): Promise<void>;
getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise<GetIncludesResult>;
getChatContext(token: vscode.CancellationToken): Promise<ChatContextResult>;
getCompletionContext(fileName: vscode.Uri, caretOffset: number, token: vscode.CancellationToken): Promise<CompletionContextsResult>;
}

export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client {
Expand Down Expand Up @@ -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<CompletionContextsResult> {
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.
*
Expand Down Expand Up @@ -4123,4 +4139,5 @@ class NullClient implements Client {
addTrustedCompiler(path: string): Promise<void> { return Promise.resolve(); }
getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise<GetIncludesResult> { return Promise.resolve({} as GetIncludesResult); }
getChatContext(token: vscode.CancellationToken): Promise<ChatContextResult> { return Promise.resolve({} as ChatContextResult); }
getCompletionContext(file: vscode.Uri, caretOffset: number, token: vscode.CancellationToken): Promise<CompletionContextsResult> { return Promise.resolve({} as CompletionContextsResult); }
}
18 changes: 17 additions & 1 deletion Extension/src/LanguageServer/copilotProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<string, unknown> },
cancellationToken: vscode.CancellationToken
) => Promise<{ entries: vscode.Uri[]; traits?: CopilotTrait[] }>
): Disposable;
registerSnippetsProvider(
providerId: { extensionId: string; languageId: string },
callback: (
uri: vscode.Uri,
context: { flags: Record<string, unknown> },
cancellationToken: vscode.CancellationToken
) => Promise<{ entries: SnippetEntry[] }>
): Disposable;
}

export async function registerRelatedFilesProvider(): Promise<void> {
Expand Down
85 changes: 78 additions & 7 deletions Extension/src/LanguageServer/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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++";
Expand Down Expand Up @@ -264,6 +279,26 @@ export async function activate(): Promise<void> {
}

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<string, unknown> }, 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 {
Expand All @@ -276,8 +311,8 @@ export function updateLanguageConfigurations(): void {
}

/**
* workspace events
*/
* workspace events
*/
async function onDidChangeSettings(event: vscode.ConfigurationChangeEvent): Promise<void> {
const client: Client = clients.getDefaultClient();
if (client instanceof DefaultClient) {
Expand Down Expand Up @@ -488,9 +523,9 @@ async function onSwitchHeaderSource(): Promise<void> {
}

/**
* 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<Client> {
if (clients.Count === 1) {
return clients.ActiveClient;
Expand Down Expand Up @@ -1387,3 +1422,39 @@ export async function preReleaseCheck(): Promise<void> {
}
}
}

export async function getIncludesWithCancellation(maxDepth: number, token: vscode.CancellationToken): Promise<GetIncludesResult> {
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<CompletionContextsResult> {
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: [] };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
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<string, unknown> }, cancellationToken: vscode.CancellationToken) => Promise<{ entries: extension.SnippetEntry[] }>): Disposable {
throw new Error('Method not implemented.');
}
public registerRelatedFilesProvider(
_providerId: { extensionId: string; languageId: string },
_callback: (
Expand Down Expand Up @@ -75,8 +78,8 @@
});

const arrange = ({ vscodeExtension, getIncludeFiles, chatContext, rootUri, flags }:
{ vscodeExtension?: vscode.Extension<unknown>; getIncludeFiles?: GetIncludesResult; chatContext?: ChatContextResult; rootUri?: vscode.Uri; flags?: Record<string, unknown> } =
{ vscodeExtension: undefined, getIncludeFiles: undefined, chatContext: undefined, rootUri: undefined, flags: {} }
{ vscodeExtension?: vscode.Extension<unknown>; getIncludeFiles?: GetIncludesResult; chatContext?: ChatContextResult; rootUri?: vscode.Uri; flags?: Record<string, unknown> } =

Check failure on line 81 in Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts

View workflow job for this annotation

GitHub Actions / job / build

Expected indentation of 4 spaces but found 8

Check failure on line 81 in Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts

View workflow job for this annotation

GitHub Actions / job / build

Expected indentation of 4 spaces but found 8

Check failure on line 81 in Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts

View workflow job for this annotation

GitHub Actions / job / build

Expected indentation of 4 spaces but found 8
{ vscodeExtension: undefined, getIncludeFiles: undefined, chatContext: undefined, rootUri: undefined, flags: {} }

Check warning on line 82 in Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts

View workflow job for this annotation

GitHub Actions / job / build

Expected indentation of 4 spaces but found 8

Check failure on line 82 in Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts

View workflow job for this annotation

GitHub Actions / job / build

Expected indentation of 4 spaces but found 8

Check warning on line 82 in Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts

View workflow job for this annotation

GitHub Actions / job / build

Expected indentation of 4 spaces but found 8

Check failure on line 82 in Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts

View workflow job for this annotation

GitHub Actions / job / build

Expected indentation of 4 spaces but found 8

Check warning on line 82 in Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts

View workflow job for this annotation

GitHub Actions / job / build

Expected indentation of 4 spaces but found 8

Check failure on line 82 in Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts

View workflow job for this annotation

GitHub Actions / job / build

Expected indentation of 4 spaces but found 8
) => {
activeClientStub.getIncludes.resolves(getIncludeFiles);
activeClientStub.getChatContext.resolves(chatContext);
Expand Down
Loading