Skip to content

Commit 05c9176

Browse files
authored
Add Cpp Context Traits to Completions Prompt (#12821)
- Move related files code to its own module - Refactor related files provider code to enable unit testing - Add Cpp context traits to completions prompt
1 parent f09715f commit 05c9176

File tree

4 files changed

+512
-82
lines changed

4 files changed

+512
-82
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All Rights Reserved.
3+
* See 'LICENSE' in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
'use strict';
6+
7+
import * as vscode from 'vscode';
8+
import * as util from '../common';
9+
import * as telemetry from '../telemetry';
10+
import { ChatContextResult, GetIncludesResult } from './client';
11+
import { getActiveClient } from './extension';
12+
13+
let isRelatedFilesApiEnabled: boolean | undefined;
14+
15+
export interface CopilotTrait {
16+
name: string;
17+
value: string;
18+
includeInPrompt?: boolean;
19+
promptTextOverride?: string;
20+
}
21+
22+
export interface CopilotApi {
23+
registerRelatedFilesProvider(
24+
providerId: { extensionId: string; languageId: string },
25+
callback: (
26+
uri: vscode.Uri,
27+
context: { flags: Record<string, unknown> },
28+
cancellationToken: vscode.CancellationToken
29+
) => Promise<{ entries: vscode.Uri[]; traits?: CopilotTrait[] }>
30+
): Disposable;
31+
}
32+
33+
export async function registerRelatedFilesProvider(): Promise<void> {
34+
if (!await getIsRelatedFilesApiEnabled()) {
35+
return;
36+
}
37+
38+
const api = await getCopilotApi();
39+
if (util.extensionContext && api) {
40+
try {
41+
for (const languageId of ['c', 'cpp', 'cuda-cpp']) {
42+
api.registerRelatedFilesProvider(
43+
{ extensionId: util.extensionContext.extension.id, languageId },
44+
async (_uri: vscode.Uri, context: { flags: Record<string, unknown> }, token: vscode.CancellationToken) => {
45+
46+
const getIncludesHandler = async () => (await getIncludesWithCancellation(1, token))?.includedFiles.map(file => vscode.Uri.file(file)) ?? [];
47+
const getTraitsHandler = async () => {
48+
const chatContext: ChatContextResult | undefined = await (getActiveClient().getChatContext(token) ?? undefined);
49+
50+
if (!chatContext) {
51+
return undefined;
52+
}
53+
54+
let traits: CopilotTrait[] = [
55+
{ name: "language", value: chatContext.language, includeInPrompt: true, promptTextOverride: `The language is ${chatContext.language}.` },
56+
{ name: "compiler", value: chatContext.compiler, includeInPrompt: true, promptTextOverride: `This project compiles using ${chatContext.compiler}.` },
57+
{ name: "standardVersion", value: chatContext.standardVersion, includeInPrompt: true, promptTextOverride: `This project uses the ${chatContext.standardVersion} language standard.` },
58+
{ name: "targetPlatform", value: chatContext.targetPlatform, includeInPrompt: true, promptTextOverride: `This build targets ${chatContext.targetPlatform}.` },
59+
{ name: "targetArchitecture", value: chatContext.targetArchitecture, includeInPrompt: true, promptTextOverride: `This build targets ${chatContext.targetArchitecture}.` }
60+
];
61+
62+
const excludeTraits = context.flags.copilotcppExcludeTraits as string[] ?? [];
63+
traits = traits.filter(trait => !excludeTraits.includes(trait.name));
64+
65+
return traits.length > 0 ? traits : undefined;
66+
};
67+
68+
// Call both handlers in parallel
69+
const traitsPromise = ((context.flags.copilotcppTraits as boolean) ?? false) ? getTraitsHandler() : Promise.resolve(undefined);
70+
const includesPromise = getIncludesHandler();
71+
72+
return { entries: await includesPromise, traits: await traitsPromise };
73+
}
74+
);
75+
}
76+
} catch {
77+
console.log("Failed to register Copilot related files provider.");
78+
}
79+
}
80+
}
81+
82+
export async function registerRelatedFilesCommands(commandDisposables: vscode.Disposable[], enabled: boolean): Promise<void> {
83+
if (await getIsRelatedFilesApiEnabled()) {
84+
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.getIncludes', enabled ? (maxDepth: number) => getIncludes(maxDepth) : () => Promise.resolve()));
85+
}
86+
}
87+
88+
async function getIncludesWithCancellation(maxDepth: number, token: vscode.CancellationToken): Promise<GetIncludesResult> {
89+
const activeClient = getActiveClient();
90+
const includes = await activeClient.getIncludes(maxDepth, token);
91+
const wksFolder = activeClient.RootUri?.toString();
92+
93+
if (!wksFolder) {
94+
return includes;
95+
}
96+
97+
includes.includedFiles = includes.includedFiles.filter(header => vscode.Uri.file(header).toString().startsWith(wksFolder));
98+
return includes;
99+
}
100+
101+
async function getIncludes(maxDepth: number): Promise<GetIncludesResult> {
102+
const tokenSource = new vscode.CancellationTokenSource();
103+
try {
104+
const includes = await getIncludesWithCancellation(maxDepth, tokenSource.token);
105+
return includes;
106+
} finally {
107+
tokenSource.dispose();
108+
}
109+
}
110+
111+
async function getIsRelatedFilesApiEnabled(): Promise<boolean> {
112+
if (isRelatedFilesApiEnabled === undefined) {
113+
isRelatedFilesApiEnabled = await telemetry.isExperimentEnabled("CppToolsRelatedFilesApi");
114+
}
115+
116+
return isRelatedFilesApiEnabled;
117+
}
118+
119+
export async function getCopilotApi(): Promise<CopilotApi | undefined> {
120+
const copilotExtension = vscode.extensions.getExtension<CopilotApi>('github.copilot');
121+
if (!copilotExtension) {
122+
return undefined;
123+
}
124+
125+
if (!copilotExtension.isActive) {
126+
try {
127+
return await copilotExtension.activate();
128+
} catch {
129+
return undefined;
130+
}
131+
} else {
132+
return copilotExtension.exports;
133+
}
134+
}

Extension/src/LanguageServer/extension.ts

Lines changed: 6 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ import * as util from '../common';
2020
import { getCrashCallStacksChannel } from '../logger';
2121
import { PlatformInformation } from '../platform';
2222
import * as telemetry from '../telemetry';
23-
import { Client, DefaultClient, DoxygenCodeActionCommandArguments, GetIncludesResult, openFileVersions } from './client';
23+
import { Client, DefaultClient, DoxygenCodeActionCommandArguments, openFileVersions } from './client';
2424
import { ClientCollection } from './clientCollection';
2525
import { CodeActionDiagnosticInfo, CodeAnalysisDiagnosticIdentifiersAndUri, codeAnalysisAllFixes, codeAnalysisCodeToFixes, codeAnalysisFileToCodeActions } from './codeAnalysis';
26+
import { registerRelatedFilesCommands, registerRelatedFilesProvider } from './copilotProviders';
2627
import { CppBuildTaskProvider } from './cppBuildTaskProvider';
2728
import { getCustomConfigProviders } from './customProviders';
2829
import { getLanguageConfig } from './languageConfig';
@@ -33,24 +34,6 @@ import { CppSettings } from './settings';
3334
import { LanguageStatusUI, getUI } from './ui';
3435
import { makeLspRange, rangeEquals, showInstallCompilerWalkthrough } from './utils';
3536

36-
interface CopilotTrait {
37-
name: string;
38-
value: string;
39-
includeInPrompt?: boolean;
40-
promptTextOverride?: string;
41-
}
42-
43-
interface CopilotApi {
44-
registerRelatedFilesProvider(
45-
providerId: { extensionId: string; languageId: string },
46-
callback: (
47-
uri: vscode.Uri,
48-
context: { flags: Record<string, unknown> },
49-
cancellationToken: vscode.CancellationToken
50-
) => Promise<{ entries: vscode.Uri[]; traits?: CopilotTrait[] }>
51-
): Disposable;
52-
}
53-
5437
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
5538
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
5639
export const CppSourceStr: string = "C/C++";
@@ -201,8 +184,7 @@ export async function activate(): Promise<void> {
201184

202185
void clients.ActiveClient.ready.then(() => intervalTimer = global.setInterval(onInterval, 2500));
203186

204-
const isRelatedFilesApiEnabled = await telemetry.isExperimentEnabled("CppToolsRelatedFilesApi");
205-
registerCommands(true, isRelatedFilesApiEnabled);
187+
await registerCommands(true);
206188

207189
vscode.tasks.onDidStartTask(() => getActiveClient().PauseCodeAnalysis());
208190

@@ -274,22 +256,7 @@ export async function activate(): Promise<void> {
274256
disposables.push(tool);
275257
}
276258

277-
if (isRelatedFilesApiEnabled) {
278-
const api = await getCopilotApi();
279-
if (util.extensionContext && api) {
280-
try {
281-
for (const languageId of ['c', 'cpp', 'cuda-cpp']) {
282-
api.registerRelatedFilesProvider(
283-
{ extensionId: util.extensionContext.extension.id, languageId },
284-
async (_uri: vscode.Uri, _context: { flags: Record<string, unknown> }, token: vscode.CancellationToken) =>
285-
({ entries: (await getIncludesWithCancellation(1, token))?.includedFiles.map(file => vscode.Uri.file(file)) ?? [] })
286-
);
287-
}
288-
} catch {
289-
console.log("Failed to register Copilot related files provider.");
290-
}
291-
}
292-
}
259+
await registerRelatedFilesProvider();
293260
}
294261

295262
export function updateLanguageConfigurations(): void {
@@ -386,7 +353,7 @@ function onInterval(): void {
386353
/**
387354
* registered commands
388355
*/
389-
export function registerCommands(enabled: boolean, isRelatedFilesApiEnabled: boolean): void {
356+
export async function registerCommands(enabled: boolean): Promise<void> {
390357
commandDisposables.forEach(d => d.dispose());
391358
commandDisposables.length = 0;
392359
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.SwitchHeaderSource', enabled ? onSwitchHeaderSource : onDisabledCommand));
@@ -445,9 +412,7 @@ export function registerCommands(enabled: boolean, isRelatedFilesApiEnabled: boo
445412
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExtractToMemberFunction', enabled ? () => onExtractToFunction(false, true) : onDisabledCommand));
446413
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExpandSelection', enabled ? (r: Range) => onExpandSelection(r) : onDisabledCommand));
447414

448-
if (!isRelatedFilesApiEnabled) {
449-
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.getIncludes', enabled ? (maxDepth: number) => getIncludes(maxDepth) : () => Promise.resolve()));
450-
}
415+
await registerRelatedFilesCommands(commandDisposables, enabled);
451416
}
452417

453418
function onDisabledCommand() {
@@ -1412,42 +1377,3 @@ export async function preReleaseCheck(): Promise<void> {
14121377
}
14131378
}
14141379
}
1415-
1416-
export async function getIncludesWithCancellation(maxDepth: number, token: vscode.CancellationToken): Promise<GetIncludesResult> {
1417-
const includes = await clients.ActiveClient.getIncludes(maxDepth, token);
1418-
const wksFolder = clients.ActiveClient.RootUri?.toString();
1419-
1420-
if (!wksFolder) {
1421-
return includes;
1422-
}
1423-
1424-
includes.includedFiles = includes.includedFiles.filter(header => vscode.Uri.file(header).toString().startsWith(wksFolder));
1425-
return includes;
1426-
}
1427-
1428-
async function getIncludes(maxDepth: number): Promise<GetIncludesResult> {
1429-
const tokenSource = new vscode.CancellationTokenSource();
1430-
try {
1431-
const includes = await getIncludesWithCancellation(maxDepth, tokenSource.token);
1432-
return includes;
1433-
} finally {
1434-
tokenSource.dispose();
1435-
}
1436-
}
1437-
1438-
async function getCopilotApi(): Promise<CopilotApi | undefined> {
1439-
const copilotExtension = vscode.extensions.getExtension<CopilotApi>('github.copilot');
1440-
if (!copilotExtension) {
1441-
return undefined;
1442-
}
1443-
1444-
if (!copilotExtension.isActive) {
1445-
try {
1446-
return await copilotExtension.activate();
1447-
} catch {
1448-
return undefined;
1449-
}
1450-
} else {
1451-
return copilotExtension.exports;
1452-
}
1453-
}

Extension/src/main.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<CppToo
146146
if (shouldActivateLanguageServer) {
147147
await LanguageServer.activate();
148148
} else if (isIntelliSenseEngineDisabled) {
149-
const isRelatedFilesApiEnabled = await Telemetry.isExperimentEnabled("CppToolsRelatedFilesApi");
150-
LanguageServer.registerCommands(false, isRelatedFilesApiEnabled);
149+
await LanguageServer.registerCommands(false);
151150
// The check here for isIntelliSenseEngineDisabled avoids logging
152151
// the message on old Macs that we've already displayed a warning for.
153152
log(localize("intellisense.disabled", "intelliSenseEngine is disabled"));

0 commit comments

Comments
 (0)