From 0eac8bf5a3f4896022c6b61eef82a4237ad6b99f Mon Sep 17 00:00:00 2001 From: spebl Date: Mon, 5 Aug 2024 12:47:40 -0700 Subject: [PATCH 01/19] initial on-the-fly docs implementation for vscode --- Extension/package.json | 2 +- Extension/src/LanguageServer/client.ts | 35 +++++++++++++ Extension/src/LanguageServer/extension.ts | 60 +++++++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/Extension/package.json b/Extension/package.json index c9f35c0256..5abdb70ce2 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -11,7 +11,7 @@ }, "license": "SEE LICENSE IN LICENSE.txt", "engines": { - "vscode": "^1.67.0" + "vscode": "^1.90.0" }, "bugs": { "url": "https://github.com/Microsoft/vscode-cpptools/issues", diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 9479e3d597..fe2fd102cb 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -543,6 +543,21 @@ interface GetIncludesResult includedFiles: string[]; } +interface ShowOTFDocsParams +{ + content: string; +} + +interface ShowOTFDocsResult +{ + hoverPos: Position; +} + +interface GetOTFDocsInfoResult +{ + content: string; +} + // Requests const PreInitializationRequest: RequestType = new RequestType('cpptools/preinitialize'); const InitializationRequest: RequestType = new RequestType('cpptools/initialize'); @@ -562,6 +577,8 @@ const GoToDirectiveInGroupRequest: RequestType = new RequestType('cpptools/generateDoxygenComment'); const ChangeCppPropertiesRequest: RequestType = new RequestType('cpptools/didChangeCppProperties'); const IncludesRequest: RequestType = new RequestType('cpptools/getIncludes'); +const GetOTFDocsInfoRequest: RequestType = new RequestType('cpptools/getOTFDocsInfo'); +const ShowOTFDocsRequest: RequestType = new RequestType('cpptools/showOTFDocs'); // Notifications to the server const DidOpenNotification: NotificationType = new NotificationType('textDocument/didOpen'); @@ -792,6 +809,8 @@ export interface Client { setShowConfigureIntelliSenseButton(show: boolean): void; addTrustedCompiler(path: string): Promise; getIncludes(maxDepth: number): Promise; + showOTFDocs(content: string): Promise; + getOTFDocsInfo(): Promise; } export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client { @@ -1582,6 +1601,9 @@ 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: should I set the output channel? Does this sort output between servers? @@ -3978,6 +4000,17 @@ export class DefaultClient implements Client { compilerDefaults = await this.requestCompiler(path); DebugConfigurationProvider.ClearDetectedBuildTasks(); } + + public async showOTFDocs(content: string): Promise { + const params: ShowOTFDocsParams = {content: content}; + await this.ready; + return this.languageClient.sendRequest(ShowOTFDocsRequest, params); + } + + public async getOTFDocsInfo(): Promise { + await this.ready; + return this.languageClient.sendRequest(GetOTFDocsInfoRequest, null); + } } function getLanguageServerFileName(): string { @@ -4090,4 +4123,6 @@ class NullClient implements Client { setShowConfigureIntelliSenseButton(show: boolean): void { } addTrustedCompiler(path: string): Promise { return Promise.resolve(); } getIncludes(): Promise { return Promise.resolve({} as GetIncludesResult); } + showOTFDocs(content: string): Promise { return Promise.resolve({} as ShowOTFDocsResult); } + getOTFDocsInfo(): Promise { return Promise.resolve({} as GetOTFDocsInfoResult); } } diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 0981c01f34..4f919b513b 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -404,6 +404,7 @@ export function registerCommands(enabled: boolean): void { 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.getIncludes', enabled ? (maxDepth: number) => getIncludes(maxDepth) : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.OTFDocs', enabled ? onOTFDocs : onDisabledCommand)); } function onDisabledCommand() { @@ -1379,3 +1380,62 @@ export async function getIncludes(maxDepth: number): Promise { const includes = await clients.ActiveClient.getIncludes(maxDepth); return includes; } + +async function onOTFDocs(): Promise { + const response = await clients.ActiveClient.getOTFDocsInfo(); + + // Ensure the content is valid before proceeding. + const request = response.content; + + if (request.length === 0) { + return; + } + + const messages = [ + vscode.LanguageModelChatMessage + .User(request)]; + + const [model] = await vscode.lm.selectChatModels({ + vendor: 'copilot', + family: 'gpt-4' + }); + + let chatResponse: vscode.LanguageModelChatResponse | undefined; + try { + chatResponse = await model.sendRequest( + messages, + {}, + new vscode.CancellationTokenSource().token + ); + } catch (err) { + if (err instanceof vscode.LanguageModelError) { + console.log(err.message, err.code, err.cause); + } else { + throw err; + } + return; + } + + let content: string = ''; + + try { + for await (const fragment of chatResponse.text) { + content += fragment; + } + } catch (err) { + return; + } + + const editor = vscode.window.activeTextEditor; + if (!editor) { + return; + } + + // Prepare the client to show the content on next hover. + const result = await clients.ActiveClient.showOTFDocs(content); + // Move the cursor to the position of the open hover. + editor.selection = new vscode.Selection(result.hoverPos.line, result.hoverPos.character, result.hoverPos.line, result.hoverPos.character); + // Trigger a hover event to show the new content. This is necessary because the content isn't updated if the hover isn't closed and then reopened. + // API proposal to update hover content: "editorHoverVerbosityLevel". (https://github.com/microsoft/vscode/issues/195394) + await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus'}); +} From 2667ec5593d9346e30b041f0ae8aaf5c0561ba7c Mon Sep 17 00:00:00 2001 From: spebl Date: Tue, 6 Aug 2024 12:32:23 -0700 Subject: [PATCH 02/19] add localization support --- Extension/src/LanguageServer/extension.ts | 5 ++++- Extension/src/nativeStrings.json | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index be1652f198..54ce4d3020 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 { getLocaleId } from './localization'; import { PersistentState } from './persistentState'; import { NodeType, TreeNode } from './referencesModel'; import { CppSettings } from './settings'; @@ -1391,9 +1392,11 @@ async function onOTFDocs(): Promise { return; } + const locale = getLocaleId(); + const messages = [ vscode.LanguageModelChatMessage - .User(request)]; + .User(request + locale)]; const [model] = await vscode.lm.selectChatModels({ vendor: 'copilot', diff --git a/Extension/src/nativeStrings.json b/Extension/src/nativeStrings.json index 50e9e0ab01..40160adc85 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')", + "otf_docs_link": "Generate Copilot summary" } From fb310c8ff15475bb0e061a0bfa363082cd0239b0 Mon Sep 17 00:00:00 2001 From: spebl Date: Tue, 6 Aug 2024 18:10:48 -0700 Subject: [PATCH 03/19] add support for icons in hover --- Extension/src/LanguageServer/client.ts | 3 +++ Extension/src/LanguageServer/protocolFilter.ts | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index fe2fd102cb..9b49f54c9d 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -1604,6 +1604,9 @@ export class DefaultClient implements Client { }, 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? diff --git a/Extension/src/LanguageServer/protocolFilter.ts b/Extension/src/LanguageServer/protocolFilter.ts index e161d8f566..144495f0a1 100644 --- a/Extension/src/LanguageServer/protocolFilter.ts +++ b/Extension/src/LanguageServer/protocolFilter.ts @@ -89,7 +89,16 @@ export function createProtocolFilter(): Middleware { provideHover: async (document, position, token, next: (document: any, position: any, token: any) => any) => clients.ActiveClient.enqueue(async () => { const me: Client = clients.getClientFor(document.uri); if (me.TrackedDocuments.has(document.uri.toString())) { - return next(document, position, token); + const result: Thenable = next(document, position, token); + // Needed to support theme icons in markdown hover content until vscode-languageclient is updated. + return result.then((value: vscode.Hover) => { + value.contents.forEach((content) => { + if (content instanceof vscode.MarkdownString) { + content.supportThemeIcons = true; + } + }); + return value; + }); } return null; }), From 433f22dc287189441c556109f7f916b31c43b4ec Mon Sep 17 00:00:00 2001 From: spebl Date: Tue, 6 Aug 2024 23:29:33 -0700 Subject: [PATCH 04/19] add support for feature flag control --- Extension/src/LanguageServer/client.ts | 7 +++--- Extension/src/LanguageServer/settings.ts | 29 ++++++++++++++++++++++++ Extension/src/telemetry.ts | 4 ++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 9b49f54c9d..74bd689d42 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -1465,7 +1465,7 @@ export class DefaultClient implements Client { return workspaceFolderSettingsParams; } - private getAllSettings(): SettingsParams { + private async getAllSettings(): Promise { const workspaceSettings: CppSettings = new CppSettings(); const workspaceOtherSettings: OtherSettings = new OtherSettings(); const workspaceFolderSettingsParams: WorkspaceFolderSettingsParams[] = this.getAllWorkspaceFolderSettings(); @@ -1491,6 +1491,7 @@ export class DefaultClient implements Client { codeAnalysisMaxConcurrentThreads: workspaceSettings.codeAnalysisMaxConcurrentThreads, codeAnalysisMaxMemory: workspaceSettings.codeAnalysisMaxMemory, codeAnalysisUpdateDelay: workspaceSettings.codeAnalysisUpdateDelay, + otfDocsEnabled: await workspaceSettings.otfDocsEnabled, workspaceFolderSettings: workspaceFolderSettingsParams }; } @@ -1560,7 +1561,7 @@ export class DefaultClient implements Client { resetDatabase: resetDatabase, edgeMessagesDirectory: path.join(util.getExtensionFilePath("bin"), "messages", getLocaleId()), localizedStrings: localizedStrings, - settings: this.getAllSettings() + settings: await this.getAllSettings() }; this.loggingLevel = util.getNumericLoggingLevel(cppInitializationParams.settings.loggingLevel); @@ -1632,7 +1633,7 @@ export class DefaultClient implements Client { public async sendDidChangeSettings(): Promise { // Send settings json to native side await this.ready; - await this.languageClient.sendNotification(DidChangeSettingsNotification, this.getAllSettings()); + await this.languageClient.sendNotification(DidChangeSettingsNotification, await this.getAllSettings()); } public async onDidChangeSettings(_event: vscode.ConfigurationChangeEvent): Promise> { diff --git a/Extension/src/LanguageServer/settings.ts b/Extension/src/LanguageServer/settings.ts index a66a97c50c..4cb6d9bd34 100644 --- a/Extension/src/LanguageServer/settings.ts +++ b/Extension/src/LanguageServer/settings.ts @@ -16,6 +16,7 @@ import * as nls from 'vscode-nls'; import * as which from 'which'; import { getCachedClangFormatPath, getCachedClangTidyPath, getExtensionFilePath, setCachedClangFormatPath, setCachedClangTidyPath } from '../common'; import { isWindows } from '../constants'; +import { isFlightEnabled } from '../telemetry'; import { DefaultClient, cachedEditorConfigLookups, cachedEditorConfigSettings, hasTrustedCompilerPaths } from './client'; import { clients } from './extension'; import { CommentPattern } from './languageConfig'; @@ -159,6 +160,7 @@ export interface SettingsParams { codeAnalysisMaxMemory: number | null | undefined; codeAnalysisUpdateDelay: number | undefined; workspaceFolderSettings: WorkspaceFolderSettingsParams[]; + otfDocsEnabled: boolean | undefined; } function getTarget(): vscode.ConfigurationTarget { @@ -461,6 +463,33 @@ export class CppSettings extends Settings { return super.Section.get("inlayHints.referenceOperator.showSpace") === true; } + public get otfDocsEnabled(): PromiseLike { + // Check if the user has access to copilot. + return vscode.lm.selectChatModels({vendor: "copilot"}).then((models) => { + // Check if the setting is explicitly set to enabled or disabled. + const setting = super.Section.get("onTheFlyDocsEnabled"); + + // If no models are returned, the user doesn't have access to copilot. + if (models.length === 0) { + // Register to update this setting if the user gains access to copilot. + vscode.lm.onDidChangeChatModels(() => { + void this.Section.update("onTheFlyDocsEnabled", setting); + }); + return false; + } + + if (setting === "enabled") { + return true; + } + if (setting === "disabled") { + return false; + } + + // Check for the feature flag. + return isFlightEnabled("cpp.otfDocs"); + }); + } + public get enhancedColorization(): boolean { return super.Section.get("enhancedColorization")?.toLowerCase() !== "disabled" && this.intelliSenseEngine === "default" diff --git a/Extension/src/telemetry.ts b/Extension/src/telemetry.ts index dc6a1c199f..c37edfc1b1 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; From 7ec62d9ee9b0d871b15304b9063e0d9091d9804d Mon Sep 17 00:00:00 2001 From: spebl Date: Tue, 6 Aug 2024 23:30:38 -0700 Subject: [PATCH 05/19] add setting and strings --- Extension/package.json | 11 +++++++++++ Extension/package.nls.json | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/Extension/package.json b/Extension/package.json index 2ffe24408d..205960a672 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -3286,6 +3286,17 @@ "default": false, "markdownDescription": "%c_cpp.configuration.addNodeAddonIncludePaths.markdownDescription%", "scope": "application" + }, + "C_Cpp.onTheFlyDocsEnabled": { + "type": "string", + "enum": [ + "default", + "enabled", + "disabled" + ], + "default": "default", + "markdownDescription": "%c_cpp.configuration.otfDocsEnabled.markdownDescription%", + "scope": "window" } } } diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 7745216295..58f7fbd5fd 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -775,6 +775,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.otfDocsEnabled.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": [ From 7a4184174b27d882327eb942913ed5251864175b Mon Sep 17 00:00:00 2001 From: spebl Date: Wed, 7 Aug 2024 18:07:27 -0700 Subject: [PATCH 06/19] fix settings when copilot models load later and fix showing copilot when editor isnt selected --- Extension/src/LanguageServer/extension.ts | 2 ++ Extension/src/LanguageServer/settings.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 54ce4d3020..37ac4baab6 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -1436,6 +1436,8 @@ async function onOTFDocs(): Promise { // Prepare the client to show the content on next hover. const result = await clients.ActiveClient.showOTFDocs(content); + // Make sure the editor has focus. + await vscode.commands.executeCommand('workbench.action.focusActiveEditorGroup'); // Move the cursor to the position of the open hover. editor.selection = new vscode.Selection(result.hoverPos.line, result.hoverPos.character, result.hoverPos.line, result.hoverPos.character); // Trigger a hover event to show the new content. This is necessary because the content isn't updated if the hover isn't closed and then reopened. diff --git a/Extension/src/LanguageServer/settings.ts b/Extension/src/LanguageServer/settings.ts index 4cb6d9bd34..8b62f5aa42 100644 --- a/Extension/src/LanguageServer/settings.ts +++ b/Extension/src/LanguageServer/settings.ts @@ -473,7 +473,7 @@ export class CppSettings extends Settings { if (models.length === 0) { // Register to update this setting if the user gains access to copilot. vscode.lm.onDidChangeChatModels(() => { - void this.Section.update("onTheFlyDocsEnabled", setting); + clients.ActiveClient.sendDidChangeSettings(); }); return false; } From db42258d02e5ff104a2d61d015db1a4a4c60052b Mon Sep 17 00:00:00 2001 From: spebl Date: Wed, 14 Aug 2024 14:07:34 -0700 Subject: [PATCH 07/19] 'otf docs' -> 'copilot hover' in naming conventions and strings. fixes and cleanup based on feedback. add waiting spinner. --- Extension/package.json | 4 +- Extension/package.nls.json | 4 +- Extension/src/LanguageServer/client.ts | 30 ++++----- Extension/src/LanguageServer/extension.ts | 61 +++++++++++++------ .../src/LanguageServer/protocolFilter.ts | 17 +++--- Extension/src/LanguageServer/settings.ts | 35 ++++++----- Extension/src/constants.ts | 3 + Extension/src/nativeStrings.json | 2 +- 8 files changed, 95 insertions(+), 61 deletions(-) diff --git a/Extension/package.json b/Extension/package.json index 205960a672..a987d6765a 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -3287,7 +3287,7 @@ "markdownDescription": "%c_cpp.configuration.addNodeAddonIncludePaths.markdownDescription%", "scope": "application" }, - "C_Cpp.onTheFlyDocsEnabled": { + "C_Cpp.copilotHover": { "type": "string", "enum": [ "default", @@ -3295,7 +3295,7 @@ "disabled" ], "default": "default", - "markdownDescription": "%c_cpp.configuration.otfDocsEnabled.markdownDescription%", + "markdownDescription": "%c_cpp.configuration.copilotHover.markdownDescription%", "scope": "window" } } diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 58f7fbd5fd..0c093e0db1 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -775,8 +775,8 @@ "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.otfDocsEnabled.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.", + "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." ] diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 74bd689d42..e546f01c63 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -543,17 +543,17 @@ interface GetIncludesResult includedFiles: string[]; } -interface ShowOTFDocsParams +interface ShowCopilotHoverParams { content: string; } -interface ShowOTFDocsResult +interface ShowCopilotHoverResult { hoverPos: Position; } -interface GetOTFDocsInfoResult +interface GetCopilotHoverInfoResult { content: string; } @@ -577,8 +577,8 @@ const GoToDirectiveInGroupRequest: RequestType = new RequestType('cpptools/generateDoxygenComment'); const ChangeCppPropertiesRequest: RequestType = new RequestType('cpptools/didChangeCppProperties'); const IncludesRequest: RequestType = new RequestType('cpptools/getIncludes'); -const GetOTFDocsInfoRequest: RequestType = new RequestType('cpptools/getOTFDocsInfo'); -const ShowOTFDocsRequest: RequestType = new RequestType('cpptools/showOTFDocs'); +const GetCopilotHoverInfoRequest: RequestType = new RequestType('cpptools/getCopilotHoverInfo'); +const ShowCopilotHoverRequest: RequestType = new RequestType('cpptools/showCopilotHover'); // Notifications to the server const DidOpenNotification: NotificationType = new NotificationType('textDocument/didOpen'); @@ -809,8 +809,8 @@ export interface Client { setShowConfigureIntelliSenseButton(show: boolean): void; addTrustedCompiler(path: string): Promise; getIncludes(maxDepth: number): Promise; - showOTFDocs(content: string): Promise; - getOTFDocsInfo(): Promise; + showCopilotHover(content: string): Promise; + getCopilotHoverInfo(): Promise; } export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client { @@ -1491,7 +1491,7 @@ export class DefaultClient implements Client { codeAnalysisMaxConcurrentThreads: workspaceSettings.codeAnalysisMaxConcurrentThreads, codeAnalysisMaxMemory: workspaceSettings.codeAnalysisMaxMemory, codeAnalysisUpdateDelay: workspaceSettings.codeAnalysisUpdateDelay, - otfDocsEnabled: await workspaceSettings.otfDocsEnabled, + copilotHover: await workspaceSettings.copilotHover, workspaceFolderSettings: workspaceFolderSettingsParams }; } @@ -4005,15 +4005,15 @@ export class DefaultClient implements Client { DebugConfigurationProvider.ClearDetectedBuildTasks(); } - public async showOTFDocs(content: string): Promise { - const params: ShowOTFDocsParams = {content: content}; + public async showCopilotHover(content: string): Promise { + const params: ShowCopilotHoverParams = {content: content}; await this.ready; - return this.languageClient.sendRequest(ShowOTFDocsRequest, params); + return this.languageClient.sendRequest(ShowCopilotHoverRequest, params); } - public async getOTFDocsInfo(): Promise { + public async getCopilotHoverInfo(): Promise { await this.ready; - return this.languageClient.sendRequest(GetOTFDocsInfoRequest, null); + return this.languageClient.sendRequest(GetCopilotHoverInfoRequest, null); } } @@ -4127,6 +4127,6 @@ class NullClient implements Client { setShowConfigureIntelliSenseButton(show: boolean): void { } addTrustedCompiler(path: string): Promise { return Promise.resolve(); } getIncludes(): Promise { return Promise.resolve({} as GetIncludesResult); } - showOTFDocs(content: string): Promise { return Promise.resolve({} as ShowOTFDocsResult); } - getOTFDocsInfo(): Promise { return Promise.resolve({} as GetOTFDocsInfoResult); } + showCopilotHover(content: string): Promise { return Promise.resolve({} as ShowCopilotHoverResult); } + getCopilotHoverInfo(): Promise { return Promise.resolve({} as GetCopilotHoverInfoResult); } } diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 37ac4baab6..32c8504fa9 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -17,6 +17,7 @@ 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'; @@ -405,7 +406,7 @@ export function registerCommands(enabled: boolean): void { 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.getIncludes', enabled ? (maxDepth: number) => getIncludes(maxDepth) : () => Promise.resolve())); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.OTFDocs', enabled ? onOTFDocs : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowCopilotHover', enabled ? onCopilotHover : onDisabledCommand)); } function onDisabledCommand() { @@ -1382,8 +1383,32 @@ export async function getIncludes(maxDepth: number): Promise { return includes; } -async function onOTFDocs(): Promise { - const response = await clients.ActiveClient.getOTFDocsInfo(); +// 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 { + if (!vscode.window.activeTextEditor) { return; } + // Check if the user has access to vscode language model. + const vscodelm = (vscode as any).lm; + if (!vscodelm) { return; } + + // Prep hover with wait message and get the hover position location. + const copilotHoverResult = await clients.ActiveClient.showCopilotHover('$(loading~spin)'); + const hoverPosition = new vscode.Position(copilotHoverResult.hoverPos.line, copilotHoverResult.hoverPos.character); + + // Make sure the editor has focus. + await vscode.window.showTextDocument(vscode.window.activeTextEditor.document, { preserveFocus: false, selection: new vscode.Selection(hoverPosition, hoverPosition) }); + + // Workaround to force the editor to update it's content, needs to be called from another location first. + await vscode.commands.executeCommand('cursorMove', { to: 'right' }); + await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus' }); + + // Move back and show the correct hover. + await clients.ActiveClient.showCopilotHover('$(loading~spin)'); + await vscode.commands.executeCommand('cursorMove', { to: 'left' }); + await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus'}); + + // Gather the content for the query from the client. + const response = await clients.ActiveClient.getCopilotHoverInfo(); // Ensure the content is valid before proceeding. const request = response.content; @@ -1398,10 +1423,7 @@ async function onOTFDocs(): Promise { vscode.LanguageModelChatMessage .User(request + locale)]; - const [model] = await vscode.lm.selectChatModels({ - vendor: 'copilot', - family: 'gpt-4' - }); + const [model] = await vscodelm.selectChatModels(modelSelector); let chatResponse: vscode.LanguageModelChatResponse | undefined; try { @@ -1419,6 +1441,9 @@ async function onOTFDocs(): Promise { return; } + // Ensure we have a valid response from Copilot. + if (!chatResponse) { return; } + let content: string = ''; try { @@ -1429,18 +1454,16 @@ async function onOTFDocs(): Promise { return; } - const editor = vscode.window.activeTextEditor; - if (!editor) { - return; - } + if (!vscode.window.activeTextEditor) { return; } + await vscode.window.showTextDocument(vscode.window.activeTextEditor.document, { preserveFocus: false, selection: new vscode.Selection(hoverPosition, hoverPosition) }); - // Prepare the client to show the content on next hover. - const result = await clients.ActiveClient.showOTFDocs(content); - // Make sure the editor has focus. - await vscode.commands.executeCommand('workbench.action.focusActiveEditorGroup'); - // Move the cursor to the position of the open hover. - editor.selection = new vscode.Selection(result.hoverPos.line, result.hoverPos.character, result.hoverPos.line, result.hoverPos.character); - // Trigger a hover event to show the new content. This is necessary because the content isn't updated if the hover isn't closed and then reopened. - // API proposal to update hover content: "editorHoverVerbosityLevel". (https://github.com/microsoft/vscode/issues/195394) + // Same workaround as above to force the editor to update it's content. + await clients.ActiveClient.showCopilotHover('$(loading~spin)'); + await vscode.commands.executeCommand('cursorMove', { to: 'right' }); + await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus'}); + + // Prepare and show the real content. + await clients.ActiveClient.showCopilotHover(content); + await vscode.commands.executeCommand('cursorMove', { to: 'left' }); await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus'}); } diff --git a/Extension/src/LanguageServer/protocolFilter.ts b/Extension/src/LanguageServer/protocolFilter.ts index 144495f0a1..492692f6d7 100644 --- a/Extension/src/LanguageServer/protocolFilter.ts +++ b/Extension/src/LanguageServer/protocolFilter.ts @@ -89,14 +89,15 @@ export function createProtocolFilter(): Middleware { provideHover: async (document, position, token, next: (document: any, position: any, token: any) => any) => clients.ActiveClient.enqueue(async () => { const me: Client = clients.getClientFor(document.uri); if (me.TrackedDocuments.has(document.uri.toString())) { - const result: Thenable = next(document, position, token); - // Needed to support theme icons in markdown hover content until vscode-languageclient is updated. - return result.then((value: vscode.Hover) => { - value.contents.forEach((content) => { - if (content instanceof vscode.MarkdownString) { - content.supportThemeIcons = true; - } - }); + // Currently needed to support icons. + return next(document, position, token).then((value: vscode.Hover) => { + if (value && value.contents instanceof Array) { + value.contents.forEach((content) => { + if (content instanceof vscode.MarkdownString) { + content.supportThemeIcons = true; + } + }); + } return value; }); } diff --git a/Extension/src/LanguageServer/settings.ts b/Extension/src/LanguageServer/settings.ts index 8b62f5aa42..ab654745c4 100644 --- a/Extension/src/LanguageServer/settings.ts +++ b/Extension/src/LanguageServer/settings.ts @@ -15,7 +15,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import * as which from 'which'; import { getCachedClangFormatPath, getCachedClangTidyPath, getExtensionFilePath, setCachedClangFormatPath, setCachedClangTidyPath } from '../common'; -import { isWindows } from '../constants'; +import { isWindows, modelSelector } from '../constants'; import { isFlightEnabled } from '../telemetry'; import { DefaultClient, cachedEditorConfigLookups, cachedEditorConfigSettings, hasTrustedCompilerPaths } from './client'; import { clients } from './extension'; @@ -160,7 +160,7 @@ export interface SettingsParams { codeAnalysisMaxMemory: number | null | undefined; codeAnalysisUpdateDelay: number | undefined; workspaceFolderSettings: WorkspaceFolderSettingsParams[]; - otfDocsEnabled: boolean | undefined; + copilotHover: boolean | undefined; } function getTarget(): vscode.ConfigurationTarget { @@ -463,16 +463,26 @@ export class CppSettings extends Settings { return super.Section.get("inlayHints.referenceOperator.showSpace") === true; } - public get otfDocsEnabled(): PromiseLike { - // Check if the user has access to copilot. - return vscode.lm.selectChatModels({vendor: "copilot"}).then((models) => { - // Check if the setting is explicitly set to enabled or disabled. - const setting = super.Section.get("onTheFlyDocsEnabled"); + public get copilotHover(): PromiseLike { + // Check if the setting is explicitly set to enabled or disabled. + const setting = super.Section.get("copilotHover"); - // If no models are returned, the user doesn't have access to copilot. + if (setting === "disabled") { + return Promise.resolve(false); + } + + // Check if the user has access to vscode language model. + const vscodelm = (vscode as any).lm; + if (!vscodelm) { + return Promise.resolve(false); + } + + // Check if the user has access to Copilot. + return vscodelm.selectChatModels(modelSelector).then((models: any[]) => { + // If no models are returned, the user currently does not have access. if (models.length === 0) { - // Register to update this setting if the user gains access to copilot. - vscode.lm.onDidChangeChatModels(() => { + // Register to update this setting if the user gains access. + vscodelm.onDidChangeChatModels(() => { clients.ActiveClient.sendDidChangeSettings(); }); return false; @@ -481,12 +491,9 @@ export class CppSettings extends Settings { if (setting === "enabled") { return true; } - if (setting === "disabled") { - return false; - } // Check for the feature flag. - return isFlightEnabled("cpp.otfDocs"); + return isFlightEnabled("cpp.copilotHover"); }); } 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 40160adc85..f1ed3656aa 100644 --- a/Extension/src/nativeStrings.json +++ b/Extension/src/nativeStrings.json @@ -479,5 +479,5 @@ "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')", - "otf_docs_link": "Generate Copilot summary" + "copilot_hover_link": "Generate Copilot summary" } From c73a589cee6550f62137e3649d4c81e63762ba2a Mon Sep 17 00:00:00 2001 From: spebl Date: Wed, 14 Aug 2024 14:09:18 -0700 Subject: [PATCH 08/19] fix spacing --- Extension/package.nls.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 0c093e0db1..f5d2c22458 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -776,7 +776,7 @@ ] }, "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.", + "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." ] From 28390f1d4798a1bb8d2f99310bed368c757b5c1e Mon Sep 17 00:00:00 2001 From: spebl Date: Wed, 14 Aug 2024 14:20:50 -0700 Subject: [PATCH 09/19] reset vscode version --- Extension/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Extension/package.json b/Extension/package.json index dd1033b8c4..6f14787526 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -11,7 +11,7 @@ }, "license": "SEE LICENSE IN LICENSE.txt", "engines": { - "vscode": "^1.90.0" + "vscode": "^1.67.0" }, "bugs": { "url": "https://github.com/Microsoft/vscode-cpptools/issues", From 974246569ccd3e942a735f655e53c40455574d69 Mon Sep 17 00:00:00 2001 From: spebl Date: Tue, 24 Sep 2024 16:17:27 -0700 Subject: [PATCH 10/19] fix formatting --- Extension/src/LanguageServer/extension.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 7c7545429e..38f24f3b06 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -1385,7 +1385,9 @@ export async function getIncludes(maxDepth: number): 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 { - if (!vscode.window.activeTextEditor) { return; } + if (!vscode.window.activeTextEditor) { + return; + } // Check if the user has access to vscode language model. const vscodelm = (vscode as any).lm; if (!vscodelm) { return; } From 6076ca925c5634ed91796c472d62fc14b183d935 Mon Sep 17 00:00:00 2001 From: spebl Date: Fri, 27 Sep 2024 17:04:16 -0700 Subject: [PATCH 11/19] swap to using new copilot hover provider as to not interfere with existing hover as much. other fixes alongside this as well. --- .../Providers/CopilotHoverProvider.ts | 121 ++++++++++++++++++ Extension/src/LanguageServer/client.ts | 40 +++--- Extension/src/LanguageServer/extension.ts | 67 ++++++---- .../src/LanguageServer/protocolFilter.ts | 12 +- Extension/src/LanguageServer/settings.ts | 35 +---- 5 files changed, 189 insertions(+), 86 deletions(-) create mode 100644 Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts diff --git a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts new file mode 100644 index 0000000000..0033901974 --- /dev/null +++ b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts @@ -0,0 +1,121 @@ +/* -------------------------------------------------------------------------------------------- + * 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 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; + } + + if (!this.isNewHover(document, position)) { + if (this.ready) { + const contentMarkdown = new vscode.MarkdownString(this.content); + return new vscode.Hover(contentMarkdown); + } + if (this.waiting) { + const loadingMarkdown = new vscode.MarkdownString("$(loading~spin)", true); + return new vscode.Hover(loadingMarkdown); + } + } + + // Fresh hover, reset state. + this.reset(); + this.currentDocument = document; + this.currentPosition = position; + const commandString = "$(sparkle) [" + localize("generate.copilot.description", "Generate Copilot Description") + "](command:C_Cpp.ShowCopilotHover)"; + 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 async getRequestInfo(document: vscode.TextDocument, position: vscode.Position): Promise { + let requestInfo = ""; + const params: GetCopilotHoverInfoParams = { + uri: document.uri.toString(), + position: Position.create(position.line, position.character) + }; + await this.client.ready; + try { + const response = await this.client.languageClient.sendRequest(GetCopilotHoverInfoRequest, params); + 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; + } + + private 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; + } + + private 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/client.ts b/Extension/src/LanguageServer/client.ts index 80ea12bb29..017a35b784 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,12 +534,9 @@ interface GetIncludesResult { includedFiles: string[]; } -interface ShowCopilotHoverParams { - content: string; -} - -interface ShowCopilotHoverResult { - hoverPos: Position; +export interface GetCopilotHoverInfoParams { + uri: string; + position: Position; } interface GetCopilotHoverInfoResult { @@ -566,14 +564,13 @@ 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'); const GenerateDoxygenCommentRequest: RequestType = new RequestType('cpptools/generateDoxygenComment'); const ChangeCppPropertiesRequest: RequestType = new RequestType('cpptools/didChangeCppProperties'); const IncludesRequest: RequestType = new RequestType('cpptools/getIncludes'); -const GetCopilotHoverInfoRequest: RequestType = new RequestType('cpptools/getCopilotHoverInfo'); -const ShowCopilotHoverRequest: RequestType = new RequestType('cpptools/showCopilotHover'); const CppContextRequest: RequestType = new RequestType('cpptools/getChatContext'); // Notifications to the server @@ -805,8 +802,7 @@ export interface Client { setShowConfigureIntelliSenseButton(show: boolean): void; addTrustedCompiler(path: string): Promise; getIncludes(maxDepth: number): Promise; - showCopilotHover(content: string): Promise; - getCopilotHoverInfo(): Promise; + getCopilotHoverProvider(): CopilotHoverProvider | undefined; getChatContext(token: vscode.CancellationToken): Promise; } @@ -840,6 +836,7 @@ export class DefaultClient implements Client { private settingsTracker: SettingsTracker; private loggingLevel: number = 1; private configurationProvider?: string; + private copilotHoverProvider: CopilotHoverProvider | undefined; public lastCustomBrowseConfiguration: PersistentFolderState | undefined; public lastCustomBrowseConfigurationProviderId: PersistentFolderState | undefined; @@ -1274,6 +1271,12 @@ export class DefaultClient implements Client { initializedClientCount = 0; this.inlayHintsProvider = new InlayHintsProvider(); + const settings: CppSettings = new CppSettings(); + if (settings.copilotHover === "enabled" || + (settings.copilotHover === "default" && await telemetry.isFlightEnabled("cpp.copilotHover"))) { + this.copilotHoverProvider = new CopilotHoverProvider(this); + this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, this.copilotHoverProvider)); + } this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, new HoverProvider(this))); this.disposables.push(vscode.languages.registerInlayHintsProvider(util.documentSelector, this.inlayHintsProvider)); this.disposables.push(vscode.languages.registerRenameProvider(util.documentSelector, new RenameProvider(this))); @@ -1292,7 +1295,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); @@ -1460,7 +1462,7 @@ export class DefaultClient implements Client { return workspaceFolderSettingsParams; } - private async getAllSettings(): Promise { + private getAllSettings(): SettingsParams { const workspaceSettings: CppSettings = new CppSettings(); const workspaceOtherSettings: OtherSettings = new OtherSettings(); const workspaceFolderSettingsParams: WorkspaceFolderSettingsParams[] = this.getAllWorkspaceFolderSettings(); @@ -1489,7 +1491,7 @@ export class DefaultClient implements Client { codeAnalysisMaxConcurrentThreads: workspaceSettings.codeAnalysisMaxConcurrentThreads, codeAnalysisMaxMemory: workspaceSettings.codeAnalysisMaxMemory, codeAnalysisUpdateDelay: workspaceSettings.codeAnalysisUpdateDelay, - copilotHover: await workspaceSettings.copilotHover, + copilotHover: workspaceSettings.copilotHover, workspaceFolderSettings: workspaceFolderSettingsParams }; } @@ -1559,7 +1561,7 @@ export class DefaultClient implements Client { resetDatabase: resetDatabase, edgeMessagesDirectory: path.join(util.getExtensionFilePath("bin"), "messages", getLocaleId()), localizedStrings: localizedStrings, - settings: await this.getAllSettings() + settings: this.getAllSettings() }; this.loggingLevel = util.getNumericLoggingLevel(cppInitializationParams.settings.loggingLevel); @@ -1631,7 +1633,7 @@ export class DefaultClient implements Client { public async sendDidChangeSettings(): Promise { // Send settings json to native side await this.ready; - await this.languageClient.sendNotification(DidChangeSettingsNotification, await this.getAllSettings()); + await this.languageClient.sendNotification(DidChangeSettingsNotification, this.getAllSettings()); } public async onDidChangeSettings(_event: vscode.ConfigurationChangeEvent): Promise> { @@ -4023,10 +4025,8 @@ export class DefaultClient implements Client { DebugConfigurationProvider.ClearDetectedBuildTasks(); } - public async showCopilotHover(content: string): Promise { - const params: ShowCopilotHoverParams = { content: content }; - await this.ready; - return this.languageClient.sendRequest(ShowCopilotHoverRequest, params); + public getCopilotHoverProvider(): CopilotHoverProvider | undefined { + return this.copilotHoverProvider; } public async getCopilotHoverInfo(): Promise { @@ -4145,7 +4145,7 @@ class NullClient implements Client { setShowConfigureIntelliSenseButton(show: boolean): void { } addTrustedCompiler(path: string): Promise { return Promise.resolve(); } getIncludes(): Promise { return Promise.resolve({} as GetIncludesResult); } - showCopilotHover(content: string): Promise { return Promise.resolve({} as ShowCopilotHoverResult); } + getCopilotHoverProvider(): CopilotHoverProvider | undefined { return undefined; } getCopilotHoverInfo(): Promise { return Promise.resolve({} as GetCopilotHoverInfoResult); } 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 c56a6dbca0..5f6c830c1c 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -435,8 +435,7 @@ export function registerCommands(enabled: boolean, isRelatedFilesApiEnabled: boo 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.getIncludes', enabled ? (maxDepth: number) => getIncludes(maxDepth) : () => Promise.resolve())); - commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowCopilotHover', enabled ? onCopilotHover : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ShowCopilotHover', enabled ? () => onCopilotHover() : onDisabledCommand)); if (!isRelatedFilesApiEnabled) { commandDisposables.push(vscode.commands.registerCommand('C_Cpp.getIncludes', enabled ? (maxDepth: number) => getIncludes(maxDepth) : () => Promise.resolve())); @@ -1414,36 +1413,47 @@ export async function getIncludes(maxDepth: number): 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 { - if (!vscode.window.activeTextEditor) { - return; - } // Check if the user has access to vscode language model. const vscodelm = (vscode as any).lm; - if (!vscodelm) { return; } + if (!vscodelm) { + return; + } + + const copilotHoverProvider = clients.ActiveClient.getCopilotHoverProvider(); + if (!copilotHoverProvider) { + return; + } + + const hoverDocument = copilotHoverProvider.getCurrentHoverDocument(); + const hoverPosition = copilotHoverProvider.getCurrentHoverPosition(); + if (!hoverDocument || !hoverPosition) { + return; + } - // Prep hover with wait message and get the hover position location. - const copilotHoverResult = await clients.ActiveClient.showCopilotHover('$(loading~spin)'); - const hoverPosition = new vscode.Position(copilotHoverResult.hoverPos.line, copilotHoverResult.hoverPos.character); + // Prep hover with wait message. + copilotHoverProvider.showWaiting(); - // Make sure the editor has focus. - await vscode.window.showTextDocument(vscode.window.activeTextEditor.document, { preserveFocus: false, selection: new vscode.Selection(hoverPosition, hoverPosition) }); + // Make sure the hover document has focus. + await vscode.window.showTextDocument(hoverDocument, { preserveFocus: false, selection: new vscode.Selection(hoverPosition, hoverPosition) }); // Workaround to force the editor to update it's content, needs to be called from another location first. + if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { + return; + } await vscode.commands.executeCommand('cursorMove', { to: 'right' }); await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus' }); // Move back and show the correct hover. - await clients.ActiveClient.showCopilotHover('$(loading~spin)'); + if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { + return; + } await vscode.commands.executeCommand('cursorMove', { to: 'left' }); await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus' }); // Gather the content for the query from the client. - const response = await clients.ActiveClient.getCopilotHoverInfo(); - - // Ensure the content is valid before proceeding. - const request = response.content; + const requestInfo = await copilotHoverProvider.getRequestInfo(hoverDocument, hoverPosition); - if (request.length === 0) { + if (requestInfo.length === 0) { return; } @@ -1451,7 +1461,7 @@ async function onCopilotHover(): Promise { const messages = [ vscode.LanguageModelChatMessage - .User(request + locale)]; + .User(requestInfo + locale)]; const [model] = await vscodelm.selectChatModels(modelSelector); @@ -1472,7 +1482,9 @@ async function onCopilotHover(): Promise { } // Ensure we have a valid response from Copilot. - if (!chatResponse) { return; } + if (!chatResponse) { + return; + } let content: string = ''; @@ -1484,16 +1496,25 @@ async function onCopilotHover(): Promise { return; } - if (!vscode.window.activeTextEditor) { return; } - await vscode.window.showTextDocument(vscode.window.activeTextEditor.document, { preserveFocus: false, selection: new vscode.Selection(hoverPosition, hoverPosition) }); + if (content.length === 0) { + return; + } + + // Make sure the hover document has focus. + await vscode.window.showTextDocument(hoverDocument, { preserveFocus: false, selection: new vscode.Selection(hoverPosition, hoverPosition) }); // Same workaround as above to force the editor to update it's content. - await clients.ActiveClient.showCopilotHover('$(loading~spin)'); + if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { + return; + } await vscode.commands.executeCommand('cursorMove', { to: 'right' }); await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus' }); // Prepare and show the real content. - await clients.ActiveClient.showCopilotHover(content); + copilotHoverProvider.showContent(content); + if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { + return; + } await vscode.commands.executeCommand('cursorMove', { to: 'left' }); await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus' }); } diff --git a/Extension/src/LanguageServer/protocolFilter.ts b/Extension/src/LanguageServer/protocolFilter.ts index 4f012c1d1d..742727241b 100644 --- a/Extension/src/LanguageServer/protocolFilter.ts +++ b/Extension/src/LanguageServer/protocolFilter.ts @@ -90,17 +90,7 @@ export function createProtocolFilter(): Middleware { provideHover: async (document, position, token, next: (document: any, position: any, token: any) => any) => clients.ActiveClient.enqueue(async () => { const me: Client = clients.getClientFor(document.uri); if (me.TrackedDocuments.has(document.uri.toString())) { - // Currently needed to support icons. - return next(document, position, token).then((value: vscode.Hover) => { - if (value && value.contents instanceof Array) { - value.contents.forEach((content) => { - if (content instanceof vscode.MarkdownString) { - content.supportThemeIcons = true; - } - }); - } - return value; - }); + return next(document, position, token); } return null; }), diff --git a/Extension/src/LanguageServer/settings.ts b/Extension/src/LanguageServer/settings.ts index 0f1c9796c1..2c2e0fd50c 100644 --- a/Extension/src/LanguageServer/settings.ts +++ b/Extension/src/LanguageServer/settings.ts @@ -14,7 +14,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import * as which from 'which'; import { getCachedClangFormatPath, getCachedClangTidyPath, getExtensionFilePath, getRawSetting, isArray, isArrayOfString, isBoolean, isNumber, isObject, isString, isValidMapping, setCachedClangFormatPath, setCachedClangTidyPath } from '../common'; -import { isWindows, modelSelector } from '../constants'; +import { isWindows } from '../constants'; import * as telemetry from '../telemetry'; import { cachedEditorConfigLookups, DefaultClient, hasTrustedCompilerPaths } from './client'; import { getEditorConfigSettings, mapIndentationReferenceToEditorConfig, mapIndentToEditorConfig, mapNewOrSameLineToEditorConfig, mapWrapToEditorConfig } from './editorConfig'; @@ -161,7 +161,7 @@ export interface SettingsParams { codeAnalysisMaxMemory: number | null; codeAnalysisUpdateDelay: number; workspaceFolderSettings: WorkspaceFolderSettingsParams[]; - copilotHover: boolean | undefined; + copilotHover: string; } function getTarget(): vscode.ConfigurationTarget { @@ -461,37 +461,8 @@ export class CppSettings extends Settings { && this.intelliSenseEngine.toLowerCase() === "default" && vscode.workspace.getConfiguration("workbench").get("colorTheme") !== "Default High Contrast"; } - public get copilotHover(): PromiseLike { - // Check if the setting is explicitly set to enabled or disabled. - const setting = super.Section.get("copilotHover"); - if (setting === "disabled") { - return Promise.resolve(false); - } - - // Check if the user has access to vscode language model. - const vscodelm = (vscode as any).lm; - if (!vscodelm) { - return Promise.resolve(false); - } - - // Check if the user has access to Copilot. - return vscodelm.selectChatModels(modelSelector).then((models: any[]) => { - // If no models are returned, the user currently does not have access. - if (models.length === 0) { - // Register to update this setting if the user gains access. - vscodelm.onDidChangeChatModels(() => { - clients.ActiveClient.sendDidChangeSettings(); - }); - return false; - } + public get copilotHover(): string { return (vscode as any).lm ? this.getAsString("copilotHover") : "disabled"; } - if (setting === "enabled") { - return true; - } - - return telemetry.isFlightEnabled("cpp.copilotHover"); - }); - } 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"); } From fd7095d980065504abcc9048a8855f4ce5d56047 Mon Sep 17 00:00:00 2001 From: Ben McMorran Date: Wed, 9 Oct 2024 09:58:55 -0700 Subject: [PATCH 12/19] Add RAI disclaimers --- .../src/LanguageServer/Providers/CopilotHoverProvider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts index 0033901974..e9c13d660a 100644 --- a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts +++ b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts @@ -35,7 +35,7 @@ export class CopilotHoverProvider implements vscode.HoverProvider { if (!this.isNewHover(document, position)) { if (this.ready) { - const contentMarkdown = new vscode.MarkdownString(this.content); + const contentMarkdown = new vscode.MarkdownString(`$(sparkle) Copilot\n\n${this.content}\n\n_AI-generated content may be incorrect._`, true); return new vscode.Hover(contentMarkdown); } if (this.waiting) { @@ -48,7 +48,7 @@ export class CopilotHoverProvider implements vscode.HoverProvider { this.reset(); this.currentDocument = document; this.currentPosition = position; - const commandString = "$(sparkle) [" + localize("generate.copilot.description", "Generate Copilot Description") + "](command:C_Cpp.ShowCopilotHover)"; + const commandString = "$(sparkle) [" + localize("generate.copilot.description", "Generate Copilot summary") + "](command:C_Cpp.ShowCopilotHover)"; const commandMarkdown = new vscode.MarkdownString(commandString); commandMarkdown.supportThemeIcons = true; commandMarkdown.isTrusted = true; From f8cdde666c5ec5254da0bdbed69917d8c82b1a19 Mon Sep 17 00:00:00 2001 From: spebl Date: Thu, 10 Oct 2024 23:38:50 -0700 Subject: [PATCH 13/19] Copilot summary should now only show up when there is already hover content from the main hover provider. Show error message on failure to prevent getting stuck showing the spinner. --- .../Providers/CopilotHoverProvider.ts | 12 +++++-- .../LanguageServer/Providers/HoverProvider.ts | 8 +++++ Extension/src/LanguageServer/client.ts | 8 ++++- Extension/src/LanguageServer/extension.ts | 35 +++++++++---------- 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts index e9c13d660a..78d55ca007 100644 --- a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts +++ b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts @@ -26,7 +26,7 @@ export class CopilotHoverProvider implements vscode.HoverProvider { this.client = client; } - public async provideHover(document: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise { + 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") { @@ -35,7 +35,7 @@ export class CopilotHoverProvider implements vscode.HoverProvider { if (!this.isNewHover(document, position)) { if (this.ready) { - const contentMarkdown = new vscode.MarkdownString(`$(sparkle) Copilot\n\n${this.content}\n\n_AI-generated content may be incorrect._`, true); + const contentMarkdown = new vscode.MarkdownString(`$(sparkle) Copilot\n\n${this.content}\n\n_${localize("copilot.disclaimer", "AI-generated content may be incorrect.")}_`, true); return new vscode.Hover(contentMarkdown); } if (this.waiting) { @@ -46,6 +46,14 @@ export class CopilotHoverProvider implements vscode.HoverProvider { // Fresh hover, reset state. 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.currentDocument = document; this.currentPosition = position; const commandString = "$(sparkle) [" + localize("generate.copilot.description", "Generate Copilot summary") + "](command:C_Cpp.ShowCopilotHover)"; diff --git a/Extension/src/LanguageServer/Providers/HoverProvider.ts b/Extension/src/LanguageServer/Providers/HoverProvider.ts index 0a4cd6eab6..d4e7ad6629 100644 --- a/Extension/src/LanguageServer/Providers/HoverProvider.ts +++ b/Extension/src/LanguageServer/Providers/HoverProvider.ts @@ -4,17 +4,20 @@ * ------------------------------------------------------------------------------------------ */ 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 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 settings: CppSettings = new CppSettings(vscode.workspace.getWorkspaceFolder(document.uri)?.uri); if (settings.hover === "disabled") { return undefined; @@ -52,6 +55,11 @@ export class HoverProvider implements vscode.HoverProvider { hoverResult.range.end.line, hoverResult.range.end.character); } + this.hasContent.resolve(strings.length > 0); 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 12dd069e20..f74b4ccf6d 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -836,6 +836,7 @@ 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; @@ -1270,6 +1271,7 @@ export class DefaultClient implements Client { this.registerFileWatcher(); initializedClientCount = 0; this.inlayHintsProvider = new InlayHintsProvider(); + this.hoverProvider = new HoverProvider(this); const settings: CppSettings = new CppSettings(); if (settings.copilotHover === "enabled" || @@ -1277,7 +1279,7 @@ export class DefaultClient implements Client { this.copilotHoverProvider = new CopilotHoverProvider(this); this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, this.copilotHoverProvider)); } - this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, new HoverProvider(this))); + 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))); @@ -4033,6 +4035,10 @@ export class DefaultClient implements Client { DebugConfigurationProvider.ClearDetectedBuildTasks(); } + public getHoverProvider(): HoverProvider | undefined { + return this.hoverProvider; + } + public getCopilotHoverProvider(): CopilotHoverProvider | undefined { return this.copilotHoverProvider; } diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 2aa244eb70..3d3f6f6d8b 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -21,6 +21,7 @@ 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, GetIncludesResult, openFileVersions } from './client'; import { ClientCollection } from './clientCollection'; import { CodeActionDiagnosticInfo, CodeAnalysisDiagnosticIdentifiersAndUri, codeAnalysisAllFixes, codeAnalysisCodeToFixes, codeAnalysisFileToCodeActions } from './codeAnalysis'; @@ -1448,30 +1449,18 @@ async function onCopilotHover(): Promise { return; } + const errorMessage = localize("copilot.hover.error", "An error occurred while generating Copilot summary."); + // Prep hover with wait message. copilotHoverProvider.showWaiting(); - // Make sure the hover document has focus. - await vscode.window.showTextDocument(hoverDocument, { preserveFocus: false, selection: new vscode.Selection(hoverPosition, hoverPosition) }); - - // Workaround to force the editor to update it's content, needs to be called from another location first. - if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { - return; - } - await vscode.commands.executeCommand('cursorMove', { to: 'right' }); - await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus' }); - - // Move back and show the correct hover. - if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { - return; - } - await vscode.commands.executeCommand('cursorMove', { to: 'left' }); - await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus' }); + await showCopilotContent(copilotHoverProvider, hoverDocument, hoverPosition); // 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; } @@ -1493,6 +1482,7 @@ async function onCopilotHover(): Promise { } catch (err) { if (err instanceof vscode.LanguageModelError) { console.log(err.message, err.code, err.cause); + await showCopilotContent(copilotHoverProvider, hoverDocument, hoverPosition, errorMessage); } else { throw err; } @@ -1501,6 +1491,7 @@ async function onCopilotHover(): Promise { // Ensure we have a valid response from Copilot. if (!chatResponse) { + await showCopilotContent(copilotHoverProvider, hoverDocument, hoverPosition, errorMessage); return; } @@ -1511,13 +1502,19 @@ async function onCopilotHover(): Promise { 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 { // Make sure the hover document has focus. await vscode.window.showTextDocument(hoverDocument, { preserveFocus: false, selection: new vscode.Selection(hoverPosition, hoverPosition) }); @@ -1528,8 +1525,10 @@ async function onCopilotHover(): Promise { await vscode.commands.executeCommand('cursorMove', { to: 'right' }); await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus' }); - // Prepare and show the real content. - copilotHoverProvider.showContent(content); + if (content) { + copilotHoverProvider.showContent(content); + } + if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { return; } From 10fb3cfe1d8fb932755393141f74e8f0437aac20 Mon Sep 17 00:00:00 2001 From: spebl Date: Fri, 11 Oct 2024 15:59:27 -0700 Subject: [PATCH 14/19] move AI inaccuracy warning to hover tooltip, add sparkle to load, check for cancellation before refocusing the document --- .../src/LanguageServer/Providers/CopilotHoverProvider.ts | 6 +++--- Extension/src/LanguageServer/extension.ts | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts index 78d55ca007..33f696ee02 100644 --- a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts +++ b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts @@ -35,11 +35,11 @@ export class CopilotHoverProvider implements vscode.HoverProvider { if (!this.isNewHover(document, position)) { if (this.ready) { - const contentMarkdown = new vscode.MarkdownString(`$(sparkle) Copilot\n\n${this.content}\n\n_${localize("copilot.disclaimer", "AI-generated content may be incorrect.")}_`, true); + 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("$(loading~spin)", true); + const loadingMarkdown = new vscode.MarkdownString("$(sparkle) $(loading~spin)", true); return new vscode.Hover(loadingMarkdown); } } @@ -56,7 +56,7 @@ export class CopilotHoverProvider implements vscode.HoverProvider { } this.currentDocument = document; this.currentPosition = position; - const commandString = "$(sparkle) [" + localize("generate.copilot.description", "Generate Copilot summary") + "](command:C_Cpp.ShowCopilotHover)"; + 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; diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 3d3f6f6d8b..612eae47ff 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -1516,6 +1516,9 @@ async function onCopilotHover(): Promise { async function showCopilotContent(copilotHoverProvider: CopilotHoverProvider, hoverDocument: vscode.TextDocument, hoverPosition: vscode.Position, content?: string): Promise { // Make sure the hover document has focus. + if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { + return; + } await vscode.window.showTextDocument(hoverDocument, { preserveFocus: false, selection: new vscode.Selection(hoverPosition, hoverPosition) }); // Same workaround as above to force the editor to update it's content. From 3a0b8d894af963a9b01d2b4a04b6db334788e5dc Mon Sep 17 00:00:00 2001 From: spebl Date: Fri, 11 Oct 2024 16:18:58 -0700 Subject: [PATCH 15/19] prompt user to reload workspace when changing copilot hover option --- Extension/src/LanguageServer/client.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index f74b4ccf6d..61d3c73875 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -843,6 +843,7 @@ export class DefaultClient implements Client { 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 }; @@ -1274,6 +1275,7 @@ export class DefaultClient implements Client { this.hoverProvider = 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("cpp.copilotHover"))) { this.copilotHoverProvider = new CopilotHoverProvider(this); @@ -1471,6 +1473,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, From 1b37fd171a169e50b49b3344c35145d4580eebb5 Mon Sep 17 00:00:00 2001 From: spebl Date: Thu, 24 Oct 2024 14:58:00 -0700 Subject: [PATCH 16/19] track and pass cancellation token + fix copilot hover showing up before IntelliSense hover in some cases --- .../Providers/CopilotHoverProvider.ts | 24 ++++++++++++++----- Extension/src/LanguageServer/client.ts | 8 +------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts index 33f696ee02..e0fc01780b 100644 --- a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts +++ b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts @@ -16,6 +16,7 @@ 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; @@ -33,6 +34,13 @@ export class CopilotHoverProvider implements vscode.HoverProvider { return undefined; } + // Wait for the main hover provider to finish and confirm it has content. + const hoverProvider = this.client.getHoverProvider(); + if (!await hoverProvider?.contentReady) { + this.reset(); + return undefined; + } + if (!this.isNewHover(document, position)) { if (this.ready) { const contentMarkdown = new vscode.MarkdownString(`$(sparkle) Copilot\n\n${this.content}`, true); @@ -46,16 +54,12 @@ export class CopilotHoverProvider implements vscode.HoverProvider { // Fresh hover, reset state. 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.currentDocument = document; this.currentPosition = position; + this.currentCancellationToken = token; 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; @@ -86,9 +90,14 @@ export class CopilotHoverProvider implements vscode.HoverProvider { 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); + 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)) { @@ -121,6 +130,9 @@ export class CopilotHoverProvider implements vscode.HoverProvider { this.waiting = false; this.ready = false; this.content = undefined; + this.currentDocument = undefined; + this.currentPosition = undefined; + this.currentCancellationToken = undefined; } private isNewHover(document: vscode.TextDocument, position: vscode.Position): boolean { diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 61d3c73875..455705d437 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -1277,7 +1277,7 @@ export class DefaultClient implements Client { const settings: CppSettings = new CppSettings(); this.currentCopilotHoverEnabled = new PersistentWorkspaceState("cpp.copilotHover", settings.copilotHover); if (settings.copilotHover === "enabled" || - (settings.copilotHover === "default" && await telemetry.isFlightEnabled("cpp.copilotHover"))) { + (settings.copilotHover === "default" && await telemetry.isFlightEnabled("CppCopilotHover"))) { this.copilotHoverProvider = new CopilotHoverProvider(this); this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, this.copilotHoverProvider)); } @@ -4047,11 +4047,6 @@ export class DefaultClient implements Client { public getCopilotHoverProvider(): CopilotHoverProvider | undefined { return this.copilotHoverProvider; } - - public async getCopilotHoverInfo(): Promise { - await this.ready; - return this.languageClient.sendRequest(GetCopilotHoverInfoRequest, null); - } } function getLanguageServerFileName(): string { @@ -4164,7 +4159,6 @@ class NullClient implements Client { setShowConfigureIntelliSenseButton(show: boolean): void { } addTrustedCompiler(path: string): Promise { return Promise.resolve(); } getCopilotHoverProvider(): CopilotHoverProvider | undefined { return undefined; } - getCopilotHoverInfo(): Promise { return Promise.resolve({} as GetCopilotHoverInfoResult); } getIncludes(maxDepth: number, token: vscode.CancellationToken): Promise { return Promise.resolve({} as GetIncludesResult); } getChatContext(token: vscode.CancellationToken): Promise { return Promise.resolve({} as ChatContextResult); } } From 223d52b58f3b82f0f0eb71b6df23ae39d317e612 Mon Sep 17 00:00:00 2001 From: spebl Date: Mon, 28 Oct 2024 12:22:19 -0700 Subject: [PATCH 17/19] better handling of intentional cancellation by user --- .../Providers/CopilotHoverProvider.ts | 22 ++++++++------ .../LanguageServer/Providers/HoverProvider.ts | 11 +++++++ Extension/src/LanguageServer/extension.ts | 29 +++++++++++++------ 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts index e0fc01780b..4ac4d9f830 100644 --- a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts +++ b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts @@ -29,19 +29,29 @@ export class CopilotHoverProvider implements vscode.HoverProvider { 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) { - this.reset(); return undefined; } - if (!this.isNewHover(document, position)) { + 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); @@ -52,14 +62,8 @@ export class CopilotHoverProvider implements vscode.HoverProvider { } } - // Fresh hover, reset state. - this.reset(); - if (token.isCancellationRequested) { - throw new vscode.CancellationError(); - } this.currentDocument = document; this.currentPosition = position; - this.currentCancellationToken = token; 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; @@ -135,7 +139,7 @@ export class CopilotHoverProvider implements vscode.HoverProvider { this.currentCancellationToken = undefined; } - private isNewHover(document: vscode.TextDocument, position: vscode.Position): boolean { + 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 d4e7ad6629..4bc8bae199 100644 --- a/Extension/src/LanguageServer/Providers/HoverProvider.ts +++ b/Extension/src/LanguageServer/Providers/HoverProvider.ts @@ -11,6 +11,7 @@ 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; @@ -18,6 +19,15 @@ export class HoverProvider implements vscode.HoverProvider { 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; @@ -56,6 +66,7 @@ export class HoverProvider implements vscode.HoverProvider { } this.hasContent.resolve(strings.length > 0); + this.lastContent = strings; return new vscode.Hover(strings, range); } diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 0f0ad0a0f7..952ac4ef21 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -1411,7 +1411,16 @@ async function onCopilotHover(): Promise { // Prep hover with wait message. copilotHoverProvider.showWaiting(); - await showCopilotContent(copilotHoverProvider, hoverDocument, hoverPosition); + 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); @@ -1471,17 +1480,17 @@ async function onCopilotHover(): Promise { await showCopilotContent(copilotHoverProvider, hoverDocument, hoverPosition, content); } -async function showCopilotContent(copilotHoverProvider: CopilotHoverProvider, hoverDocument: vscode.TextDocument, hoverPosition: vscode.Position, content?: string): Promise { - // Make sure the hover document has focus. - if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { - return; +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)) { + return false; } - await vscode.window.showTextDocument(hoverDocument, { preserveFocus: false, selection: new vscode.Selection(hoverPosition, hoverPosition) }); - // Same workaround as above to force the editor to update it's content. if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { - return; + return false; } + await vscode.commands.executeCommand('cursorMove', { to: 'right' }); await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus' }); @@ -1490,8 +1499,10 @@ async function showCopilotContent(copilotHoverProvider: CopilotHoverProvider, ho } if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { - return; + return false; } await vscode.commands.executeCommand('cursorMove', { to: 'left' }); await vscode.commands.executeCommand('editor.action.showHover', { focus: 'noAutoFocus' }); + + return true; } From c055ce20e73e71cdb7a63005483a26e9b30dbe8a Mon Sep 17 00:00:00 2001 From: spebl Date: Mon, 28 Oct 2024 12:36:07 -0700 Subject: [PATCH 18/19] ensure tokens and cancellation are all in sync between copilot hover and the extension callback --- .../src/LanguageServer/Providers/CopilotHoverProvider.ts | 6 +++++- Extension/src/LanguageServer/extension.ts | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts index 4ac4d9f830..0d56c069e6 100644 --- a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts +++ b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts @@ -88,6 +88,10 @@ export class CopilotHoverProvider implements vscode.HoverProvider { 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 = { @@ -124,7 +128,7 @@ export class CopilotHoverProvider implements vscode.HoverProvider { return false; } - private reset(): void { + public reset(): void { // If there was a previous call, cancel it. if (this.waiting) { this.cancelled = true; diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 952ac4ef21..78cc87c367 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -1443,7 +1443,7 @@ async function onCopilotHover(): Promise { chatResponse = await model.sendRequest( messages, {}, - new vscode.CancellationTokenSource().token + copilotHoverProvider.getCurrentHoverCancellationToken() ); } catch (err) { if (err instanceof vscode.LanguageModelError) { @@ -1484,7 +1484,8 @@ async function showCopilotContent(copilotHoverProvider: CopilotHoverProvider, ho // 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)) { - return false; + // Reset implies cancellation, but we need to ensure cancellation is acknowledged before returning. + copilotHoverProvider.reset(); } if (copilotHoverProvider.isCancelled(hoverDocument, hoverPosition)) { From 76000c729915c6e32184ddcdb60d36eb86504a91 Mon Sep 17 00:00:00 2001 From: spebl Date: Wed, 13 Nov 2024 13:58:00 -0800 Subject: [PATCH 19/19] swap to use same params for copilot hover info as text document position params --- Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts | 2 +- Extension/src/LanguageServer/client.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts index 0d56c069e6..018e2279dc 100644 --- a/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts +++ b/Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts @@ -95,7 +95,7 @@ export class CopilotHoverProvider implements vscode.HoverProvider { public async getRequestInfo(document: vscode.TextDocument, position: vscode.Position): Promise { let requestInfo = ""; const params: GetCopilotHoverInfoParams = { - uri: document.uri.toString(), + textDocument: { uri: document.uri.toString() }, position: Position.create(position.line, position.character) }; diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 74ffecb402..ef2c342505 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -535,7 +535,7 @@ export interface GetIncludesResult { } export interface GetCopilotHoverInfoParams { - uri: string; + textDocument: TextDocumentIdentifier; position: Position; }