Skip to content

Commit

Permalink
Support for copilot-generated summaries in quick info. (On-the-fly do…
Browse files Browse the repository at this point in the history
…cs) (#12552)

Off by default, can be explicitly disabled in the setting.

Co-authored-by: Ben McMorran <[email protected]>

---------

Co-authored-by: Ben McMorran <[email protected]>
  • Loading branch information
spebl and benmcmorran authored Dec 9, 2024
1 parent ee71e1f commit fa80d44
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 2 deletions.
10 changes: 10 additions & 0 deletions Extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3313,6 +3313,16 @@
"default": false,
"markdownDescription": "%c_cpp.configuration.addNodeAddonIncludePaths.markdownDescription%",
"scope": "application"
},
"C_Cpp.copilotHover": {
"type": "string",
"enum": [
"default",
"disabled"
],
"default": "default",
"markdownDescription": "%c_cpp.configuration.copilotHover.markdownDescription%",
"scope": "window"
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions Extension/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,12 @@
"Markdown text between `` should not be translated or localized (they represent literal text) and the capitalization, spacing, and punctuation (including the ``) should not be altered."
]
},
"c_cpp.configuration.copilotHover.markdownDescription": {
"message": "If `disabled`, no Copilot information will appear in Hover.",
"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": [
Expand Down
149 changes: 149 additions & 0 deletions Extension/src/LanguageServer/Providers/CopilotHoverProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import * as vscode from 'vscode';
import { Position, ResponseError } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import { DefaultClient, GetCopilotHoverInfoParams, GetCopilotHoverInfoRequest } from '../client';
import { RequestCancelled, ServerCancelled } from '../protocolFilter';
import { CppSettings } from '../settings';

nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();

export class CopilotHoverProvider implements vscode.HoverProvider {
private client: DefaultClient;
private currentDocument: vscode.TextDocument | undefined;
private currentPosition: vscode.Position | undefined;
private currentCancellationToken: vscode.CancellationToken | undefined;
private waiting: boolean = false;
private ready: boolean = false;
private cancelled: boolean = false;
private cancelledDocument: vscode.TextDocument | undefined;
private cancelledPosition: vscode.Position | undefined;
private content: string | undefined;
constructor(client: DefaultClient) {
this.client = client;
}

public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | undefined> {
await this.client.ready;

const settings: CppSettings = new CppSettings(vscode.workspace.getWorkspaceFolder(document.uri)?.uri);
if (settings.hover === "disabled") {
return undefined;
}

const newHover = this.isNewHover(document, position);
if (newHover) {
this.reset();
}

// Wait for the main hover provider to finish and confirm it has content.
const hoverProvider = this.client.getHoverProvider();
if (!await hoverProvider?.contentReady) {
return undefined;
}

if (token.isCancellationRequested) {
throw new vscode.CancellationError();
}
this.currentCancellationToken = token;

if (!newHover) {
if (this.ready) {
const contentMarkdown = new vscode.MarkdownString(`$(sparkle) Copilot\n\n${this.content}`, true);
return new vscode.Hover(contentMarkdown);
}
if (this.waiting) {
const loadingMarkdown = new vscode.MarkdownString("$(sparkle) $(loading~spin)", true);
return new vscode.Hover(loadingMarkdown);
}
}

this.currentDocument = document;
this.currentPosition = position;
const commandString = "$(sparkle) [" + localize("generate.copilot.description", "Generate Copilot summary") + "](command:C_Cpp.ShowCopilotHover \"" + localize("copilot.disclaimer", "AI-generated content may be incorrect.") + "\")";
const commandMarkdown = new vscode.MarkdownString(commandString);
commandMarkdown.supportThemeIcons = true;
commandMarkdown.isTrusted = { enabledCommands: ["C_Cpp.ShowCopilotHover"] };
return new vscode.Hover(commandMarkdown);
}

public showWaiting(): void {
this.waiting = true;
}

public showContent(content: string): void {
this.ready = true;
this.content = content;
}

public getCurrentHoverDocument(): vscode.TextDocument | undefined {
return this.currentDocument;
}

public getCurrentHoverPosition(): vscode.Position | undefined {
return this.currentPosition;
}

public getCurrentHoverCancellationToken(): vscode.CancellationToken | undefined {
return this.currentCancellationToken;
}

public async getRequestInfo(document: vscode.TextDocument, position: vscode.Position): Promise<string> {
let requestInfo = "";
const params: GetCopilotHoverInfoParams = {
textDocument: { uri: document.uri.toString() },
position: Position.create(position.line, position.character)
};

await this.client.ready;
if (this.currentCancellationToken?.isCancellationRequested) {
throw new vscode.CancellationError();
}

try {
const response = await this.client.languageClient.sendRequest(GetCopilotHoverInfoRequest, params, this.currentCancellationToken);
requestInfo = response.content;
} catch (e: any) {
if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) {
throw new vscode.CancellationError();
}
throw e;
}

return requestInfo;
}

public isCancelled(document: vscode.TextDocument, position: vscode.Position): boolean {
if (this.cancelled && this.cancelledDocument === document && this.cancelledPosition === position) {
// Cancellation is being acknowledged.
this.cancelled = false;
this.cancelledDocument = undefined;
this.cancelledPosition = undefined;
return true;
}
return false;
}

public reset(): void {
// If there was a previous call, cancel it.
if (this.waiting) {
this.cancelled = true;
this.cancelledDocument = this.currentDocument;
this.cancelledPosition = this.currentPosition;
}
this.waiting = false;
this.ready = false;
this.content = undefined;
this.currentDocument = undefined;
this.currentPosition = undefined;
this.currentCancellationToken = undefined;
}

public isNewHover(document: vscode.TextDocument, position: vscode.Position): boolean {
return !(this.currentDocument === document && this.currentPosition?.line === position.line && (this.currentPosition?.character === position.character || this.currentPosition?.character === position.character - 1));
}
}
19 changes: 19 additions & 0 deletions Extension/src/LanguageServer/Providers/HoverProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,30 @@
* ------------------------------------------------------------------------------------------ */
import * as vscode from 'vscode';
import { Position, ResponseError, TextDocumentPositionParams } from 'vscode-languageclient';
import { ManualSignal } from '../../Utility/Async/manualSignal';
import { DefaultClient, HoverRequest } from '../client';
import { RequestCancelled, ServerCancelled } from '../protocolFilter';
import { CppSettings } from '../settings';

export class HoverProvider implements vscode.HoverProvider {
private client: DefaultClient;
private lastContent: vscode.MarkdownString[] | undefined;
private readonly hasContent = new ManualSignal<boolean>(true);
constructor(client: DefaultClient) {
this.client = client;
}

public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | undefined> {
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;
Expand Down Expand Up @@ -52,6 +65,12 @@ export class HoverProvider implements vscode.HoverProvider {
hoverResult.range.end.line, hoverResult.range.end.character);
}

this.hasContent.resolve(strings.length > 0);
this.lastContent = strings;
return new vscode.Hover(strings, range);
}

get contentReady(): Promise<boolean> {
return this.hasContent;
}
}
45 changes: 43 additions & 2 deletions Extension/src/LanguageServer/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -533,6 +534,15 @@ export interface GetIncludesResult {
includedFiles: string[];
}

export interface GetCopilotHoverInfoParams {
textDocument: TextDocumentIdentifier;
position: Position;
}

interface GetCopilotHoverInfoResult {
content: string;
}

export interface ChatContextResult {
language: string;
standardVersion: string;
Expand Down Expand Up @@ -567,6 +577,7 @@ export const FormatDocumentRequest: RequestType<FormatParams, FormatResult, void
export const FormatRangeRequest: RequestType<FormatParams, FormatResult, void> = new RequestType<FormatParams, FormatResult, void>('cpptools/formatRange');
export const FormatOnTypeRequest: RequestType<FormatParams, FormatResult, void> = new RequestType<FormatParams, FormatResult, void>('cpptools/formatOnType');
export const HoverRequest: RequestType<TextDocumentPositionParams, vscode.Hover, void> = new RequestType<TextDocumentPositionParams, vscode.Hover, void>('cpptools/hover');
export const GetCopilotHoverInfoRequest: RequestType<GetCopilotHoverInfoParams, GetCopilotHoverInfoResult, void> = new RequestType<GetCopilotHoverInfoParams, GetCopilotHoverInfoResult, void>('cpptools/getCopilotHoverInfo');
const CreateDeclarationOrDefinitionRequest: RequestType<CreateDeclarationOrDefinitionParams, CreateDeclarationOrDefinitionResult, void> = new RequestType<CreateDeclarationOrDefinitionParams, CreateDeclarationOrDefinitionResult, void>('cpptools/createDeclDef');
const ExtractToFunctionRequest: RequestType<ExtractToFunctionParams, WorkspaceEditResult, void> = new RequestType<ExtractToFunctionParams, WorkspaceEditResult, void>('cpptools/extractToFunction');
const GoToDirectiveInGroupRequest: RequestType<GoToDirectiveInGroupParams, Position | undefined, void> = new RequestType<GoToDirectiveInGroupParams, Position | undefined, void>('cpptools/goToDirectiveInGroup');
Expand Down Expand Up @@ -804,6 +815,7 @@ export interface Client {
getShowConfigureIntelliSenseButton(): boolean;
setShowConfigureIntelliSenseButton(show: boolean): void;
addTrustedCompiler(path: string): Promise<void>;
getCopilotHoverProvider(): CopilotHoverProvider | undefined;
getIncludes(maxDepth: number): Promise<GetIncludesResult>;
getChatContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise<ChatContextResult>;
getProjectContext(uri: vscode.Uri): Promise<ProjectContextResult>;
Expand Down Expand Up @@ -839,11 +851,14 @@ export class DefaultClient implements Client {
private settingsTracker: SettingsTracker;
private loggingLevel: number = 1;
private configurationProvider?: string;
private hoverProvider: HoverProvider | undefined;
private copilotHoverProvider: CopilotHoverProvider | undefined;

public lastCustomBrowseConfiguration: PersistentFolderState<WorkspaceBrowseConfiguration | undefined> | undefined;
public lastCustomBrowseConfigurationProviderId: PersistentFolderState<string | undefined> | undefined;
public lastCustomBrowseConfigurationProviderVersion: PersistentFolderState<Version> | undefined;
public currentCaseSensitiveFileSupport: PersistentWorkspaceState<boolean> | undefined;
public currentCopilotHoverEnabled: PersistentWorkspaceState<string> | undefined;
private registeredProviders: PersistentFolderState<string[]> | undefined;

private configStateReceived: ConfigStateReceived = { compilers: false, compileCommands: false, configProviders: undefined, timeout: false };
Expand Down Expand Up @@ -1273,8 +1288,16 @@ export class DefaultClient implements Client {
this.registerFileWatcher();
initializedClientCount = 0;
this.inlayHintsProvider = new InlayHintsProvider();
this.hoverProvider = new HoverProvider(this);

this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, new HoverProvider(this)));
const settings: CppSettings = new CppSettings();
this.currentCopilotHoverEnabled = new PersistentWorkspaceState<string>("cpp.copilotHover", settings.copilotHover);
if (settings.copilotHover === "enabled" ||
(settings.copilotHover === "default" && await telemetry.isFlightEnabled("CppCopilotHover"))) {
this.copilotHoverProvider = new CopilotHoverProvider(this);
this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, this.copilotHoverProvider));
}
this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, this.hoverProvider));
this.disposables.push(vscode.languages.registerInlayHintsProvider(util.documentSelector, this.inlayHintsProvider));
this.disposables.push(vscode.languages.registerRenameProvider(util.documentSelector, new RenameProvider(this)));
this.disposables.push(vscode.languages.registerReferenceProvider(util.documentSelector, new FindAllReferencesProvider(this)));
Expand All @@ -1292,7 +1315,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);
Expand Down Expand Up @@ -1473,6 +1495,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,
Expand All @@ -1495,6 +1520,7 @@ export class DefaultClient implements Client {
codeAnalysisMaxConcurrentThreads: workspaceSettings.codeAnalysisMaxConcurrentThreads,
codeAnalysisMaxMemory: workspaceSettings.codeAnalysisMaxMemory,
codeAnalysisUpdateDelay: workspaceSettings.codeAnalysisUpdateDelay,
copilotHover: workspaceSettings.copilotHover,
workspaceFolderSettings: workspaceFolderSettingsParams
};
}
Expand Down Expand Up @@ -1605,6 +1631,12 @@ export class DefaultClient implements Client {
// We manually restart the language server so tell the LanguageClient not to do it automatically for us.
return { action: CloseAction.DoNotRestart, message };
}
},
markdown: {
isTrusted: true
// TODO: support for icons in markdown is not yet in the released version of vscode-languageclient.
// Based on PR (https://github.com/microsoft/vscode-languageserver-node/pull/1504)
//supportThemeIcons: true
}

// TODO: should I set the output channel? Does this sort output between servers?
Expand Down Expand Up @@ -4045,6 +4077,14 @@ export class DefaultClient implements Client {
compilerDefaults = await this.requestCompiler(path);
DebugConfigurationProvider.ClearDetectedBuildTasks();
}

public getHoverProvider(): HoverProvider | undefined {
return this.hoverProvider;
}

public getCopilotHoverProvider(): CopilotHoverProvider | undefined {
return this.copilotHoverProvider;
}
}

function getLanguageServerFileName(): string {
Expand Down Expand Up @@ -4156,6 +4196,7 @@ class NullClient implements Client {
getShowConfigureIntelliSenseButton(): boolean { return false; }
setShowConfigureIntelliSenseButton(show: boolean): void { }
addTrustedCompiler(path: string): Promise<void> { return Promise.resolve(); }
getCopilotHoverProvider(): CopilotHoverProvider | undefined { return undefined; }
getIncludes(maxDepth: number): Promise<GetIncludesResult> { return Promise.resolve({} as GetIncludesResult); }
getChatContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise<ChatContextResult> { return Promise.resolve({} as ChatContextResult); }
getProjectContext(uri: vscode.Uri): Promise<ProjectContextResult> { return Promise.resolve({} as ProjectContextResult); }
Expand Down
Loading

0 comments on commit fa80d44

Please sign in to comment.