From 07240392702c15ac5b5bc061fbf1dfb2da853085 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 19 Apr 2023 14:18:44 -0700 Subject: [PATCH] Initial IssueUriRequestHandler proposed API (#180363) --- .../issue/issueReporterMain.ts | 68 +++++++++++++++---- .../issue/issueReporterPage.ts | 2 +- src/vs/platform/issue/common/issue.ts | 1 + .../issue/electron-main/issueMainService.ts | 42 +++++++++++- .../api/browser/extensionHost.contribution.ts | 1 + .../api/browser/mainThreadIssueReporter.ts | 39 +++++++++++ .../workbench/api/common/extHost.api.impl.ts | 6 ++ .../workbench/api/common/extHost.protocol.ts | 13 +++- .../api/common/extHostIssueReporter.ts | 50 ++++++++++++++ .../contrib/issue/browser/issueService.ts | 46 +++++++++++-- .../common/extensionsApiProposals.ts | 1 + .../workbench/services/issue/common/issue.ts | 8 +++ .../issue/electron-sandbox/issueService.ts | 34 ++++++++-- .../vscode.proposed.handleIssueUri.d.ts | 17 +++++ 14 files changed, 299 insertions(+), 29 deletions(-) create mode 100644 src/vs/workbench/api/browser/mainThreadIssueReporter.ts create mode 100644 src/vs/workbench/api/common/extHostIssueReporter.ts create mode 100644 src/vscode-dts/vscode.proposed.handleIssueUri.d.ts diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index a7687cdc47fb9..b23c22e475ac3 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -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'); @@ -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'); @@ -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); @@ -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); @@ -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); - } } } @@ -818,6 +840,16 @@ export class IssueReporter extends Disposable { } private async createIssue(): Promise { + // 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 @@ -1062,6 +1094,10 @@ 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 = (this.getElementById('issue-title')).value; this.searchExtensionIssues(title); } else { @@ -1069,6 +1105,8 @@ export class IssueReporter extends Disposable { this.clearSearchResults(); this.validateSelectedExtension(); } + this.updatePreviewButtonState(); + this.renderBlocks(); }); } diff --git a/src/vs/code/electron-sandbox/issue/issueReporterPage.ts b/src/vs/code/electron-sandbox/issue/issueReporterPage.ts index f8988669091bb..0eedb215f1c96 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterPage.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterPage.ts @@ -59,7 +59,7 @@ export default (): string => ` -
+
diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index c6fa575d75401..0c2b9eb130e6f 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -50,6 +50,7 @@ export interface IssueReporterExtensionData { displayName: string | undefined; repositoryUrl: string | undefined; bugsUrl: string | undefined; + hasIssueUriRequestHandler?: boolean; } export interface IssueReporterData extends WindowData { diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index 5ac173fffe057..759240c32767c 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -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('issueMainService'); const processExplorerWindowState = 'issue.processExplorerWindowState'; @@ -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(); } @@ -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 { + 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(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 { diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index de3b9096221ae..65aeecacbf53a 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -82,6 +82,7 @@ import './mainThreadTesting'; import './mainThreadSecretState'; import './mainThreadProfilContentHandlers'; import './mainThreadSemanticSimilarity'; +import './mainThreadIssueReporter'; export class ExtensionPoints implements IWorkbenchContribution { diff --git a/src/vs/workbench/api/browser/mainThreadIssueReporter.ts b/src/vs/workbench/api/browser/mainThreadIssueReporter.ts new file mode 100644 index 0000000000000..d3d65d24743a4 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadIssueReporter.ts @@ -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()); + + 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); + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 4bc7878a34051..8e66edb74ca68 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -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; @@ -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>(ExtHostContext); @@ -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) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 6c4f356d27075..db6380db3a8ed 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2295,6 +2295,15 @@ export interface MainThreadLocalizationShape extends IDisposable { $fetchBundleContents(uriComponents: UriComponents): Promise; } +export interface ExtHostIssueReporterShape { + $getIssueReporterUri(extensionId: string, token: CancellationToken): Promise; +} + +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; @@ -2482,7 +2491,8 @@ export const MainContext = { MainThreadTimeline: createProxyIdentifier('MainThreadTimeline'), MainThreadTesting: createProxyIdentifier('MainThreadTesting'), MainThreadLocalization: createProxyIdentifier('MainThreadLocalizationShape'), - MainThreadSemanticSimilarity: createProxyIdentifier('MainThreadSemanticSimilarity') + MainThreadSemanticSimilarity: createProxyIdentifier('MainThreadSemanticSimilarity'), + MainThreadIssueReporter: createProxyIdentifier('MainThreadIssueReporter'), }; export const ExtHostContext = { @@ -2544,4 +2554,5 @@ export const ExtHostContext = { ExtHostTesting: createProxyIdentifier('ExtHostTesting'), ExtHostTelemetry: createProxyIdentifier('ExtHostTelemetry'), ExtHostLocalization: createProxyIdentifier('ExtHostLocalization'), + ExtHostIssueReporter: createProxyIdentifier('ExtHostIssueReporter'), }; diff --git a/src/vs/workbench/api/common/extHostIssueReporter.ts b/src/vs/workbench/api/common/extHostIssueReporter.ts new file mode 100644 index 0000000000000..ef16213423067 --- /dev/null +++ b/src/vs/workbench/api/common/extHostIssueReporter.ts @@ -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 = new Map(); + + private readonly _proxy: MainThreadIssueReporterShape; + + constructor( + mainContext: IMainContext + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadIssueReporter); + } + + async $getIssueReporterUri(extensionId: string, token: CancellationToken): Promise { + 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); + }); + } +} diff --git a/src/vs/workbench/contrib/issue/browser/issueService.ts b/src/vs/workbench/contrib/issue/browser/issueService.ts index dc7430b544d09..1263f9953dd1c 100644 --- a/src/vs/workbench/contrib/issue/browser/issueService.ts +++ b/src/vs/workbench/contrib/issue/browser/issueService.ts @@ -8,16 +8,23 @@ import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; import { IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { IIssueUriRequestHandler, IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; import { IssueReporterData } from 'vs/platform/issue/common/issue'; import { userAgent } from 'vs/base/common/platform'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; +import { ILogService } from 'vs/platform/log/common/log'; export class WebIssueService implements IWorkbenchIssueService { declare readonly _serviceBrand: undefined; + private readonly _handlers = new Map(); + constructor( @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @ILogService private readonly logService: ILogService ) { } //TODO @TylerLeonhardt @Tyriar to implement a process explorer for the web @@ -29,11 +36,23 @@ export class WebIssueService implements IWorkbenchIssueService { let repositoryUrl = this.productService.reportIssueUrl; let selectedExtension: ILocalExtension | undefined; if (options.extensionId) { - const extensions = await this.extensionManagementService.getInstalled(ExtensionType.User); - selectedExtension = extensions.filter(ext => ext.identifier.id === options.extensionId)[0]; - const extensionGitHubUrl = this.getExtensionGitHubUrl(selectedExtension); - if (extensionGitHubUrl) { - repositoryUrl = `${extensionGitHubUrl}/issues/new`; + if (this._handlers.has(options.extensionId)) { + try { + const uri = await this.getIssueReporterUri(options.extensionId, CancellationToken.None); + repositoryUrl = uri.toString(true); + } catch (e) { + this.logService.error(e); + } + } + + // if we don't have a handler, or the handler failed, try to get the extension's github url + if (!repositoryUrl) { + const extensions = await this.extensionManagementService.getInstalled(ExtensionType.User); + selectedExtension = extensions.filter(ext => ext.identifier.id === options.extensionId)[0]; + const extensionGitHubUrl = this.getExtensionGitHubUrl(selectedExtension); + if (extensionGitHubUrl) { + repositoryUrl = `${extensionGitHubUrl}/issues/new`; + } } } @@ -45,6 +64,19 @@ export class WebIssueService implements IWorkbenchIssueService { } } + registerIssueUriRequestHandler(extensionId: string, handler: IIssueUriRequestHandler): IDisposable { + this._handlers.set(extensionId, handler); + return toDisposable(() => this._handlers.delete(extensionId)); + } + + private async getIssueReporterUri(extensionId: string, token: CancellationToken): Promise { + const handler = this._handlers.get(extensionId); + if (!handler) { + throw new Error(`No handler registered for extension ${extensionId}`); + } + return handler.provideIssueUrl(token); + } + private getExtensionGitHubUrl(extension: ILocalExtension): string { let repositoryUrl = ''; diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index fe819b5b6c47e..8085ebef643e9 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -41,6 +41,7 @@ export const allApiProposals = Object.freeze({ formatMultipleRanges: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.formatMultipleRanges.d.ts', fsChunks: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fsChunks.d.ts', getSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.getSessions.d.ts', + handleIssueUri: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.handleIssueUri.d.ts', idToken: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.idToken.d.ts', indentSize: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.indentSize.d.ts', inlineCompletionsAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts', diff --git a/src/vs/workbench/services/issue/common/issue.ts b/src/vs/workbench/services/issue/common/issue.ts index f5cb676c561a2..67c74c97fa3c5 100644 --- a/src/vs/workbench/services/issue/common/issue.ts +++ b/src/vs/workbench/services/issue/common/issue.ts @@ -3,13 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IssueReporterData } from 'vs/platform/issue/common/issue'; +export interface IIssueUriRequestHandler { + provideIssueUrl(token: CancellationToken): Promise; +} + export const IWorkbenchIssueService = createDecorator('workbenchIssueService'); export interface IWorkbenchIssueService { readonly _serviceBrand: undefined; openReporter(dataOverrides?: Partial): Promise; openProcessExplorer(): Promise; + registerIssueUriRequestHandler(extensionId: string, handler: IIssueUriRequestHandler): IDisposable; } diff --git a/src/vs/workbench/services/issue/electron-sandbox/issueService.ts b/src/vs/workbench/services/issue/electron-sandbox/issueService.ts index e7659413d9332..5f5350507a43a 100644 --- a/src/vs/workbench/services/issue/electron-sandbox/issueService.ts +++ b/src/vs/workbench/services/issue/electron-sandbox/issueService.ts @@ -11,7 +11,7 @@ import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getZoomLevel } from 'vs/base/browser/browser'; -import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { IIssueUriRequestHandler, IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { platform } from 'vs/base/common/process'; @@ -21,11 +21,16 @@ import { IAuthenticationService } from 'vs/workbench/services/authentication/com import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IIntegrityService } from 'vs/workbench/services/integrity/common/integrity'; -import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { ipcRenderer, process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; export class NativeIssueService implements IWorkbenchIssueService { declare readonly _serviceBrand: undefined; + private readonly _handlers = new Map(); + constructor( @IIssueService private readonly issueService: IIssueService, @IThemeService private readonly themeService: IThemeService, @@ -36,8 +41,13 @@ export class NativeIssueService implements IWorkbenchIssueService { @IProductService private readonly productService: IProductService, @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, - @IIntegrityService private readonly integrityService: IIntegrityService - ) { } + @IIntegrityService private readonly integrityService: IIntegrityService, + ) { + ipcRenderer.on('vscode:triggerIssueUriRequestHandler', async (event: unknown, request: { replyChannel: string; extensionId: string }) => { + const result = await this.getIssueReporterUri(request.extensionId, CancellationToken.None); + ipcRenderer.send(request.replyChannel, result.toString()); + }); + } async openReporter(dataOverrides: Partial = {}): Promise { const extensionData: IssueReporterExtensionData[] = []; @@ -55,6 +65,7 @@ export class NativeIssueService implements IWorkbenchIssueService { version: manifest.version, repositoryUrl: manifest.repository && manifest.repository.url, bugsUrl: manifest.bugs && manifest.bugs.url, + hasIssueUriRequestHandler: this._handlers.has(extension.identifier.id), displayName: manifest.displayName, id: extension.identifier.id, isTheme, @@ -133,6 +144,21 @@ export class NativeIssueService implements IWorkbenchIssueService { }; return this.issueService.openProcessExplorer(data); } + + registerIssueUriRequestHandler(extensionId: string, handler: IIssueUriRequestHandler): IDisposable { + this._handlers.set(extensionId, handler); + return { + dispose: () => this._handlers.delete(extensionId) + }; + } + + private async getIssueReporterUri(extensionId: string, token: CancellationToken): Promise { + const handler = this._handlers.get(extensionId); + if (!handler) { + throw new Error(`No issue uri request handler registered for extension '${extensionId}'`); + } + return handler.provideIssueUrl(token); + } } export function getIssueReporterStyles(theme: IColorTheme): IssueReporterStyles { diff --git a/src/vscode-dts/vscode.proposed.handleIssueUri.d.ts b/src/vscode-dts/vscode.proposed.handleIssueUri.d.ts new file mode 100644 index 0000000000000..60c8ad08609db --- /dev/null +++ b/src/vscode-dts/vscode.proposed.handleIssueUri.d.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/46726 + + export interface IssueUriRequestHandler { + handleIssueUrlRequest(): ProviderResult; + } + + export namespace env { + export function registerIssueUriRequestHandler(handler: IssueUriRequestHandler): Disposable; + } +}