Skip to content

Commit fa80d44

Browse files
speblbenmcmorran
andauthored
Support for copilot-generated summaries in quick info. (On-the-fly docs) (microsoft#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]>
1 parent ee71e1f commit fa80d44

10 files changed

+383
-2
lines changed

Extension/package.json

+10
Original file line numberDiff line numberDiff line change
@@ -3313,6 +3313,16 @@
33133313
"default": false,
33143314
"markdownDescription": "%c_cpp.configuration.addNodeAddonIncludePaths.markdownDescription%",
33153315
"scope": "application"
3316+
},
3317+
"C_Cpp.copilotHover": {
3318+
"type": "string",
3319+
"enum": [
3320+
"default",
3321+
"disabled"
3322+
],
3323+
"default": "default",
3324+
"markdownDescription": "%c_cpp.configuration.copilotHover.markdownDescription%",
3325+
"scope": "window"
33163326
}
33173327
}
33183328
}

Extension/package.nls.json

+6
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,12 @@
768768
"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."
769769
]
770770
},
771+
"c_cpp.configuration.copilotHover.markdownDescription": {
772+
"message": "If `disabled`, no Copilot information will appear in Hover.",
773+
"comment": [
774+
"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."
775+
]
776+
},
771777
"c_cpp.configuration.renameRequiresIdentifier.markdownDescription": {
772778
"message": "If `true`, 'Rename Symbol' will require a valid C/C++ identifier.",
773779
"comment": [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All Rights Reserved.
3+
* See 'LICENSE' in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
import * as vscode from 'vscode';
6+
import { Position, ResponseError } from 'vscode-languageclient';
7+
import * as nls from 'vscode-nls';
8+
import { DefaultClient, GetCopilotHoverInfoParams, GetCopilotHoverInfoRequest } from '../client';
9+
import { RequestCancelled, ServerCancelled } from '../protocolFilter';
10+
import { CppSettings } from '../settings';
11+
12+
nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })();
13+
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
14+
15+
export class CopilotHoverProvider implements vscode.HoverProvider {
16+
private client: DefaultClient;
17+
private currentDocument: vscode.TextDocument | undefined;
18+
private currentPosition: vscode.Position | undefined;
19+
private currentCancellationToken: vscode.CancellationToken | undefined;
20+
private waiting: boolean = false;
21+
private ready: boolean = false;
22+
private cancelled: boolean = false;
23+
private cancelledDocument: vscode.TextDocument | undefined;
24+
private cancelledPosition: vscode.Position | undefined;
25+
private content: string | undefined;
26+
constructor(client: DefaultClient) {
27+
this.client = client;
28+
}
29+
30+
public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | undefined> {
31+
await this.client.ready;
32+
33+
const settings: CppSettings = new CppSettings(vscode.workspace.getWorkspaceFolder(document.uri)?.uri);
34+
if (settings.hover === "disabled") {
35+
return undefined;
36+
}
37+
38+
const newHover = this.isNewHover(document, position);
39+
if (newHover) {
40+
this.reset();
41+
}
42+
43+
// Wait for the main hover provider to finish and confirm it has content.
44+
const hoverProvider = this.client.getHoverProvider();
45+
if (!await hoverProvider?.contentReady) {
46+
return undefined;
47+
}
48+
49+
if (token.isCancellationRequested) {
50+
throw new vscode.CancellationError();
51+
}
52+
this.currentCancellationToken = token;
53+
54+
if (!newHover) {
55+
if (this.ready) {
56+
const contentMarkdown = new vscode.MarkdownString(`$(sparkle) Copilot\n\n${this.content}`, true);
57+
return new vscode.Hover(contentMarkdown);
58+
}
59+
if (this.waiting) {
60+
const loadingMarkdown = new vscode.MarkdownString("$(sparkle) $(loading~spin)", true);
61+
return new vscode.Hover(loadingMarkdown);
62+
}
63+
}
64+
65+
this.currentDocument = document;
66+
this.currentPosition = position;
67+
const commandString = "$(sparkle) [" + localize("generate.copilot.description", "Generate Copilot summary") + "](command:C_Cpp.ShowCopilotHover \"" + localize("copilot.disclaimer", "AI-generated content may be incorrect.") + "\")";
68+
const commandMarkdown = new vscode.MarkdownString(commandString);
69+
commandMarkdown.supportThemeIcons = true;
70+
commandMarkdown.isTrusted = { enabledCommands: ["C_Cpp.ShowCopilotHover"] };
71+
return new vscode.Hover(commandMarkdown);
72+
}
73+
74+
public showWaiting(): void {
75+
this.waiting = true;
76+
}
77+
78+
public showContent(content: string): void {
79+
this.ready = true;
80+
this.content = content;
81+
}
82+
83+
public getCurrentHoverDocument(): vscode.TextDocument | undefined {
84+
return this.currentDocument;
85+
}
86+
87+
public getCurrentHoverPosition(): vscode.Position | undefined {
88+
return this.currentPosition;
89+
}
90+
91+
public getCurrentHoverCancellationToken(): vscode.CancellationToken | undefined {
92+
return this.currentCancellationToken;
93+
}
94+
95+
public async getRequestInfo(document: vscode.TextDocument, position: vscode.Position): Promise<string> {
96+
let requestInfo = "";
97+
const params: GetCopilotHoverInfoParams = {
98+
textDocument: { uri: document.uri.toString() },
99+
position: Position.create(position.line, position.character)
100+
};
101+
102+
await this.client.ready;
103+
if (this.currentCancellationToken?.isCancellationRequested) {
104+
throw new vscode.CancellationError();
105+
}
106+
107+
try {
108+
const response = await this.client.languageClient.sendRequest(GetCopilotHoverInfoRequest, params, this.currentCancellationToken);
109+
requestInfo = response.content;
110+
} catch (e: any) {
111+
if (e instanceof ResponseError && (e.code === RequestCancelled || e.code === ServerCancelled)) {
112+
throw new vscode.CancellationError();
113+
}
114+
throw e;
115+
}
116+
117+
return requestInfo;
118+
}
119+
120+
public isCancelled(document: vscode.TextDocument, position: vscode.Position): boolean {
121+
if (this.cancelled && this.cancelledDocument === document && this.cancelledPosition === position) {
122+
// Cancellation is being acknowledged.
123+
this.cancelled = false;
124+
this.cancelledDocument = undefined;
125+
this.cancelledPosition = undefined;
126+
return true;
127+
}
128+
return false;
129+
}
130+
131+
public reset(): void {
132+
// If there was a previous call, cancel it.
133+
if (this.waiting) {
134+
this.cancelled = true;
135+
this.cancelledDocument = this.currentDocument;
136+
this.cancelledPosition = this.currentPosition;
137+
}
138+
this.waiting = false;
139+
this.ready = false;
140+
this.content = undefined;
141+
this.currentDocument = undefined;
142+
this.currentPosition = undefined;
143+
this.currentCancellationToken = undefined;
144+
}
145+
146+
public isNewHover(document: vscode.TextDocument, position: vscode.Position): boolean {
147+
return !(this.currentDocument === document && this.currentPosition?.line === position.line && (this.currentPosition?.character === position.character || this.currentPosition?.character === position.character - 1));
148+
}
149+
}

Extension/src/LanguageServer/Providers/HoverProvider.ts

+19
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,30 @@
44
* ------------------------------------------------------------------------------------------ */
55
import * as vscode from 'vscode';
66
import { Position, ResponseError, TextDocumentPositionParams } from 'vscode-languageclient';
7+
import { ManualSignal } from '../../Utility/Async/manualSignal';
78
import { DefaultClient, HoverRequest } from '../client';
89
import { RequestCancelled, ServerCancelled } from '../protocolFilter';
910
import { CppSettings } from '../settings';
1011

1112
export class HoverProvider implements vscode.HoverProvider {
1213
private client: DefaultClient;
14+
private lastContent: vscode.MarkdownString[] | undefined;
15+
private readonly hasContent = new ManualSignal<boolean>(true);
1316
constructor(client: DefaultClient) {
1417
this.client = client;
1518
}
1619

1720
public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | undefined> {
21+
this.hasContent.reset();
22+
const copilotHoverProvider = this.client.getCopilotHoverProvider();
23+
if (copilotHoverProvider) {
24+
// Check if this is a reinvocation from Copilot.
25+
if (!copilotHoverProvider.isNewHover(document, position) && this.lastContent) {
26+
this.hasContent.resolve(this.lastContent.length > 0);
27+
return new vscode.Hover(this.lastContent);
28+
}
29+
}
30+
1831
const settings: CppSettings = new CppSettings(vscode.workspace.getWorkspaceFolder(document.uri)?.uri);
1932
if (settings.hover === "disabled") {
2033
return undefined;
@@ -52,6 +65,12 @@ export class HoverProvider implements vscode.HoverProvider {
5265
hoverResult.range.end.line, hoverResult.range.end.character);
5366
}
5467

68+
this.hasContent.resolve(strings.length > 0);
69+
this.lastContent = strings;
5570
return new vscode.Hover(strings, range);
5671
}
72+
73+
get contentReady(): Promise<boolean> {
74+
return this.hasContent;
75+
}
5776
}

Extension/src/LanguageServer/client.ts

+43-2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { localizedStringCount, lookupString } from '../nativeStrings';
4343
import { SessionState } from '../sessionState';
4444
import * as telemetry from '../telemetry';
4545
import { TestHook, getTestHook } from '../testHook';
46+
import { CopilotHoverProvider } from './Providers/CopilotHoverProvider';
4647
import { HoverProvider } from './Providers/HoverProvider';
4748
import {
4849
CodeAnalysisDiagnosticIdentifiersAndUri,
@@ -533,6 +534,15 @@ export interface GetIncludesResult {
533534
includedFiles: string[];
534535
}
535536

537+
export interface GetCopilotHoverInfoParams {
538+
textDocument: TextDocumentIdentifier;
539+
position: Position;
540+
}
541+
542+
interface GetCopilotHoverInfoResult {
543+
content: string;
544+
}
545+
536546
export interface ChatContextResult {
537547
language: string;
538548
standardVersion: string;
@@ -567,6 +577,7 @@ export const FormatDocumentRequest: RequestType<FormatParams, FormatResult, void
567577
export const FormatRangeRequest: RequestType<FormatParams, FormatResult, void> = new RequestType<FormatParams, FormatResult, void>('cpptools/formatRange');
568578
export const FormatOnTypeRequest: RequestType<FormatParams, FormatResult, void> = new RequestType<FormatParams, FormatResult, void>('cpptools/formatOnType');
569579
export const HoverRequest: RequestType<TextDocumentPositionParams, vscode.Hover, void> = new RequestType<TextDocumentPositionParams, vscode.Hover, void>('cpptools/hover');
580+
export const GetCopilotHoverInfoRequest: RequestType<GetCopilotHoverInfoParams, GetCopilotHoverInfoResult, void> = new RequestType<GetCopilotHoverInfoParams, GetCopilotHoverInfoResult, void>('cpptools/getCopilotHoverInfo');
570581
const CreateDeclarationOrDefinitionRequest: RequestType<CreateDeclarationOrDefinitionParams, CreateDeclarationOrDefinitionResult, void> = new RequestType<CreateDeclarationOrDefinitionParams, CreateDeclarationOrDefinitionResult, void>('cpptools/createDeclDef');
571582
const ExtractToFunctionRequest: RequestType<ExtractToFunctionParams, WorkspaceEditResult, void> = new RequestType<ExtractToFunctionParams, WorkspaceEditResult, void>('cpptools/extractToFunction');
572583
const GoToDirectiveInGroupRequest: RequestType<GoToDirectiveInGroupParams, Position | undefined, void> = new RequestType<GoToDirectiveInGroupParams, Position | undefined, void>('cpptools/goToDirectiveInGroup');
@@ -804,6 +815,7 @@ export interface Client {
804815
getShowConfigureIntelliSenseButton(): boolean;
805816
setShowConfigureIntelliSenseButton(show: boolean): void;
806817
addTrustedCompiler(path: string): Promise<void>;
818+
getCopilotHoverProvider(): CopilotHoverProvider | undefined;
807819
getIncludes(maxDepth: number): Promise<GetIncludesResult>;
808820
getChatContext(uri: vscode.Uri, token: vscode.CancellationToken): Promise<ChatContextResult>;
809821
getProjectContext(uri: vscode.Uri): Promise<ProjectContextResult>;
@@ -839,11 +851,14 @@ export class DefaultClient implements Client {
839851
private settingsTracker: SettingsTracker;
840852
private loggingLevel: number = 1;
841853
private configurationProvider?: string;
854+
private hoverProvider: HoverProvider | undefined;
855+
private copilotHoverProvider: CopilotHoverProvider | undefined;
842856

843857
public lastCustomBrowseConfiguration: PersistentFolderState<WorkspaceBrowseConfiguration | undefined> | undefined;
844858
public lastCustomBrowseConfigurationProviderId: PersistentFolderState<string | undefined> | undefined;
845859
public lastCustomBrowseConfigurationProviderVersion: PersistentFolderState<Version> | undefined;
846860
public currentCaseSensitiveFileSupport: PersistentWorkspaceState<boolean> | undefined;
861+
public currentCopilotHoverEnabled: PersistentWorkspaceState<string> | undefined;
847862
private registeredProviders: PersistentFolderState<string[]> | undefined;
848863

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

1277-
this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, new HoverProvider(this)));
1293+
const settings: CppSettings = new CppSettings();
1294+
this.currentCopilotHoverEnabled = new PersistentWorkspaceState<string>("cpp.copilotHover", settings.copilotHover);
1295+
if (settings.copilotHover === "enabled" ||
1296+
(settings.copilotHover === "default" && await telemetry.isFlightEnabled("CppCopilotHover"))) {
1297+
this.copilotHoverProvider = new CopilotHoverProvider(this);
1298+
this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, this.copilotHoverProvider));
1299+
}
1300+
this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, this.hoverProvider));
12781301
this.disposables.push(vscode.languages.registerInlayHintsProvider(util.documentSelector, this.inlayHintsProvider));
12791302
this.disposables.push(vscode.languages.registerRenameProvider(util.documentSelector, new RenameProvider(this)));
12801303
this.disposables.push(vscode.languages.registerReferenceProvider(util.documentSelector, new FindAllReferencesProvider(this)));
@@ -1292,7 +1315,6 @@ export class DefaultClient implements Client {
12921315
this.codeFoldingProvider = new FoldingRangeProvider(this);
12931316
this.codeFoldingProviderDisposable = vscode.languages.registerFoldingRangeProvider(util.documentSelector, this.codeFoldingProvider);
12941317

1295-
const settings: CppSettings = new CppSettings();
12961318
if (settings.isEnhancedColorizationEnabled && semanticTokensLegend) {
12971319
this.semanticTokensProvider = new SemanticTokensProvider();
12981320
this.semanticTokensProviderDisposable = vscode.languages.registerDocumentSemanticTokensProvider(util.documentSelector, this.semanticTokensProvider, semanticTokensLegend);
@@ -1473,6 +1495,9 @@ export class DefaultClient implements Client {
14731495
if (this.currentCaseSensitiveFileSupport && workspaceSettings.isCaseSensitiveFileSupportEnabled !== this.currentCaseSensitiveFileSupport.Value) {
14741496
void util.promptForReloadWindowDueToSettingsChange();
14751497
}
1498+
if (this.currentCopilotHoverEnabled && workspaceSettings.copilotHover !== this.currentCopilotHoverEnabled.Value) {
1499+
void util.promptForReloadWindowDueToSettingsChange();
1500+
}
14761501
return {
14771502
filesAssociations: workspaceOtherSettings.filesAssociations,
14781503
workspaceFallbackEncoding: workspaceOtherSettings.filesEncoding,
@@ -1495,6 +1520,7 @@ export class DefaultClient implements Client {
14951520
codeAnalysisMaxConcurrentThreads: workspaceSettings.codeAnalysisMaxConcurrentThreads,
14961521
codeAnalysisMaxMemory: workspaceSettings.codeAnalysisMaxMemory,
14971522
codeAnalysisUpdateDelay: workspaceSettings.codeAnalysisUpdateDelay,
1523+
copilotHover: workspaceSettings.copilotHover,
14981524
workspaceFolderSettings: workspaceFolderSettingsParams
14991525
};
15001526
}
@@ -1605,6 +1631,12 @@ export class DefaultClient implements Client {
16051631
// We manually restart the language server so tell the LanguageClient not to do it automatically for us.
16061632
return { action: CloseAction.DoNotRestart, message };
16071633
}
1634+
},
1635+
markdown: {
1636+
isTrusted: true
1637+
// TODO: support for icons in markdown is not yet in the released version of vscode-languageclient.
1638+
// Based on PR (https://github.com/microsoft/vscode-languageserver-node/pull/1504)
1639+
//supportThemeIcons: true
16081640
}
16091641

16101642
// TODO: should I set the output channel? Does this sort output between servers?
@@ -4045,6 +4077,14 @@ export class DefaultClient implements Client {
40454077
compilerDefaults = await this.requestCompiler(path);
40464078
DebugConfigurationProvider.ClearDetectedBuildTasks();
40474079
}
4080+
4081+
public getHoverProvider(): HoverProvider | undefined {
4082+
return this.hoverProvider;
4083+
}
4084+
4085+
public getCopilotHoverProvider(): CopilotHoverProvider | undefined {
4086+
return this.copilotHoverProvider;
4087+
}
40484088
}
40494089

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

0 commit comments

Comments
 (0)