Skip to content

Commit

Permalink
Initial IssueUriRequestHandler proposed API (#180363)
Browse files Browse the repository at this point in the history
  • Loading branch information
TylerLeonhardt authored Apr 19, 2023
1 parent bc0bdd3 commit 0724039
Show file tree
Hide file tree
Showing 14 changed files with 299 additions and 29 deletions.
68 changes: 53 additions & 15 deletions src/vs/code/electron-sandbox/issue/issueReporterMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,19 @@ export class IssueReporter extends Disposable {
this.updatePreviewButtonState();
});

ipcRenderer.on('vscode:getIssueReporterUriResponse', (_: unknown, args: { extensionId: string; uri?: string; error?: string }) => {
const extension = this.issueReporterModel.getData().allExtensions.find(extension => extension.id === args.extensionId);
if (extension) {
if (args.error) {
extension.hasIssueUriRequestHandler = false;
// The issue handler failed so fall back to old issue reporter experience.
this.renderBlocks();
} else {
extension.bugsUrl = args.uri;
}
}
});

ipcRenderer.send('vscode:issueSystemInfoRequest');
if (configuration.data.issueType === IssueType.PerformanceIssue) {
ipcRenderer.send('vscode:issuePerformanceInfoRequest');
Expand Down Expand Up @@ -692,7 +705,7 @@ export class IssueReporter extends Disposable {

private renderBlocks(): void {
// Depending on Issue Type, we render different blocks and text
const { issueType, fileOnExtension, fileOnMarketplace } = this.issueReporterModel.getData();
const { issueType, fileOnExtension, fileOnMarketplace, selectedExtension } = this.issueReporterModel.getData();
const blockContainer = this.getElementById('block-container');
const systemBlock = document.querySelector('.block-system');
const processBlock = document.querySelector('.block-process');
Expand All @@ -705,6 +718,9 @@ export class IssueReporter extends Disposable {
const descriptionSubtitle = this.getElementById('issue-description-subtitle')!;
const extensionSelector = this.getElementById('extension-selection')!;

const titleTextArea = this.getElementById('issue-title-container')!;
const descriptionTextArea = this.getElementById('description')!;

// Hide all by default
hide(blockContainer);
hide(systemBlock);
Expand All @@ -715,25 +731,36 @@ export class IssueReporter extends Disposable {
hide(problemSource);
hide(extensionSelector);

if (issueType === IssueType.Bug) {
show(problemSource);
show(problemSource);
show(titleTextArea);
show(descriptionTextArea);

if (fileOnExtension) {
show(extensionSelector);
}

if (fileOnExtension && selectedExtension?.hasIssueUriRequestHandler) {
hide(titleTextArea);
hide(descriptionTextArea);
reset(descriptionTitle, localize('handlesIssuesElsewhere', "This extension handles issues outside of VS Code"));
reset(descriptionSubtitle, localize('elsewhereDescription', "The '{0}' extension prefers to use an external issue reporter. To be taken to that issue reporting experience, click the button below.", selectedExtension.displayName));
this.previewButton.label = localize('openIssueReporter', "Open Issue Reporter");
return;
}

if (issueType === IssueType.Bug) {
if (!fileOnMarketplace) {
show(blockContainer);
show(systemBlock);
show(experimentsBlock);
if (!fileOnExtension) {
show(extensionsBlock);
}
}

if (fileOnExtension) {
show(extensionSelector);
} else if (!fileOnMarketplace) {
show(extensionsBlock);
}
reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce") + ' ', $('span.required-input', undefined, '*'));
reset(descriptionSubtitle, localize('bugDescription', "Share the steps needed to reliably reproduce the problem. Please include actual and expected results. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub."));
} else if (issueType === IssueType.PerformanceIssue) {
show(problemSource);

if (!fileOnMarketplace) {
show(blockContainer);
show(systemBlock);
Expand All @@ -753,11 +780,6 @@ export class IssueReporter extends Disposable {
} else if (issueType === IssueType.FeatureRequest) {
reset(descriptionTitle, localize('description', "Description") + ' ', $('span.required-input', undefined, '*'));
reset(descriptionSubtitle, localize('featureRequestDescription', "Please describe the feature you would like to see. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub."));
show(problemSource);

if (fileOnExtension) {
show(extensionSelector);
}
}
}

Expand Down Expand Up @@ -818,6 +840,16 @@ export class IssueReporter extends Disposable {
}

private async createIssue(): Promise<boolean> {
// Short circuit if the extension provides a custom issue handler
if (this.issueReporterModel.getData().selectedExtension?.hasIssueUriRequestHandler) {
const url = this.getExtensionBugsUrl();
if (url) {
this.hasBeenSubmitted = true;
ipcRenderer.send('vscode:openExternal', url);
return true;
}
}

if (!this.validateInputs()) {
// If inputs are invalid, set focus to the first one and add listeners on them
// to detect further changes
Expand Down Expand Up @@ -1062,13 +1094,19 @@ export class IssueReporter extends Disposable {
this.issueReporterModel.update({ selectedExtension: matches[0] });
this.validateSelectedExtension();

if (matches[0].hasIssueUriRequestHandler) {
ipcRenderer.send('vscode:getIssueReporterUriRequest', matches[0].id);
}

const title = (<HTMLInputElement>this.getElementById('issue-title')).value;
this.searchExtensionIssues(title);
} else {
this.issueReporterModel.update({ selectedExtension: undefined });
this.clearSearchResults();
this.validateSelectedExtension();
}
this.updatePreviewButtonState();
this.renderBlocks();
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/vs/code/electron-sandbox/issue/issueReporterPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default (): string => `
</div>
</div>
<div class="input-group">
<div id="issue-title-container" class="input-group">
<label class="inline-label" for="issue-title">${escape(localize('issueTitleLabel', "Title"))} <span class="required-input">*</span></label>
<input id="issue-title" type="text" class="inline-form-control" placeholder="${escape(localize('issueTitleRequired', "Please enter a title."))}" required>
<div id="issue-title-empty-error" class="validation-error hidden" role="alert">${escape(localize('titleEmptyValidation', "A title is required."))}</div>
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/issue/common/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface IssueReporterExtensionData {
displayName: string | undefined;
repositoryUrl: string | undefined;
bugsUrl: string | undefined;
hasIssueUriRequestHandler?: boolean;
}

export interface IssueReporterData extends WindowData {
Expand Down
42 changes: 41 additions & 1 deletion src/vs/platform/issue/electron-main/issueMainService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import { randomPath } from 'vs/base/common/extpath';
import { withNullAsUndefined } from 'vs/base/common/types';
import { IStateService } from 'vs/platform/state/node/state';
import { UtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess';
import { CancellationToken } from 'vs/base/common/cancellation';
import { URI } from 'vs/base/common/uri';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { Promises, timeout } from 'vs/base/common/async';

export const IIssueMainService = createDecorator<IIssueMainService>('issueMainService');
const processExplorerWindowState = 'issue.processExplorerWindowState';
Expand Down Expand Up @@ -67,7 +71,8 @@ export class IssueMainService implements IIssueMainService {
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
@IProtocolMainService private readonly protocolMainService: IProtocolMainService,
@IProductService private readonly productService: IProductService,
@IStateService private readonly stateService: IStateService
@IStateService private readonly stateService: IStateService,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
) {
this.registerListeners();
}
Expand Down Expand Up @@ -193,6 +198,41 @@ export class IssueMainService implements IIssueMainService {

this.safeSend(event, 'vscode:pidToNameResponse', pidToNames);
});

validatedIpcMain.on('vscode:getIssueReporterUriRequest', async (event, extensionId: string) => {
try {
const res = await this.getIssueReporterUri(extensionId, CancellationToken.None);
this.safeSend(event, 'vscode:getIssueReporterUriResponse', { extensionId, uri: res.toString(true) });
} catch (e) {
this.logService.error(e);
this.safeSend(event, 'vscode:getIssueReporterUriResponse', { extensionId, error: e.message ?? e.toString() ?? 'Unknown Error' });
}
});
}

async getIssueReporterUri(extensionId: string, token: CancellationToken): Promise<URI> {
if (!this.issueReporterParentWindow) {
throw new Error('Issue reporter window not available');
}
const window = this.windowsMainService.getWindowById(this.issueReporterParentWindow.id);
if (!window) {
throw new Error('Window not found');
}
const replyChannel = `vscode:triggerIssueUriRequestHandlerResponse${window.id}`;
return Promises.withAsyncBody<URI>(async (resolve, reject) => {
window.sendWhenReady('vscode:triggerIssueUriRequestHandler', token, { replyChannel, extensionId });

validatedIpcMain.once(replyChannel, (_: unknown, data: string) => {
resolve(URI.parse(data));
});

try {
await timeout(5000, token);
reject(new Error('Timed out waiting for issue reporter URI'));
} finally {
validatedIpcMain.removeHandler(replyChannel);
}
});
}

private safeSend(event: IpcMainEvent, channel: string, ...args: unknown[]): void {
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/browser/extensionHost.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import './mainThreadTesting';
import './mainThreadSecretState';
import './mainThreadProfilContentHandlers';
import './mainThreadSemanticSimilarity';
import './mainThreadIssueReporter';

export class ExtensionPoints implements IWorkbenchContribution {

Expand Down
39 changes: 39 additions & 0 deletions src/vs/workbench/api/browser/mainThreadIssueReporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, DisposableMap } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ExtHostContext, ExtHostIssueReporterShape, MainContext, MainThreadIssueReporterShape } from 'vs/workbench/api/common/extHost.protocol';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { IIssueUriRequestHandler, IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue';

@extHostNamedCustomer(MainContext.MainThreadIssueReporter)
export class MainThreadIssueReporter extends Disposable implements MainThreadIssueReporterShape {
private readonly _proxy: ExtHostIssueReporterShape;
private readonly _registrations = this._register(new DisposableMap<string>());

constructor(
context: IExtHostContext,
@IWorkbenchIssueService private readonly _issueService: IWorkbenchIssueService
) {
super();
this._proxy = context.getProxy(ExtHostContext.ExtHostIssueReporter);
}

$registerIssueUriRequestHandler(extensionId: string): void {
const handler: IIssueUriRequestHandler = {
provideIssueUrl: async (token: CancellationToken) => {
const parts = await this._proxy.$getIssueReporterUri(extensionId, token);
return URI.from(parts);
}
};
this._registrations.set(extensionId, this._issueService.registerIssueUriRequestHandler(extensionId, handler));
}

$unregisterIssueUriRequestHandler(extensionId: string): void {
this._registrations.deleteAndDispose(extensionId);
}
}
6 changes: 6 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import { ExtHostInteractiveSession } from 'vs/workbench/api/common/extHostIntera
import { ExtHostInteractiveEditor } from 'vs/workbench/api/common/extHostInteractiveEditor';
import { ExtHostNotebookDocumentSaveParticipant } from 'vs/workbench/api/common/extHostNotebookDocumentSaveParticipant';
import { ExtHostSemanticSimilarity } from 'vs/workbench/api/common/extHostSemanticSimilarity';
import { ExtHostIssueReporter } from 'vs/workbench/api/common/extHostIssueReporter';

export interface IExtensionRegistries {
mine: ExtensionDescriptionRegistry;
Expand Down Expand Up @@ -199,6 +200,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInteractiveEditor, new ExtHostInteractiveEditor(rpcProtocol, extHostDocuments, extHostLogService));
const extHostInteractiveSession = rpcProtocol.set(ExtHostContext.ExtHostInteractiveSession, new ExtHostInteractiveSession(rpcProtocol, extHostLogService));
const extHostSemanticSimilarity = rpcProtocol.set(ExtHostContext.ExtHostSemanticSimilarity, new ExtHostSemanticSimilarity(rpcProtocol));
const extHostIssueReporter = rpcProtocol.set(ExtHostContext.ExtHostIssueReporter, new ExtHostIssueReporter(rpcProtocol));

// Check that no named customers are missing
const expected = Object.values<ProxyIdentifier<any>>(ExtHostContext);
Expand Down Expand Up @@ -388,6 +390,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
},
get onDidChangeLogLevel() {
return extHostLogService.onDidChangeLogLevel;
},
registerIssueUriRequestHandler(handler: vscode.IssueUriRequestHandler) {
checkProposedApiEnabled(extension, 'handleIssueUri');
return extHostIssueReporter.registerIssueUriRequestHandler(extension, handler);
}
};
if (!initData.environment.extensionTestsLocationURI) {
Expand Down
13 changes: 12 additions & 1 deletion src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2295,6 +2295,15 @@ export interface MainThreadLocalizationShape extends IDisposable {
$fetchBundleContents(uriComponents: UriComponents): Promise<string>;
}

export interface ExtHostIssueReporterShape {
$getIssueReporterUri(extensionId: string, token: CancellationToken): Promise<UriComponents>;
}

export interface MainThreadIssueReporterShape extends IDisposable {
$registerIssueUriRequestHandler(extensionId: string): void;
$unregisterIssueUriRequestHandler(extensionId: string): void;
}

export interface TunnelDto {
remoteAddress: { port: number; host: string };
localAddress: { port: number; host: string } | string;
Expand Down Expand Up @@ -2482,7 +2491,8 @@ export const MainContext = {
MainThreadTimeline: createProxyIdentifier<MainThreadTimelineShape>('MainThreadTimeline'),
MainThreadTesting: createProxyIdentifier<MainThreadTestingShape>('MainThreadTesting'),
MainThreadLocalization: createProxyIdentifier<MainThreadLocalizationShape>('MainThreadLocalizationShape'),
MainThreadSemanticSimilarity: createProxyIdentifier<MainThreadSemanticSimilarityShape>('MainThreadSemanticSimilarity')
MainThreadSemanticSimilarity: createProxyIdentifier<MainThreadSemanticSimilarityShape>('MainThreadSemanticSimilarity'),
MainThreadIssueReporter: createProxyIdentifier<MainThreadIssueReporterShape>('MainThreadIssueReporter'),
};

export const ExtHostContext = {
Expand Down Expand Up @@ -2544,4 +2554,5 @@ export const ExtHostContext = {
ExtHostTesting: createProxyIdentifier<ExtHostTestingShape>('ExtHostTesting'),
ExtHostTelemetry: createProxyIdentifier<ExtHostTelemetryShape>('ExtHostTelemetry'),
ExtHostLocalization: createProxyIdentifier<ExtHostLocalizationShape>('ExtHostLocalization'),
ExtHostIssueReporter: createProxyIdentifier<ExtHostIssueReporterShape>('ExtHostIssueReporter'),
};
50 changes: 50 additions & 0 deletions src/vs/workbench/api/common/extHostIssueReporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { IssueUriRequestHandler } from 'vscode';
import { CancellationToken } from 'vs/base/common/cancellation';
import { UriComponents } from 'vs/base/common/uri';
import { ExtHostIssueReporterShape, IMainContext, MainContext, MainThreadIssueReporterShape } from 'vs/workbench/api/common/extHost.protocol';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { Disposable } from 'vs/workbench/api/common/extHostTypes';

export class ExtHostIssueReporter implements ExtHostIssueReporterShape {
private _IssueUriRequestHandlers: Map<string, IssueUriRequestHandler> = new Map();

private readonly _proxy: MainThreadIssueReporterShape;

constructor(
mainContext: IMainContext
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadIssueReporter);
}

async $getIssueReporterUri(extensionId: string, token: CancellationToken): Promise<UriComponents> {
if (this._IssueUriRequestHandlers.size === 0) {
throw new Error('No issue request handlers registered');
}

const provider = this._IssueUriRequestHandlers.get(extensionId);
if (!provider) {
throw new Error('Issue request handler not found');
}

const result = await provider.handleIssueUrlRequest();
if (!result) {
throw new Error('Issue request handler returned no result');
}
return result;
}

registerIssueUriRequestHandler(extension: IExtensionDescription, provider: IssueUriRequestHandler): Disposable {
const extensionId = extension.identifier.value;
this._IssueUriRequestHandlers.set(extensionId, provider);
this._proxy.$registerIssueUriRequestHandler(extensionId);
return new Disposable(() => {
this._proxy.$unregisterIssueUriRequestHandler(extensionId);
this._IssueUriRequestHandlers.delete(extensionId);
});
}
}
Loading

0 comments on commit 0724039

Please sign in to comment.