Skip to content

Commit ce1c8fe

Browse files
committed
Support A/B Compiler Arguments Traits
- Depends on cpptools' update to provide ProjectContextResult. - Added the following new traits - intellisense: compiler information disclaimer. - intellisenseBegin: to note the beginning of IntelliSense information. - compilerArguments: a list of compiler command arguments that could affect Copilot generating completions. - directAsks: direct asking Copilot to do something instead of providing an argument. - intellisenseEnd: to note the end of IntelliSense information. - A/B Experimental flags - copilotcppTraits: boolean flag to enable cpp traits - copilotcppExcludeTraits: string array to exclude individual trait, i.e., compilerArguments. - copilotcppMsvcCompilerArgumentFilter: regex string to match compiler arguments for GCC. - copilotcppClangCompilerArgumentFilter: regex string to match compiler arguments for Clang. - copilotcppGccCompilerArgumentFilter: regex string to match compiler arguments for MSVC. - copilotcppCompilerArgumentDirectAskMap: a stringify map string to map arguments to direct ask statements.
1 parent c9cae0b commit ce1c8fe

File tree

7 files changed

+613
-90
lines changed

7 files changed

+613
-90
lines changed

Extension/src/LanguageServer/client.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,19 @@ export interface ChatContextResult {
541541
targetArchitecture: string;
542542
}
543543

544+
export interface FileContextResult {
545+
compilerArguments: string[];
546+
}
547+
548+
export interface ProjectContextResult {
549+
language: string;
550+
standardVersion: string;
551+
compiler: string;
552+
targetPlatform: string;
553+
targetArchitecture: string;
554+
fileContext: FileContextResult;
555+
}
556+
544557
// Requests
545558
const PreInitializationRequest: RequestType<void, string, void> = new RequestType<void, string, void>('cpptools/preinitialize');
546559
const InitializationRequest: RequestType<CppInitializationParams, void, void> = new RequestType<CppInitializationParams, void, void>('cpptools/initialize');
@@ -561,6 +574,7 @@ const GenerateDoxygenCommentRequest: RequestType<GenerateDoxygenCommentParams, G
561574
const ChangeCppPropertiesRequest: RequestType<CppPropertiesParams, void, void> = new RequestType<CppPropertiesParams, void, void>('cpptools/didChangeCppProperties');
562575
const IncludesRequest: RequestType<GetIncludesParams, GetIncludesResult, void> = new RequestType<GetIncludesParams, GetIncludesResult, void>('cpptools/getIncludes');
563576
const CppContextRequest: RequestType<void, ChatContextResult, void> = new RequestType<void, ChatContextResult, void>('cpptools/getChatContext');
577+
const ProjectContextRequest: RequestType<void, ProjectContextResult, void> = new RequestType<void, ProjectContextResult, void>('cpptools/getProjectContext');
564578

565579
// Notifications to the server
566580
const DidOpenNotification: NotificationType<DidOpenTextDocumentParams> = new NotificationType<DidOpenTextDocumentParams>('textDocument/didOpen');
@@ -792,6 +806,7 @@ export interface Client {
792806
addTrustedCompiler(path: string): Promise<void>;
793807
getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise<GetIncludesResult>;
794808
getChatContext(token: vscode.CancellationToken): Promise<ChatContextResult>;
809+
getProjectContext(token: vscode.CancellationToken): Promise<ProjectContextResult>;
795810
}
796811

797812
export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client {
@@ -2220,6 +2235,12 @@ export class DefaultClient implements Client {
22202235
() => this.languageClient.sendRequest(CppContextRequest, null, token), token);
22212236
}
22222237

2238+
public async getProjectContext(token: vscode.CancellationToken): Promise<ProjectContextResult> {
2239+
await withCancellation(this.ready, token);
2240+
return DefaultClient.withLspCancellationHandling(
2241+
() => this.languageClient.sendRequest(ProjectContextRequest, null, token), token);
2242+
}
2243+
22232244
/**
22242245
* a Promise that can be awaited to know when it's ok to proceed.
22252246
*
@@ -4123,4 +4144,5 @@ class NullClient implements Client {
41234144
addTrustedCompiler(path: string): Promise<void> { return Promise.resolve(); }
41244145
getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise<GetIncludesResult> { return Promise.resolve({} as GetIncludesResult); }
41254146
getChatContext(token: vscode.CancellationToken): Promise<ChatContextResult> { return Promise.resolve({} as ChatContextResult); }
4147+
getProjectContext(token: vscode.CancellationToken): Promise<ProjectContextResult> { return Promise.resolve({} as ProjectContextResult); }
41264148
}

Extension/src/LanguageServer/copilotProviders.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
import * as vscode from 'vscode';
88
import * as util from '../common';
9-
import { ChatContextResult, GetIncludesResult } from './client';
9+
import { GetIncludesResult } from './client';
1010
import { getActiveClient } from './extension';
11+
import { getProjectContext } from './lmTool';
1112

1213
export interface CopilotTrait {
1314
name: string;
@@ -38,19 +39,51 @@ export async function registerRelatedFilesProvider(): Promise<void> {
3839

3940
const getIncludesHandler = async () => (await getIncludesWithCancellation(1, token))?.includedFiles.map(file => vscode.Uri.file(file)) ?? [];
4041
const getTraitsHandler = async () => {
41-
const chatContext: ChatContextResult | undefined = await (getActiveClient().getChatContext(token) ?? undefined);
42+
const cppContext = await getProjectContext(context, token);
4243

43-
if (!chatContext) {
44+
if (!cppContext) {
4445
return undefined;
4546
}
4647

4748
let traits: CopilotTrait[] = [
48-
{ name: "language", value: chatContext.language, includeInPrompt: true, promptTextOverride: `The language is ${chatContext.language}.` },
49-
{ name: "compiler", value: chatContext.compiler, includeInPrompt: true, promptTextOverride: `This project compiles using ${chatContext.compiler}.` },
50-
{ name: "standardVersion", value: chatContext.standardVersion, includeInPrompt: true, promptTextOverride: `This project uses the ${chatContext.standardVersion} language standard.` },
51-
{ name: "targetPlatform", value: chatContext.targetPlatform, includeInPrompt: true, promptTextOverride: `This build targets ${chatContext.targetPlatform}.` },
52-
{ name: "targetArchitecture", value: chatContext.targetArchitecture, includeInPrompt: true, promptTextOverride: `This build targets ${chatContext.targetArchitecture}.` }
49+
{ name: "intellisense", value: 'intellisense', includeInPrompt: true, promptTextOverride: `IntelliSense is currently configured with the following compiler information. It reflects the active configuration, and the project may have more configurations targeting different platforms.` },
50+
{ name: "intellisenseBegin", value: 'Begin', includeInPrompt: true, promptTextOverride: `Beginning of IntelliSense information.` }
5351
];
52+
if (cppContext.language) {
53+
traits.push({ name: "language", value: cppContext.language, includeInPrompt: true, promptTextOverride: `The language is ${cppContext.language}.` });
54+
}
55+
if (cppContext.compiler) {
56+
traits.push({ name: "compiler", value: cppContext.compiler, includeInPrompt: true, promptTextOverride: `This project compiles using ${cppContext.compiler}.` });
57+
}
58+
if (cppContext.standardVersion) {
59+
traits.push({ name: "standardVersion", value: cppContext.standardVersion, includeInPrompt: true, promptTextOverride: `This project uses the ${cppContext.standardVersion} language standard.` });
60+
}
61+
if (cppContext.targetPlatform) {
62+
traits.push({ name: "targetPlatform", value: cppContext.targetPlatform, includeInPrompt: true, promptTextOverride: `This build targets ${cppContext.targetPlatform}.` });
63+
}
64+
if (cppContext.targetArchitecture) {
65+
traits.push({ name: "targetArchitecture", value: cppContext.targetArchitecture, includeInPrompt: true, promptTextOverride: `This build targets ${cppContext.targetArchitecture}.` });
66+
}
67+
let directAsks: string = '';
68+
if (cppContext.compilerArguments.length > 0) {
69+
// Example: JSON.stringify({'-fno-rtti': "Do not generate code using RTTI keywords."})
70+
const directAskMap: { [key: string]: string } = JSON.parse(context.flags.copilotcppCompilerArgumentDirectAskMap as string ?? '{}');
71+
const updatedArguments = cppContext.compilerArguments.filter(arg => {
72+
if (directAskMap[arg]) {
73+
directAsks += `${directAskMap[arg]} `;
74+
return false;
75+
}
76+
return true;
77+
});
78+
79+
const compilerArgumentsValue = updatedArguments.join(", ");
80+
traits.push({ name: "compilerArguments", value: compilerArgumentsValue, includeInPrompt: true, promptTextOverride: `The compiler arguments include: ${compilerArgumentsValue}.` });
81+
}
82+
if (directAsks) {
83+
traits.push({ name: "directAsks", value: directAsks, includeInPrompt: true, promptTextOverride: directAsks });
84+
}
85+
86+
traits.push({ name: "intellisenseEnd", value: 'End', includeInPrompt: true, promptTextOverride: `End of IntelliSense information.` });
5487

5588
const excludeTraits = context.flags.copilotcppExcludeTraits as string[] ?? [];
5689
traits = traits.filter(trait => !excludeTraits.includes(trait.name));

Extension/src/LanguageServer/lmTool.ts

Lines changed: 116 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,23 @@ import { localize } from 'vscode-nls';
99
import * as util from '../common';
1010
import * as logger from '../logger';
1111
import * as telemetry from '../telemetry';
12-
import { ChatContextResult } from './client';
12+
import { ChatContextResult, ProjectContextResult } from './client';
1313
import { getClients } from './extension';
14+
import { checkTime } from './utils';
1415

16+
const MSVC: string = 'MSVC';
17+
const Clang: string = 'Clang';
18+
const GCC: string = 'GCC';
1519
const knownValues: { [Property in keyof ChatContextResult]?: { [id: string]: string } } = {
1620
language: {
1721
'c': 'C',
1822
'cpp': 'C++',
1923
'cuda-cpp': 'CUDA C++'
2024
},
2125
compiler: {
22-
'msvc': 'MSVC',
23-
'clang': 'Clang',
24-
'gcc': 'GCC'
26+
'msvc': MSVC,
27+
'clang': Clang,
28+
'gcc': GCC
2529
},
2630
standardVersion: {
2731
'c++98': 'C++98',
@@ -44,6 +48,112 @@ const knownValues: { [Property in keyof ChatContextResult]?: { [id: string]: str
4448
}
4549
};
4650

51+
function formatChatContext(context: ChatContextResult | ProjectContextResult): void {
52+
type KnownKeys = 'language' | 'standardVersion' | 'compiler' | 'targetPlatform';
53+
for (const key in knownValues) {
54+
const knownKey = key as KnownKeys;
55+
if (knownValues[knownKey] && context[knownKey]) {
56+
// Clear the value if it's not in the known values.
57+
context[knownKey] = knownValues[knownKey][context[knownKey]] || "";
58+
}
59+
}
60+
}
61+
62+
export interface ProjectContext {
63+
language: string;
64+
standardVersion: string;
65+
compiler: string;
66+
targetPlatform: string;
67+
targetArchitecture: string;
68+
compilerArguments: string[];
69+
}
70+
71+
// Set these values for local testing purpose without involving control tower.
72+
const defaultCompilerArgumentFilters: { [id: string]: RegExp | undefined } = {
73+
MSVC: undefined, // Example: /^(\/std:.*|\/EHs-c-|\/GR-|\/await.*)$/,
74+
Clang: undefined,
75+
GCC: undefined // Example: /^(-std=.*|-fno-rtti|-fno-exceptions)$/
76+
};
77+
78+
function filterComplierArguments(compiler: string, compilerArguments: string[], context: { flags: Record<string, unknown> }): string[] {
79+
const defaultFilter: RegExp | undefined = defaultCompilerArgumentFilters[compiler] ?? undefined;
80+
let additionalFilter: RegExp | undefined;
81+
switch (compiler) {
82+
case MSVC:
83+
additionalFilter = context.flags.copilotcppMsvcCompilerArgumentFilter ? new RegExp(context.flags.copilotcppMsvcCompilerArgumentFilter as string) : undefined;
84+
break;
85+
case Clang:
86+
additionalFilter = context.flags.copilotcppClangCompilerArgumentFilter ? new RegExp(context.flags.copilotcppClangCompilerArgumentFilter as string) : undefined;
87+
break;
88+
case GCC:
89+
additionalFilter = context.flags.copilotcppGccCompilerArgumentFilter ? new RegExp(context.flags.copilotcppGccCompilerArgumentFilter as string) : undefined;
90+
break;
91+
}
92+
93+
return compilerArguments.filter(arg => defaultFilter?.test(arg) || additionalFilter?.test(arg));
94+
}
95+
96+
export async function getProjectContext(context: { flags: Record<string, unknown> }, token: vscode.CancellationToken): Promise<ProjectContext | undefined> {
97+
const telemetryProperties: Record<string, string> = {};
98+
try {
99+
const projectContext = await checkTime<ProjectContextResult | undefined>(async () => await getClients()?.ActiveClient?.getProjectContext(token) ?? undefined);
100+
telemetryProperties["time"] = projectContext.time.toString();
101+
if (!projectContext.result) {
102+
return undefined;
103+
}
104+
105+
formatChatContext(projectContext.result);
106+
107+
const result: ProjectContext = {
108+
language: projectContext.result.language,
109+
standardVersion: projectContext.result.standardVersion,
110+
compiler: projectContext.result.compiler,
111+
targetPlatform: projectContext.result.targetPlatform,
112+
targetArchitecture: projectContext.result.targetArchitecture,
113+
compilerArguments: []
114+
};
115+
116+
if (projectContext.result.language) {
117+
telemetryProperties["language"] = projectContext.result.language;
118+
}
119+
if (projectContext.result.compiler) {
120+
telemetryProperties["compiler"] = projectContext.result.compiler;
121+
}
122+
if (projectContext.result.standardVersion) {
123+
telemetryProperties["standardVersion"] = projectContext.result.standardVersion;
124+
}
125+
if (projectContext.result.targetPlatform) {
126+
telemetryProperties["targetPlatform"] = projectContext.result.targetPlatform;
127+
}
128+
if (projectContext.result.targetArchitecture) {
129+
telemetryProperties["targetArchitecture"] = projectContext.result.targetArchitecture;
130+
}
131+
telemetryProperties["compilerArgumentCount"] = projectContext.result.fileContext.compilerArguments.length.toString();
132+
// Telemtry to learn about the argument distribution. The filtered arguments are expected to be non-PII.
133+
if (projectContext.result.fileContext.compilerArguments.length) {
134+
const filteredCompilerArguments = filterComplierArguments(projectContext.result.compiler, projectContext.result.fileContext.compilerArguments, context);
135+
if (filteredCompilerArguments.length > 0) {
136+
telemetryProperties["filteredCompilerArguments"] = filteredCompilerArguments.join(', ');
137+
result.compilerArguments = filteredCompilerArguments;
138+
}
139+
}
140+
141+
return result;
142+
}
143+
catch {
144+
try {
145+
logger.getOutputChannelLogger().appendLine(localize("copilot.cppcontext.error", "Error while retrieving the project context."));
146+
}
147+
catch {
148+
// Intentionally swallow any exception.
149+
}
150+
telemetryProperties["error"] = "true";
151+
return undefined;
152+
} finally {
153+
telemetry.logLanguageModelToolEvent('Completions/tool', telemetryProperties);
154+
}
155+
}
156+
47157
export class CppConfigurationLanguageModelTool implements vscode.LanguageModelTool<void> {
48158
public async invoke(options: vscode.LanguageModelToolInvocationOptions<void>, token: vscode.CancellationToken): Promise<vscode.LanguageModelToolResult> {
49159
return new vscode.LanguageModelToolResult([
@@ -63,13 +173,7 @@ export class CppConfigurationLanguageModelTool implements vscode.LanguageModelTo
63173
return 'No configuration information is available for the active document.';
64174
}
65175

66-
for (const key in knownValues) {
67-
const knownKey = key as keyof ChatContextResult;
68-
if (knownValues[knownKey] && chatContext[knownKey]) {
69-
// Clear the value if it's not in the known values.
70-
chatContext[knownKey] = knownValues[knownKey][chatContext[knownKey]] || "";
71-
}
72-
}
176+
formatChatContext(chatContext);
73177

74178
let contextString = "";
75179
if (chatContext.language) {
@@ -100,7 +204,7 @@ export class CppConfigurationLanguageModelTool implements vscode.LanguageModelTo
100204
telemetryProperties["error"] = "true";
101205
return "";
102206
} finally {
103-
telemetry.logLanguageModelToolEvent('cpp', telemetryProperties);
207+
telemetry.logLanguageModelToolEvent('Chat/Tool/cpp', telemetryProperties);
104208
}
105209
}
106210

Extension/src/LanguageServer/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,9 @@ export async function withCancellation<T>(promise: Promise<T>, token: vscode.Can
112112
});
113113
});
114114
}
115+
116+
export async function checkTime<T>(fn: () => Promise<T>): Promise<{ result: T; time: number }> {
117+
const start = Date.now();
118+
const result = await fn();
119+
return { result, time: Date.now() - start };
120+
}

Extension/src/telemetry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export function logLanguageServerEvent(eventName: string, properties?: Record<st
126126
export function logLanguageModelToolEvent(eventName: string, properties?: Record<string, string>, metrics?: Record<string, number>): void {
127127
const sendTelemetry = () => {
128128
if (experimentationTelemetry) {
129-
const eventNamePrefix: string = "C_Cpp/Copilot/Chat/Tool/";
129+
const eventNamePrefix: string = "C_Cpp/Copilot/";
130130
experimentationTelemetry.sendTelemetryEvent(eventNamePrefix + eventName, properties, metrics);
131131
}
132132
};

0 commit comments

Comments
 (0)