Skip to content

Commit 1c49d45

Browse files
committed
code snippet provider
1 parent 74d691f commit 1c49d45

File tree

3 files changed

+184
-1
lines changed

3 files changed

+184
-1
lines changed

Extension/src/LanguageServer/client.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import { Location, TextEdit, WorkspaceEdit } from './commonTypes';
5555
import * as configs from './configurations';
5656
import { DataBinding } from './dataBinding';
5757
import { cachedEditorConfigSettings, getEditorConfigSettings } from './editorConfig';
58-
import { CppSourceStr, clients, configPrefix, updateLanguageConfigurations, usesCrashHandler, watchForCrashes } from './extension';
58+
import { CppSourceStr, SnippetEntry, clients, configPrefix, updateLanguageConfigurations, usesCrashHandler, watchForCrashes } from './extension';
5959
import { LocalizeStringParams, getLocaleId, getLocalizedString } from './localization';
6060
import { PersistentFolderState, PersistentWorkspaceState } from './persistentState';
6161
import { RequestCancelled, ServerCancelled, createProtocolFilter } from './protocolFilter';
@@ -554,6 +554,15 @@ export interface ProjectContextResult {
554554
fileContext: FileContextResult;
555555
}
556556

557+
export interface CompletionContextsResult {
558+
context: SnippetEntry[];
559+
}
560+
561+
export interface CompletionContextParams {
562+
file: string;
563+
caretOffset: number;
564+
}
565+
557566
// Requests
558567
const PreInitializationRequest: RequestType<void, string, void> = new RequestType<void, string, void>('cpptools/preinitialize');
559568
const InitializationRequest: RequestType<CppInitializationParams, void, void> = new RequestType<CppInitializationParams, void, void>('cpptools/initialize');
@@ -575,6 +584,7 @@ const ChangeCppPropertiesRequest: RequestType<CppPropertiesParams, void, void> =
575584
const IncludesRequest: RequestType<GetIncludesParams, GetIncludesResult, void> = new RequestType<GetIncludesParams, GetIncludesResult, void>('cpptools/getIncludes');
576585
const CppContextRequest: RequestType<TextDocumentIdentifier, ChatContextResult, void> = new RequestType<TextDocumentIdentifier, ChatContextResult, void>('cpptools/getChatContext');
577586
const ProjectContextRequest: RequestType<TextDocumentIdentifier, ProjectContextResult, void> = new RequestType<TextDocumentIdentifier, ProjectContextResult, void>('cpptools/getProjectContext');
587+
const CompletionContextRequest: RequestType<CompletionContextParams, CompletionContextsResult, void> = new RequestType<CompletionContextParams, CompletionContextsResult, void>('cpptools/getCompletionContext');
578588

579589
// Notifications to the server
580590
const DidOpenNotification: NotificationType<DidOpenTextDocumentParams> = new NotificationType<DidOpenTextDocumentParams>('textDocument/didOpen');
@@ -807,6 +817,7 @@ export interface Client {
807817
getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise<GetIncludesResult>;
808818
getChatContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise<ChatContextResult>;
809819
getProjectContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise<ProjectContextResult>;
820+
getCompletionContext(fileName: vscode.Uri, caretOffset: number, token: vscode.CancellationToken): Promise<CompletionContextsResult>;
810821
}
811822

812823
export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client {
@@ -2249,6 +2260,12 @@ export class DefaultClient implements Client {
22492260
() => this.languageClient.sendRequest(ProjectContextRequest, params, token), token);
22502261
}
22512262

2263+
public async getCompletionContext(file: vscode.Uri, caretOffset: number, token: vscode.CancellationToken): Promise<CompletionContextsResult> {
2264+
await withCancellation(this.ready, token);
2265+
return DefaultClient.withLspCancellationHandling(
2266+
() => this.languageClient.sendRequest(CompletionContextRequest, { file: file.toString(), caretOffset }, token), token);
2267+
}
2268+
22522269
/**
22532270
* a Promise that can be awaited to know when it's ok to proceed.
22542271
*
@@ -4154,4 +4171,5 @@ class NullClient implements Client {
41544171
getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise<GetIncludesResult> { return Promise.resolve({} as GetIncludesResult); }
41554172
getChatContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise<ChatContextResult> { return Promise.resolve({} as ChatContextResult); }
41564173
getProjectContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise<ProjectContextResult> { return Promise.resolve({} as ProjectContextResult); }
4174+
getCompletionContext(file: vscode.Uri, caretOffset: number, token: vscode.CancellationToken): Promise<CompletionContextsResult> { return Promise.resolve({} as CompletionContextsResult); }
41574175
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All Rights Reserved.
3+
* See 'LICENSE' in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
import * as vscode from 'vscode';
6+
import { DocumentSelector } from 'vscode-languageserver-protocol';
7+
import { getOutputChannelLogger, Logger } from '../logger';
8+
import * as telemetry from '../telemetry';
9+
import { getCopilotApi } from "./copilotProviders";
10+
import { clients } from './extension';
11+
import { CodeSnippet, CompletionContext, ContextProviderApiV1 } from './tmp/contextProviderV1';
12+
13+
// An ever growing cache of completion context snippets. //?? TODO Evict old entries.
14+
const completionContextCache: Map<string, CodeSnippet[]> = new Map<string, CodeSnippet[]>();
15+
const cppDocumentSelector: DocumentSelector = [{ language: 'cpp' }, { language: 'c' }];
16+
17+
class DefaultValueFallback extends Error {
18+
static readonly DefaultValue = "DefaultValue";
19+
constructor() { super(DefaultValueFallback.DefaultValue); }
20+
}
21+
22+
class CancellationError extends Error {
23+
static readonly Cancelled = "Cancelled";
24+
constructor() { super(CancellationError.Cancelled); }
25+
}
26+
27+
let completionContextCancellation = new vscode.CancellationTokenSource();
28+
29+
// Mutually exclusive values for the kind of snippets. They either come from the cache,
30+
// are computed, or the computation is taking too long and no cache is present. In the latter
31+
// case, the cache is computed anyway while unblocking the execution flow returning undefined.
32+
enum SnippetsKind {
33+
Computed = 'computed',
34+
CacheHit = 'cacheHit',
35+
CacheMiss = 'cacheMiss'
36+
}
37+
38+
// Get the default value if the timeout expires, but throws an exception if the token is cancelled.
39+
async function waitForCompletionWithTimeoutAndCancellation<T>(promise: Promise<T>, defaultValue: T | undefined,
40+
timeout: number, token: vscode.CancellationToken): Promise<[T | undefined, SnippetsKind]> {
41+
const defaultValuePromise = new Promise<T>((resolve, reject) => setTimeout(() => {
42+
if (token.isCancellationRequested) {
43+
reject('DefaultValuePromise was cancelled');
44+
} else {
45+
reject(new DefaultValueFallback());
46+
}
47+
}, timeout));
48+
const cancellationPromise = new Promise<T>((_, reject) => {
49+
token.onCancellationRequested(() => {
50+
reject(new CancellationError());
51+
});
52+
});
53+
let snippetsOrNothing: T | undefined;
54+
try {
55+
snippetsOrNothing = await Promise.race([promise, cancellationPromise, defaultValuePromise]);
56+
} catch (e) {
57+
if (e instanceof DefaultValueFallback) {
58+
return [defaultValue, defaultValue !== undefined ? SnippetsKind.CacheHit : SnippetsKind.CacheMiss];
59+
}
60+
61+
// Rethrow the error for cancellation cases.
62+
throw e;
63+
}
64+
65+
return [snippetsOrNothing, SnippetsKind.Computed];
66+
}
67+
68+
// Get the completion context with a timeout and a cancellation token.
69+
// The cancellationToken indicates that the value should not be returned nor cached.
70+
async function getCompletionContextWithCancellation(documentUri: string, caretOffset: number,
71+
startTime: number, out: Logger, token: vscode.CancellationToken): Promise<CodeSnippet[]> {
72+
try {
73+
const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor;
74+
if (!activeEditor ||
75+
activeEditor.document.uri.toString() !== vscode.Uri.parse(documentUri).toString()) {
76+
return [];
77+
}
78+
79+
const snippets = await clients.ActiveClient.getCompletionContext(activeEditor.document.uri, caretOffset, token);
80+
81+
const codeSnippets = snippets.context.map((item) => {
82+
if (token.isCancellationRequested) {
83+
throw new CancellationError();
84+
}
85+
return {
86+
importance: item.importance, uri: item.uri, value: item.text
87+
};
88+
});
89+
90+
completionContextCache.set(documentUri, codeSnippets);
91+
const duration: number = Date.now() - startTime;
92+
out.appendLine(`Copilot: getCompletionContextWithCancellation(): Cached in [ms]: ${duration}`);
93+
// //?? TODO Add telemetry for elapsed time.
94+
95+
return codeSnippets;
96+
} catch (e) {
97+
const err = e as Error;
98+
out.appendLine(`Copilot: getCompletionContextWithCancellation(): Error: '${err?.message}', stack '${err?.stack}`);
99+
100+
// //?? TODO Add telemetry for failure.
101+
return [];
102+
}
103+
}
104+
105+
const timeBudgetFactor: number = 0.5;
106+
const cppToolsResolver = {
107+
async resolve(context: CompletionContext, copilotAborts: vscode.CancellationToken): Promise<CodeSnippet[]> {
108+
const startTime = Date.now();
109+
const out: Logger = getOutputChannelLogger();
110+
let snippetsKind: SnippetsKind = SnippetsKind.Computed;
111+
try {
112+
completionContextCancellation.cancel();
113+
completionContextCancellation = new vscode.CancellationTokenSource();
114+
const docUri = context.documentContext.uri;
115+
const cachedValue: CodeSnippet[] | undefined = completionContextCache.get(docUri.toString());
116+
const snippetsPromise = getCompletionContextWithCancellation(docUri,
117+
context.documentContext.offset, startTime, out, completionContextCancellation.token);
118+
const [codeSnippets, kind] = await waitForCompletionWithTimeoutAndCancellation(
119+
snippetsPromise, cachedValue, context.timeBudget * timeBudgetFactor, copilotAborts);
120+
snippetsKind = kind;
121+
// //?? TODO Add telemetry for Computed vs Cached.
122+
123+
return codeSnippets ?? [];
124+
} catch (e: any) {
125+
if (e instanceof CancellationError) {
126+
out.appendLine(`Copilot: getCompletionContext(): cancelled!`);
127+
}
128+
// //?? TODO Add telemetry for failure.
129+
} finally {
130+
const duration: number = Date.now() - startTime;
131+
out.appendLine(`Copilot: getCompletionContext(): snippets retrieval (${snippetsKind.toString()}) elapsed time (ms): ${duration}`);
132+
// //?? TODO Add telemetry for elapsed time.
133+
}
134+
135+
return [];
136+
}
137+
};
138+
139+
export async function registerCopilotContextProvider(): Promise<void> {
140+
try {
141+
const isCustomSnippetProviderApiEnabled = await telemetry.isExperimentEnabled("CppToolsCustomSnippetsApi");
142+
if (isCustomSnippetProviderApiEnabled) {
143+
const contextAPI = (await getCopilotApi() as any).getContextProviderAPI('v1') as ContextProviderApiV1;
144+
contextAPI.registerContextProvider({
145+
id: 'cppTools',
146+
selector: cppDocumentSelector,
147+
resolver: cppToolsResolver
148+
});
149+
}
150+
} catch {
151+
console.warn("Failed to register the Copilot Context Provider.");
152+
// //?? TODO Add telemetry for failure.
153+
}
154+
}

Extension/src/LanguageServer/extension.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import * as telemetry from '../telemetry';
2323
import { Client, DefaultClient, DoxygenCodeActionCommandArguments, openFileVersions } from './client';
2424
import { ClientCollection } from './clientCollection';
2525
import { CodeActionDiagnosticInfo, CodeAnalysisDiagnosticIdentifiersAndUri, codeAnalysisAllFixes, codeAnalysisCodeToFixes, codeAnalysisFileToCodeActions } from './codeAnalysis';
26+
import { registerCopilotContextProvider } from './copilotCompletionContextProvider';
2627
import { registerRelatedFilesProvider } from './copilotProviders';
2728
import { CppBuildTaskProvider } from './cppBuildTaskProvider';
2829
import { getCustomConfigProviders } from './customProviders';
@@ -34,6 +35,14 @@ import { CppSettings } from './settings';
3435
import { LanguageStatusUI, getUI } from './ui';
3536
import { makeLspRange, rangeEquals, showInstallCompilerWalkthrough } from './utils';
3637

38+
export interface SnippetEntry {
39+
uri: string;
40+
text: string;
41+
startLine: number;
42+
endLine: number;
43+
importance: number;
44+
}
45+
3746
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
3847
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
3948
export const CppSourceStr: string = "C/C++";
@@ -264,6 +273,8 @@ export async function activate(): Promise<void> {
264273
}
265274

266275
await registerRelatedFilesProvider();
276+
277+
await registerCopilotContextProvider();
267278
}
268279

269280
export function updateLanguageConfigurations(): void {

0 commit comments

Comments
 (0)