diff --git a/Extension/.vscode/settings.json b/Extension/.vscode/settings.json index 969f4c40dc..7c60adca74 100644 --- a/Extension/.vscode/settings.json +++ b/Extension/.vscode/settings.json @@ -37,7 +37,7 @@ }, "[typescript]": { "editor.tabSize": 4, - "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.defaultFormatter": "vscode.typescript-language-features", "editor.formatOnSave": true, "files.insertFinalNewline": true, "editor.codeActionsOnSave": { diff --git a/Extension/package.json b/Extension/package.json index d95c577d0c..47dacea175 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -38,7 +38,8 @@ "Snippets" ], "enabledApiProposals": [ - "terminalDataWriteEvent" + "terminalDataWriteEvent", + "lmTools" ], "capabilities": { "untrustedWorkspaces": { @@ -6440,6 +6441,19 @@ "description": "%c_cpp.codeActions.refactor.extract.function.description%" } } + ], + "languageModelTools": [ + { + "id": "cpptools-lmtool-configuration", + "name": "cpp", + "displayName": "%c_cpp.languageModelTools.configuration.displayName%", + "canBeInvokedManually": true, + "userDescription": "%c_cpp.languageModelTools.configuration.userDescription%", + "modelDescription": "For the active C or C++ file, this tool provides: the language (C, C++, or CUDA), the language standard version (such as C++11, C++14, C++17, or C++20), the compiler (such as GCC, Clang, or MSVC), the target platform (such as x86, x64, or ARM), and the target architecture (such as 32-bit or 64-bit).", + "icon": "$(file-code)", + "parametersSchema": {}, + "when": "(config.C_Cpp.experimentalFeatures =~ /^[eE]nabled$/)" + } ] }, "scripts": { diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 2768013971..7f784f7c99 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -1006,5 +1006,7 @@ "c_cpp.configuration.refactoring.includeHeader.markdownDescription": "Controls whether to include the header file of a refactored function/symbol to its corresponding source file when doing a refactoring action, such as create declaration/definition.", "c_cpp.configuration.refactoring.includeHeader.always.description": "Always include the header file if it is not included explicitly in its source file.", "c_cpp.configuration.refactoring.includeHeader.ifNeeded.description": "Only include the header file if it is not included explicitly in its source file or through implicit inclusion.", - "c_cpp.configuration.refactoring.includeHeader.never.description": "Never include the header file." + "c_cpp.configuration.refactoring.includeHeader.never.description": "Never include the header file.", + "c_cpp.languageModelTools.configuration.displayName": "C/C++ configuration", + "c_cpp.languageModelTools.configuration.userDescription": "Configuration of the active C or C++ file, like language standard version and target platform." } diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index e8dc911259..79463dd11d 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -27,7 +27,7 @@ import * as fs from 'fs'; import * as os from 'os'; import { SourceFileConfiguration, SourceFileConfigurationItem, Version, WorkspaceBrowseConfiguration } from 'vscode-cpptools'; import { IntelliSenseStatus, Status } from 'vscode-cpptools/out/testApi'; -import { CloseAction, DidOpenTextDocumentParams, ErrorAction, LanguageClientOptions, NotificationType, Position, Range, RequestType, TextDocumentIdentifier, TextDocumentPositionParams } from 'vscode-languageclient'; +import { CloseAction, DidOpenTextDocumentParams, ErrorAction, LanguageClientOptions, NotificationType, Position, Range, RequestType, ResponseError, TextDocumentIdentifier, TextDocumentPositionParams } from 'vscode-languageclient'; import { LanguageClient, ServerOptions } from 'vscode-languageclient/node'; import * as nls from 'vscode-nls'; import { DebugConfigurationProvider } from '../Debugger/configurationProvider'; @@ -58,12 +58,12 @@ import { cachedEditorConfigSettings, getEditorConfigSettings } from './editorCon import { CppSourceStr, clients, configPrefix, updateLanguageConfigurations, usesCrashHandler, watchForCrashes } from './extension'; import { LocalizeStringParams, getLocaleId, getLocalizedString } from './localization'; import { PersistentFolderState, PersistentWorkspaceState } from './persistentState'; -import { createProtocolFilter } from './protocolFilter'; +import { RequestCancelled, ServerCancelled, createProtocolFilter } from './protocolFilter'; import * as refs from './references'; import { CppSettings, OtherSettings, SettingsParams, WorkspaceFolderSettingsParams } from './settings'; import { SettingsTracker } from './settingsTracker'; import { ConfigurationType, LanguageStatusUI, getUI } from './ui'; -import { handleChangedFromCppToC, makeLspRange, makeVscodeLocation, makeVscodeRange } from './utils'; +import { handleChangedFromCppToC, makeLspRange, makeVscodeLocation, makeVscodeRange, withCancellation } from './utils'; import minimatch = require("minimatch"); function deepCopy(obj: any) { @@ -542,6 +542,14 @@ interface GetIncludesResult { includedFiles: string[]; } +export interface ChatContextResult { + language: string; + standardVersion: string; + compiler: string; + targetPlatform: string; + targetArchitecture: string; +} + // Requests const PreInitializationRequest: RequestType = new RequestType('cpptools/preinitialize'); const InitializationRequest: RequestType = new RequestType('cpptools/initialize'); @@ -562,6 +570,7 @@ const GoToDirectiveInGroupRequest: RequestType = new RequestType('cpptools/generateDoxygenComment'); const ChangeCppPropertiesRequest: RequestType = new RequestType('cpptools/didChangeCppProperties'); const IncludesRequest: RequestType = new RequestType('cpptools/getIncludes'); +const CppContextRequest: RequestType = new RequestType('cpptools/getChatContext'); // Notifications to the server const DidOpenNotification: NotificationType = new NotificationType('textDocument/didOpen'); @@ -792,6 +801,7 @@ export interface Client { setShowConfigureIntelliSenseButton(show: boolean): void; addTrustedCompiler(path: string): Promise; getIncludes(maxDepth: number): Promise; + getChatContext(token: vscode.CancellationToken): Promise; } export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client { @@ -2234,6 +2244,25 @@ export class DefaultClient implements Client { return this.languageClient.sendRequest(IncludesRequest, params); } + public async getChatContext(token: vscode.CancellationToken): Promise { + await withCancellation(this.ready, token); + let result: ChatContextResult; + try { + result = await this.languageClient.sendRequest(CppContextRequest, null, token); + } catch (e: any) { + if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) { + throw new vscode.CancellationError(); + } + + throw e; + } + if (token.isCancellationRequested) { + throw new vscode.CancellationError(); + } + + return result; + } + /** * a Promise that can be awaited to know when it's ok to proceed. * @@ -4119,4 +4148,5 @@ class NullClient implements Client { setShowConfigureIntelliSenseButton(show: boolean): void { } addTrustedCompiler(path: string): Promise { return Promise.resolve(); } getIncludes(): Promise { return Promise.resolve({} as GetIncludesResult); } + getChatContext(token: vscode.CancellationToken): Promise { return Promise.resolve({} as ChatContextResult); } } diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 5c656ad6b7..470ca51fe5 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -26,6 +26,7 @@ import { CodeActionDiagnosticInfo, CodeAnalysisDiagnosticIdentifiersAndUri, code import { CppBuildTaskProvider } from './cppBuildTaskProvider'; import { getCustomConfigProviders } from './customProviders'; import { getLanguageConfig } from './languageConfig'; +import { CppConfigurationLanguageModelTool } from './lmTool'; import { PersistentState } from './persistentState'; import { NodeType, TreeNode } from './referencesModel'; import { CppSettings } from './settings'; @@ -248,6 +249,11 @@ export async function activate(): Promise { clients.timeTelemetryCollector.setFirstFile(activeEditor.document.uri); activeDocument = activeEditor.document; } + + if (util.extensionContext && new CppSettings().experimentalFeatures) { + const tool = vscode.lm.registerTool('cpptools-lmtool-configuration', new CppConfigurationLanguageModelTool()); + disposables.push(tool); + } } export function updateLanguageConfigurations(): void { diff --git a/Extension/src/LanguageServer/lmTool.ts b/Extension/src/LanguageServer/lmTool.ts new file mode 100644 index 0000000000..f643c63714 --- /dev/null +++ b/Extension/src/LanguageServer/lmTool.ts @@ -0,0 +1,102 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +import * as vscode from 'vscode'; +import { localize } from 'vscode-nls'; +import * as util from '../common'; +import * as logger from '../logger'; +import * as telemetry from '../telemetry'; +import { ChatContextResult } from './client'; +import { getClients } from './extension'; + +const knownValues: { [Property in keyof ChatContextResult]?: { [id: string]: string } } = { + language: { + 'c': 'C', + 'cpp': 'C++', + 'cuda-cpp': 'CUDA C++' + }, + compiler: { + 'msvc': 'MSVC', + 'clang': 'Clang', + 'gcc': 'GCC' + }, + standardVersion: { + 'c++98': 'C++98', + 'c++03': 'C++03', + 'c++11': 'C++11', + 'c++14': 'C++14', + 'c++17': 'C++17', + 'c++20': 'C++20', + 'c++23': 'C++23', + 'c90': "C90", + 'c99': "C99", + 'c11': "C11", + 'c17': "C17", + 'c23': "C23" + }, + targetPlatform: { + 'windows': 'Windows', + 'Linux': 'Linux', + 'macos': 'macOS' + } +}; + +class StringLanguageModelToolResult implements vscode.LanguageModelToolResult { + public constructor(public readonly value: string) { } + public toString(): string { return this.value; } +} + +export class CppConfigurationLanguageModelTool implements vscode.LanguageModelTool { + public async invoke(_parameters: any, token: vscode.CancellationToken): Promise { + return new StringLanguageModelToolResult(await this.getContext(token)); + } + + private async getContext(token: vscode.CancellationToken): Promise { + try { + const currentDoc = vscode.window.activeTextEditor?.document; + if (!currentDoc || (!util.isCpp(currentDoc) && !util.isHeaderFile(currentDoc.uri))) { + return 'The active document is not a C, C++, or CUDA file.'; + } + + const chatContext: ChatContextResult | undefined = await (getClients()?.ActiveClient?.getChatContext(token) ?? undefined); + if (!chatContext) { + return 'No configuration information is available for the active document.'; + } + + telemetry.logLanguageModelToolEvent( + 'cpp', + { + "language": chatContext.language, + "compiler": chatContext.compiler, + "standardVersion": chatContext.standardVersion, + "targetPlatform": chatContext.targetPlatform, + "targetArchitecture": chatContext.targetArchitecture + }); + + for (const key in knownValues) { + const knownKey = key as keyof ChatContextResult; + if (knownValues[knownKey] && chatContext[knownKey]) { + chatContext[knownKey] = knownValues[knownKey][chatContext[knownKey]] || chatContext[knownKey]; + } + } + + return `The user is working on a ${chatContext.language} project. The project uses language version ${chatContext.standardVersion}, compiles using the ${chatContext.compiler} compiler, targets the ${chatContext.targetPlatform} platform, and targets the ${chatContext.targetArchitecture} architecture.`; + } + catch { + await this.reportError(); + return ""; + } + } + + private async reportError(): Promise { + try { + logger.getOutputChannelLogger().appendLine(localize("copilot.cppcontext.error", "Error while retrieving the #cpp context.")); + } + catch { + // Intentionally swallow any exception. + } + } +} diff --git a/Extension/src/LanguageServer/utils.ts b/Extension/src/LanguageServer/utils.ts index da3d29b693..e8c8073ee1 100644 --- a/Extension/src/LanguageServer/utils.ts +++ b/Extension/src/LanguageServer/utils.ts @@ -1,101 +1,114 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All Rights Reserved. - * See 'LICENSE' in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; -import * as os from 'os'; -import * as vscode from 'vscode'; -import { Range } from 'vscode-languageclient'; -import { SessionState } from '../sessionState'; -import { Location, TextEdit } from './commonTypes'; -import { CppSettings } from './settings'; - -export function makeLspRange(vscRange: vscode.Range): Range { - return { - start: { line: vscRange.start.line, character: vscRange.start.character }, - end: { line: vscRange.end.line, character: vscRange.end.character } - }; -} - -export function makeVscodeRange(cpptoolsRange: Range): vscode.Range { - return new vscode.Range(cpptoolsRange.start.line, cpptoolsRange.start.character, cpptoolsRange.end.line, cpptoolsRange.end.character); -} - -export function makeVscodeLocation(cpptoolsLocation: Location): vscode.Location { - return new vscode.Location(vscode.Uri.parse(cpptoolsLocation.uri), makeVscodeRange(cpptoolsLocation.range)); -} - -export function makeVscodeTextEdits(cpptoolsTextEdits: TextEdit[]): vscode.TextEdit[] { - return cpptoolsTextEdits.map(textEdit => new vscode.TextEdit(makeVscodeRange(textEdit.range), textEdit.newText)); -} - -export function rangeEquals(range1: vscode.Range | Range, range2: vscode.Range | Range): boolean { - return range1.start.line === range2.start.line && range1.start.character === range2.start.character && - range1.end.line === range2.end.line && range1.end.character === range2.end.character; -} - -// Check this before attempting to switch a document from C to C++. -export function shouldChangeFromCToCpp(document: vscode.TextDocument): boolean { - if (document.fileName.endsWith(".C") || document.fileName.endsWith(".H")) { - const cppSettings: CppSettings = new CppSettings(); - if (cppSettings.autoAddFileAssociations) { - return !docsChangedFromCppToC.has(document.fileName); - } - // We could potentially add a new setting to enable switching to cpp even when files.associations isn't changed. - } - return false; -} - -// Call this before changing from C++ to C. -export function handleChangedFromCppToC(document: vscode.TextDocument): void { - if (shouldChangeFromCToCpp(document)) { - docsChangedFromCppToC.add(document.fileName); - } -} - -export function showInstallCompilerWalkthrough(): void { - // Because we need to conditionally enable/disable steps to alter their contents, - // we need to determine which step is actually visible. If the steps change, this - // logic will need to change to reflect them. - enum Step { - Activation = 'awaiting.activation', - NoCompilers = 'no.compilers.found', - Verify = 'verify.compiler' - } - - const step = (() => { - if (!SessionState.scanForCompilersDone.get()) { - return Step.Activation; - } else if (!SessionState.trustedCompilerFound.get()) { - return Step.NoCompilers; - } else { - return Step.Verify; - } - })(); - - const platform = (() => { - switch (os.platform()) { - case 'win32': return 'windows'; - case 'darwin': return 'mac'; - default: return 'linux'; - } - })(); - - const version = (platform === 'windows') ? SessionState.windowsVersion.get() : ''; - - const index = `ms-vscode.cpptools#${step}.${platform}${version}`; - - void vscode.commands.executeCommand( - 'workbench.action.openWalkthrough', - { category: 'ms-vscode.cpptools#cppWelcome', step: index }, - false) - // Run it twice for now because of VS Code bug #187958 - .then(() => vscode.commands.executeCommand( - "workbench.action.openWalkthrough", - { category: 'ms-vscode.cpptools#cppWelcome', step: index }, - false) - ); - return; -} - -const docsChangedFromCppToC: Set = new Set(); +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; +import * as os from 'os'; +import * as vscode from 'vscode'; +import { Range } from 'vscode-languageclient'; +import { SessionState } from '../sessionState'; +import { Location, TextEdit } from './commonTypes'; +import { CppSettings } from './settings'; + +export function makeLspRange(vscRange: vscode.Range): Range { + return { + start: { line: vscRange.start.line, character: vscRange.start.character }, + end: { line: vscRange.end.line, character: vscRange.end.character } + }; +} + +export function makeVscodeRange(cpptoolsRange: Range): vscode.Range { + return new vscode.Range(cpptoolsRange.start.line, cpptoolsRange.start.character, cpptoolsRange.end.line, cpptoolsRange.end.character); +} + +export function makeVscodeLocation(cpptoolsLocation: Location): vscode.Location { + return new vscode.Location(vscode.Uri.parse(cpptoolsLocation.uri), makeVscodeRange(cpptoolsLocation.range)); +} + +export function makeVscodeTextEdits(cpptoolsTextEdits: TextEdit[]): vscode.TextEdit[] { + return cpptoolsTextEdits.map(textEdit => new vscode.TextEdit(makeVscodeRange(textEdit.range), textEdit.newText)); +} + +export function rangeEquals(range1: vscode.Range | Range, range2: vscode.Range | Range): boolean { + return range1.start.line === range2.start.line && range1.start.character === range2.start.character && + range1.end.line === range2.end.line && range1.end.character === range2.end.character; +} + +// Check this before attempting to switch a document from C to C++. +export function shouldChangeFromCToCpp(document: vscode.TextDocument): boolean { + if (document.fileName.endsWith(".C") || document.fileName.endsWith(".H")) { + const cppSettings: CppSettings = new CppSettings(); + if (cppSettings.autoAddFileAssociations) { + return !docsChangedFromCppToC.has(document.fileName); + } + // We could potentially add a new setting to enable switching to cpp even when files.associations isn't changed. + } + return false; +} + +// Call this before changing from C++ to C. +export function handleChangedFromCppToC(document: vscode.TextDocument): void { + if (shouldChangeFromCToCpp(document)) { + docsChangedFromCppToC.add(document.fileName); + } +} + +export function showInstallCompilerWalkthrough(): void { + // Because we need to conditionally enable/disable steps to alter their contents, + // we need to determine which step is actually visible. If the steps change, this + // logic will need to change to reflect them. + enum Step { + Activation = 'awaiting.activation', + NoCompilers = 'no.compilers.found', + Verify = 'verify.compiler' + } + + const step = (() => { + if (!SessionState.scanForCompilersDone.get()) { + return Step.Activation; + } else if (!SessionState.trustedCompilerFound.get()) { + return Step.NoCompilers; + } else { + return Step.Verify; + } + })(); + + const platform = (() => { + switch (os.platform()) { + case 'win32': return 'windows'; + case 'darwin': return 'mac'; + default: return 'linux'; + } + })(); + + const version = (platform === 'windows') ? SessionState.windowsVersion.get() : ''; + + const index = `ms-vscode.cpptools#${step}.${platform}${version}`; + + void vscode.commands.executeCommand( + 'workbench.action.openWalkthrough', + { category: 'ms-vscode.cpptools#cppWelcome', step: index }, + false) + // Run it twice for now because of VS Code bug #187958 + .then(() => vscode.commands.executeCommand( + "workbench.action.openWalkthrough", + { category: 'ms-vscode.cpptools#cppWelcome', step: index }, + false) + ); + return; +} + +const docsChangedFromCppToC: Set = new Set(); + +export async function withCancellation(promise: Promise, token: vscode.CancellationToken): Promise { + return new Promise((resolve, reject) => { + const disposable = token.onCancellationRequested(() => reject(new vscode.CancellationError())); + promise.then((value) => { + disposable.dispose(); + resolve(value); + }, (reason) => { + disposable.dispose(); + reject(reason); + }); + }); +} diff --git a/Extension/src/telemetry.ts b/Extension/src/telemetry.ts index dc6a1c199f..600ffa4c45 100644 --- a/Extension/src/telemetry.ts +++ b/Extension/src/telemetry.ts @@ -123,6 +123,20 @@ export function logLanguageServerEvent(eventName: string, properties?: Record, metrics?: Record): void { + const sendTelemetry = () => { + if (experimentationTelemetry) { + const eventNamePrefix: string = "C_Cpp/Copilot/Chat/Tool/"; + experimentationTelemetry.sendTelemetryEvent(eventNamePrefix + eventName, properties, metrics); + } + }; + + if (is.promise(initializationPromise)) { + return void initializationPromise.catch(logAndReturn.undefined).then(sendTelemetry).catch(logAndReturn.undefined); + } + sendTelemetry(); +} + function getPackageInfo(): IPackageInfo { return { name: util.packageJson.publisher + "." + util.packageJson.name,