diff --git a/Extension/package.json b/Extension/package.json index 4f1f849f95..c62ff41dd5 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -3313,6 +3313,17 @@ "default": false, "markdownDescription": "%c_cpp.configuration.addNodeAddonIncludePaths.markdownDescription%", "scope": "application" + }, + "C_Cpp.copilotHover": { + "type": "string", + "enum": [ + "default", + "enabled", + "disabled" + ], + "default": "default", + "markdownDescription": "%c_cpp.configuration.copilotHover.markdownDescription%", + "scope": "window" } } } diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 7f784f7c99..9238421594 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -768,6 +768,12 @@ "Markdown text between `` should not be translated or localized (they represent literal text) and the capitalization, spacing, and punctuation (including the ``) should not be altered." ] }, + "c_cpp.configuration.copilotHover.markdownDescription": { + "message": "If `enabled`, the hover tooltip will display an option to generate a summary of the symbol with Copilot. If `disabled`, the option will not be displayed.", + "comment": [ + "Markdown text between `` should not be translated or localized (they represent literal text) and the capitalization, spacing, and punctuation (including the ``) should not be altered." + ] + }, "c_cpp.configuration.renameRequiresIdentifier.markdownDescription": { "message": "If `true`, 'Rename Symbol' will require a valid C/C++ identifier.", "comment": [ diff --git a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts new file mode 100644 index 0000000000..018e2279dc --- /dev/null +++ b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts @@ -0,0 +1,149 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +import * as vscode from 'vscode'; +import { Position, ResponseError } from 'vscode-languageclient'; +import * as nls from 'vscode-nls'; +import { DefaultClient, GetCopilotHoverInfoParams, GetCopilotHoverInfoRequest } from '../client'; +import { RequestCancelled, ServerCancelled } from '../protocolFilter'; +import { CppSettings } from '../settings'; + +nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); +const localize: nls.LocalizeFunc = nls.loadMessageBundle(); + +export class CopilotHoverProvider implements vscode.HoverProvider { + private client: DefaultClient; + private currentDocument: vscode.TextDocument | undefined; + private currentPosition: vscode.Position | undefined; + private currentCancellationToken: vscode.CancellationToken | undefined; + private waiting: boolean = false; + private ready: boolean = false; + private cancelled: boolean = false; + private cancelledDocument: vscode.TextDocument | undefined; + private cancelledPosition: vscode.Position | undefined; + private content: string | undefined; + constructor(client: DefaultClient) { + this.client = client; + } + + public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + await this.client.ready; + + const settings: CppSettings = new CppSettings(vscode.workspace.getWorkspaceFolder(document.uri)?.uri); + if (settings.hover === "disabled") { + return undefined; + } + + const newHover = this.isNewHover(document, position); + if (newHover) { + this.reset(); + } + + // Wait for the main hover provider to finish and confirm it has content. + const hoverProvider = this.client.getHoverProvider(); + if (!await hoverProvider?.contentReady) { + return undefined; + } + + if (token.isCancellationRequested) { + throw new vscode.CancellationError(); + } + this.currentCancellationToken = token; + + if (!newHover) { + if (this.ready) { + const contentMarkdown = new vscode.MarkdownString(`$(sparkle) Copilot\n\n${this.content}`, true); + return new vscode.Hover(contentMarkdown); + } + if (this.waiting) { + const loadingMarkdown = new vscode.MarkdownString("$(sparkle) $(loading~spin)", true); + return new vscode.Hover(loadingMarkdown); + } + } + + this.currentDocument = document; + this.currentPosition = position; + const commandString = "$(sparkle) [" + localize("generate.copilot.description", "Generate Copilot summary") + "](command:C_Cpp.ShowCopilotHover \"" + localize("copilot.disclaimer", "AI-generated content may be incorrect.") + "\")"; + const commandMarkdown = new vscode.MarkdownString(commandString); + commandMarkdown.supportThemeIcons = true; + commandMarkdown.isTrusted = true; + return new vscode.Hover(commandMarkdown); + } + + public showWaiting(): void { + this.waiting = true; + } + + public showContent(content: string): void { + this.ready = true; + this.content = content; + } + + public getCurrentHoverDocument(): vscode.TextDocument | undefined { + return this.currentDocument; + } + + public getCurrentHoverPosition(): vscode.Position | undefined { + return this.currentPosition; + } + + public getCurrentHoverCancellationToken(): vscode.CancellationToken | undefined { + return this.currentCancellationToken; + } + + public async getRequestInfo(document: vscode.TextDocument, position: vscode.Position): Promise { + let requestInfo = ""; + const params: GetCopilotHoverInfoParams = { + textDocument: { uri: document.uri.toString() }, + position: Position.create(position.line, position.character) + }; + + await this.client.ready; + if (this.currentCancellationToken?.isCancellationRequested) { + throw new vscode.CancellationError(); + } + + try { + const response = await this.client.languageClient.sendRequest(GetCopilotHoverInfoRequest, params, this.currentCancellationToken); + requestInfo = response.content; + } catch (e: any) { + if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) { + throw new vscode.CancellationError(); + } + throw e; + } + + return requestInfo; + } + + public isCancelled(document: vscode.TextDocument, position: vscode.Position): boolean { + if (this.cancelled && this.cancelledDocument === document && this.cancelledPosition === position) { + // Cancellation is being acknowledged. + this.cancelled = false; + this.cancelledDocument = undefined; + this.cancelledPosition = undefined; + return true; + } + return false; + } + + public reset(): void { + // If there was a previous call, cancel it. + if (this.waiting) { + this.cancelled = true; + this.cancelledDocument = this.currentDocument; + this.cancelledPosition = this.currentPosition; + } + this.waiting = false; + this.ready = false; + this.content = undefined; + this.currentDocument = undefined; + this.currentPosition = undefined; + this.currentCancellationToken = undefined; + } + + public isNewHover(document: vscode.TextDocument, position: vscode.Position): boolean { + return !(this.currentDocument === document && this.currentPosition?.line === position.line && (this.currentPosition?.character === position.character || this.currentPosition?.character === position.character - 1)); + } +} diff --git a/Extension/src/LanguageServer/Providers/HoverProvider.ts b/Extension/src/LanguageServer/Providers/HoverProvider.ts index 0a4cd6eab6..4bc8bae199 100644 --- a/Extension/src/LanguageServer/Providers/HoverProvider.ts +++ b/Extension/src/LanguageServer/Providers/HoverProvider.ts @@ -4,17 +4,30 @@ * ------------------------------------------------------------------------------------------ */ import * as vscode from 'vscode'; import { Position, ResponseError, TextDocumentPositionParams } from 'vscode-languageclient'; +import { ManualSignal } from '../../Utility/Async/manualSignal'; import { DefaultClient, HoverRequest } from '../client'; import { RequestCancelled, ServerCancelled } from '../protocolFilter'; import { CppSettings } from '../settings'; export class HoverProvider implements vscode.HoverProvider { private client: DefaultClient; + private lastContent: vscode.MarkdownString[] | undefined; + private readonly hasContent = new ManualSignal(true); constructor(client: DefaultClient) { this.client = client; } public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + this.hasContent.reset(); + const copilotHoverProvider = this.client.getCopilotHoverProvider(); + if (copilotHoverProvider) { + // Check if this is a reinvocation from Copilot. + if (!copilotHoverProvider.isNewHover(document, position) && this.lastContent) { + this.hasContent.resolve(this.lastContent.length > 0); + return new vscode.Hover(this.lastContent); + } + } + const settings: CppSettings = new CppSettings(vscode.workspace.getWorkspaceFolder(document.uri)?.uri); if (settings.hover === "disabled") { return undefined; @@ -52,6 +65,12 @@ export class HoverProvider implements vscode.HoverProvider { hoverResult.range.end.line, hoverResult.range.end.character); } + this.hasContent.resolve(strings.length > 0); + this.lastContent = strings; return new vscode.Hover(strings, range); } + + get contentReady(): Promise { + return this.hasContent; + } } diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 6dd00d741b..1635a314fe 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -43,6 +43,7 @@ import { localizedStringCount, lookupString } from '../nativeStrings'; import { SessionState } from '../sessionState'; import * as telemetry from '../telemetry'; import { TestHook, getTestHook } from '../testHook'; +import { CopilotHoverProvider } from './Providers/CopilotHoverProvider'; import { HoverProvider } from './Providers/HoverProvider'; import { CodeAnalysisDiagnosticIdentifiersAndUri, @@ -533,6 +534,15 @@ export interface GetIncludesResult { includedFiles: string[]; } +export interface GetCopilotHoverInfoParams { + textDocument: TextDocumentIdentifier; + position: Position; +} + +interface GetCopilotHoverInfoResult { + content: string; +} + export interface ChatContextResult { language: string; standardVersion: string; @@ -554,6 +564,7 @@ export const FormatDocumentRequest: RequestType = new RequestType('cpptools/formatRange'); export const FormatOnTypeRequest: RequestType = new RequestType('cpptools/formatOnType'); export const HoverRequest: RequestType = new RequestType('cpptools/hover'); +export const GetCopilotHoverInfoRequest: RequestType = new RequestType('cpptools/getCopilotHoverInfo'); const CreateDeclarationOrDefinitionRequest: RequestType = new RequestType('cpptools/createDeclDef'); const ExtractToFunctionRequest: RequestType = new RequestType('cpptools/extractToFunction'); const GoToDirectiveInGroupRequest: RequestType = new RequestType('cpptools/goToDirectiveInGroup'); @@ -790,6 +801,7 @@ export interface Client { getShowConfigureIntelliSenseButton(): boolean; setShowConfigureIntelliSenseButton(show: boolean): void; addTrustedCompiler(path: string): Promise; + getCopilotHoverProvider(): CopilotHoverProvider | undefined; getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise; getChatContext(token: vscode.CancellationToken): Promise; } @@ -824,11 +836,14 @@ export class DefaultClient implements Client { private settingsTracker: SettingsTracker; private loggingLevel: number = 1; private configurationProvider?: string; + private hoverProvider: HoverProvider | undefined; + private copilotHoverProvider: CopilotHoverProvider | undefined; public lastCustomBrowseConfiguration: PersistentFolderState | undefined; public lastCustomBrowseConfigurationProviderId: PersistentFolderState | undefined; public lastCustomBrowseConfigurationProviderVersion: PersistentFolderState | undefined; public currentCaseSensitiveFileSupport: PersistentWorkspaceState | undefined; + public currentCopilotHoverEnabled: PersistentWorkspaceState | undefined; private registeredProviders: PersistentFolderState | undefined; private configStateReceived: ConfigStateReceived = { compilers: false, compileCommands: false, configProviders: undefined, timeout: false }; @@ -1258,8 +1273,16 @@ export class DefaultClient implements Client { this.registerFileWatcher(); initializedClientCount = 0; this.inlayHintsProvider = new InlayHintsProvider(); + this.hoverProvider = new HoverProvider(this); - this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, new HoverProvider(this))); + const settings: CppSettings = new CppSettings(); + this.currentCopilotHoverEnabled = new PersistentWorkspaceState("cpp.copilotHover", settings.copilotHover); + if (settings.copilotHover === "enabled" || + (settings.copilotHover === "default" && await telemetry.isFlightEnabled("CppCopilotHover"))) { + this.copilotHoverProvider = new CopilotHoverProvider(this); + this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, this.copilotHoverProvider)); + } + this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, this.hoverProvider)); this.disposables.push(vscode.languages.registerInlayHintsProvider(util.documentSelector, this.inlayHintsProvider)); this.disposables.push(vscode.languages.registerRenameProvider(util.documentSelector, new RenameProvider(this))); this.disposables.push(vscode.languages.registerReferenceProvider(util.documentSelector, new FindAllReferencesProvider(this))); @@ -1277,7 +1300,6 @@ export class DefaultClient implements Client { this.codeFoldingProvider = new FoldingRangeProvider(this); this.codeFoldingProviderDisposable = vscode.languages.registerFoldingRangeProvider(util.documentSelector, this.codeFoldingProvider); - const settings: CppSettings = new CppSettings(); if (settings.isEnhancedColorizationEnabled && semanticTokensLegend) { this.semanticTokensProvider = new SemanticTokensProvider(); this.semanticTokensProviderDisposable = vscode.languages.registerDocumentSemanticTokensProvider(util.documentSelector, this.semanticTokensProvider, semanticTokensLegend); @@ -1458,6 +1480,9 @@ export class DefaultClient implements Client { if (this.currentCaseSensitiveFileSupport && workspaceSettings.isCaseSensitiveFileSupportEnabled !== this.currentCaseSensitiveFileSupport.Value) { void util.promptForReloadWindowDueToSettingsChange(); } + if (this.currentCopilotHoverEnabled && workspaceSettings.copilotHover !== this.currentCopilotHoverEnabled.Value) { + void util.promptForReloadWindowDueToSettingsChange(); + } return { filesAssociations: workspaceOtherSettings.filesAssociations, workspaceFallbackEncoding: workspaceOtherSettings.filesEncoding, @@ -1480,6 +1505,7 @@ export class DefaultClient implements Client { codeAnalysisMaxConcurrentThreads: workspaceSettings.codeAnalysisMaxConcurrentThreads, codeAnalysisMaxMemory: workspaceSettings.codeAnalysisMaxMemory, codeAnalysisUpdateDelay: workspaceSettings.codeAnalysisUpdateDelay, + copilotHover: workspaceSettings.copilotHover, workspaceFolderSettings: workspaceFolderSettingsParams }; } @@ -1590,6 +1616,12 @@ export class DefaultClient implements Client { // We manually restart the language server so tell the LanguageClient not to do it automatically for us. return { action: CloseAction.DoNotRestart, message }; } + }, + markdown: { + isTrusted: true + // TODO: support for icons in markdown is not yet in the released version of vscode-languageclient. + // Based on PR (https://github.com/microsoft/vscode-languageserver-node/pull/1504) + //supportThemeIcons: true } // TODO: should I set the output channel? Does this sort output between servers? @@ -4017,6 +4049,14 @@ export class DefaultClient implements Client { compilerDefaults = await this.requestCompiler(path); DebugConfigurationProvider.ClearDetectedBuildTasks(); } + + public getHoverProvider(): HoverProvider | undefined { + return this.hoverProvider; + } + + public getCopilotHoverProvider(): CopilotHoverProvider | undefined { + return this.copilotHoverProvider; + } } function getLanguageServerFileName(): string { @@ -4128,6 +4168,7 @@ class NullClient implements Client { getShowConfigureIntelliSenseButton(): boolean { return false; } setShowConfigureIntelliSenseButton(show: boolean): void { } addTrustedCompiler(path: string): Promise { return Promise.resolve(); } + getCopilotHoverProvider(): CopilotHoverProvider | undefined { return undefined; } getIncludes(maxDepth: number, token: vscode.CancellationToken): 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 02dd3e8861..b7441625d4 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -17,9 +17,11 @@ import { TargetPopulation } from 'vscode-tas-client'; import * as which from 'which'; import { logAndReturn } from '../Utility/Async/returns'; import * as util from '../common'; +import { modelSelector } from '../constants'; import { getCrashCallStacksChannel } from '../logger'; import { PlatformInformation } from '../platform'; import * as telemetry from '../telemetry'; +import { CopilotHoverProvider } from './Providers/CopilotHoverProvider'; import { Client, DefaultClient, DoxygenCodeActionCommandArguments, openFileVersions } from './client'; import { ClientCollection } from './clientCollection'; import { CodeActionDiagnosticInfo, CodeAnalysisDiagnosticIdentifiersAndUri, codeAnalysisAllFixes, codeAnalysisCodeToFixes, codeAnalysisFileToCodeActions } from './codeAnalysis'; @@ -28,6 +30,7 @@ import { CppBuildTaskProvider } from './cppBuildTaskProvider'; import { getCustomConfigProviders } from './customProviders'; import { getLanguageConfig } from './languageConfig'; import { CppConfigurationLanguageModelTool } from './lmTool'; +import { getLocaleId } from './localization'; import { PersistentState } from './persistentState'; import { NodeType, TreeNode } from './referencesModel'; import { CppSettings } from './settings'; @@ -423,6 +426,7 @@ export async function registerCommands(enabled: boolean): Promise { commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExtractToFreeFunction', enabled ? () => onExtractToFunction(true, false) : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExtractToMemberFunction', enabled ? () => onExtractToFunction(false, true) : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ExpandSelection', enabled ? (r: Range) => onExpandSelection(r) : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowCopilotHover', enabled ? () => onCopilotHover() : onDisabledCommand)); } function onDisabledCommand() { @@ -1387,3 +1391,125 @@ export async function preReleaseCheck(): Promise { } } } + +// This uses several workarounds for interacting with the hover feature. +// A proposal for dynamic hover content would help, such as the one here (https://github.com/microsoft/vscode/issues/195394) +async function onCopilotHover(): Promise { + // Check if the user has access to vscode language model. + const vscodelm = (vscode as any).lm; + if (!vscodelm) { + return; + } + + const copilotHoverProvider = clients.ActiveClient.getCopilotHoverProvider(); + if (!copilotHoverProvider) { + return; + } + + const hoverDocument = copilotHoverProvider.getCurrentHoverDocument(); + const hoverPosition = copilotHoverProvider.getCurrentHoverPosition(); + if (!hoverDocument || !hoverPosition) { + return; + } + + const errorMessage = localize("copilot.hover.error", "An error occurred while generating Copilot summary."); + + // Prep hover with wait message. + copilotHoverProvider.showWaiting(); + + if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { + return; + } + + // Move the cursor to the hover position, but don't focus the editor. + await vscode.window.showTextDocument(hoverDocument, { preserveFocus: true, selection: new vscode.Selection(hoverPosition, hoverPosition) }); + + if (!await showCopilotContent(copilotHoverProvider, hoverDocument, hoverPosition)) { + return; + } + + // Gather the content for the query from the client. + const requestInfo = await copilotHoverProvider.getRequestInfo(hoverDocument, hoverPosition); + + if (requestInfo.length === 0) { + await showCopilotContent(copilotHoverProvider, hoverDocument, hoverPosition, errorMessage); + return; + } + + const locale = getLocaleId(); + + const messages = [ + vscode.LanguageModelChatMessage + .User(requestInfo + locale)]; + + const [model] = await vscodelm.selectChatModels(modelSelector); + + let chatResponse: vscode.LanguageModelChatResponse | undefined; + try { + chatResponse = await model.sendRequest( + messages, + {}, + copilotHoverProvider.getCurrentHoverCancellationToken() + ); + } catch (err) { + if (err instanceof vscode.LanguageModelError) { + console.log(err.message, err.code, err.cause); + await showCopilotContent(copilotHoverProvider, hoverDocument, hoverPosition, errorMessage); + } else { + throw err; + } + return; + } + + // Ensure we have a valid response from Copilot. + if (!chatResponse) { + await showCopilotContent(copilotHoverProvider, hoverDocument, hoverPosition, errorMessage); + return; + } + + let content: string = ''; + + try { + for await (const fragment of chatResponse.text) { + content += fragment; + } + } catch (err) { + await showCopilotContent(copilotHoverProvider, hoverDocument, hoverPosition, errorMessage); + return; + } + + if (content.length === 0) { + await showCopilotContent(copilotHoverProvider, hoverDocument, hoverPosition, errorMessage); + return; + } + + await showCopilotContent(copilotHoverProvider, hoverDocument, hoverPosition, content); +} + +async function showCopilotContent(copilotHoverProvider: CopilotHoverProvider, hoverDocument: vscode.TextDocument, hoverPosition: vscode.Position, content?: string): Promise { + // Check if the cursor has been manually moved by the user. If so, exit. + const currentCursorPosition = vscode.window.activeTextEditor?.selection.active; + if (!currentCursorPosition?.isEqual(hoverPosition)) { + // Reset implies cancellation, but we need to ensure cancellation is acknowledged before returning. + copilotHoverProvider.reset(); + } + + if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { + return false; + } + + await vscode.commands.executeCommand('cursorMove', { to: 'right' }); + await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus' }); + + if (content) { + copilotHoverProvider.showContent(content); + } + + if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { + return false; + } + await vscode.commands.executeCommand('cursorMove', { to: 'left' }); + await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus' }); + + return true; +} diff --git a/Extension/src/LanguageServer/settings.ts b/Extension/src/LanguageServer/settings.ts index 8e8d6c0654..6e0829618d 100644 --- a/Extension/src/LanguageServer/settings.ts +++ b/Extension/src/LanguageServer/settings.ts @@ -161,6 +161,7 @@ export interface SettingsParams { codeAnalysisMaxMemory: number | null; codeAnalysisUpdateDelay: number; workspaceFolderSettings: WorkspaceFolderSettingsParams[]; + copilotHover: string; } function getTarget(): vscode.ConfigurationTarget { @@ -454,6 +455,8 @@ export class CppSettings extends Settings { && this.intelliSenseEngine.toLowerCase() === "default" && vscode.workspace.getConfiguration("workbench").get("colorTheme") !== "Default High Contrast"; } + public get copilotHover(): string { return (vscode as any).lm ? this.getAsString("copilotHover") : "disabled"; } + public get formattingEngine(): string { return this.getAsString("formatting"); } public get vcFormatIndentBraces(): boolean { return this.getAsBoolean("vcFormat.indent.braces"); } public get vcFormatIndentMultiLineRelativeTo(): string { return this.getAsString("vcFormat.indent.multiLineRelativeTo"); } diff --git a/Extension/src/constants.ts b/Extension/src/constants.ts index e38b513df2..059bac203b 100644 --- a/Extension/src/constants.ts +++ b/Extension/src/constants.ts @@ -13,3 +13,6 @@ export const isLinux = OperatingSystem === 'linux'; // if you want to see the output of verbose logging, set this to true. export const verboseEnabled = false; + +// Model selector for Copilot features +export const modelSelector = { vendor: 'copilot', family: 'gpt-4' }; diff --git a/Extension/src/nativeStrings.json b/Extension/src/nativeStrings.json index 50e9e0ab01..f1ed3656aa 100644 --- a/Extension/src/nativeStrings.json +++ b/Extension/src/nativeStrings.json @@ -478,5 +478,6 @@ "refactor_extract_reference_return_c_code": "The function would have to return a value by reference. C code cannot return references.", "refactor_extract_xborder_jump": "Jumps between the selected code and the surrounding code are present.", "refactor_extract_missing_return": "In the selected code, some control paths exit without setting the return value. This is supported only for scalar, numeric, and pointer return types.", - "expand_selection": "Expand selection (to enable 'Extract to function')" + "expand_selection": "Expand selection (to enable 'Extract to function')", + "copilot_hover_link": "Generate Copilot summary" } diff --git a/Extension/src/telemetry.ts b/Extension/src/telemetry.ts index 600ffa4c45..1050faac7e 100644 --- a/Extension/src/telemetry.ts +++ b/Extension/src/telemetry.ts @@ -83,6 +83,10 @@ export async function isExperimentEnabled(experimentName: string): Promise { const experimentationService: IExperimentationService | undefined = await getExperimentationService(); const isEnabled: boolean | undefined = experimentationService?.getTreatmentVariable("vscode", experimentName); return isEnabled ?? false;