diff --git a/src/sql/workbench/contrib/modelView/browser/webview.component.ts b/src/sql/workbench/contrib/modelView/browser/webview.component.ts index e7a25f9a50..87eefa61aa 100644 --- a/src/sql/workbench/contrib/modelView/browser/webview.component.ts +++ b/src/sql/workbench/contrib/modelView/browser/webview.component.ts @@ -128,8 +128,10 @@ export default class WebViewComponent extends ComponentBase i if (!link) { return; } - if (WebViewComponent.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0 || this.enableCommandUris && link.scheme === 'command') { + if (WebViewComponent.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0) { this._openerService.open(link); + } else if (this.enableCommandUris && link.scheme === 'command') { + this._openerService.open(link, { allowCommands: true }); } } diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/linkHandler.directive.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/linkHandler.directive.ts index a4622a1959..3aff7fc0d0 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/linkHandler.directive.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/linkHandler.directive.ts @@ -80,7 +80,7 @@ export class LinkHandlerDirective { this.openerService.open(uri, { openExternal: true }).catch(onUnexpectedError); } else { - this.openerService.open(uri).catch(onUnexpectedError); + this.openerService.open(uri, { allowCommands: true }).catch(onUnexpectedError); } } } diff --git a/src/vs/editor/browser/core/markdownRenderer.ts b/src/vs/editor/browser/core/markdownRenderer.ts index 6b61fffaad..cc754843ae 100644 --- a/src/vs/editor/browser/core/markdownRenderer.ts +++ b/src/vs/editor/browser/core/markdownRenderer.ts @@ -58,7 +58,7 @@ export class MarkdownRenderer { if (!markdown) { element = document.createElement('span'); } else { - element = renderMarkdown(markdown, { ...this._getRenderOptions(disposeables), ...options }, markedOptions); + element = renderMarkdown(markdown, { ...this._getRenderOptions(markdown, disposeables), ...options }, markedOptions); } return { @@ -67,7 +67,7 @@ export class MarkdownRenderer { }; } - protected _getRenderOptions(disposeables: DisposableStore): MarkdownRenderOptions { + protected _getRenderOptions(markdown: IMarkdownString, disposeables: DisposableStore): MarkdownRenderOptions { return { baseUrl: this._options.baseUrl, codeBlockRenderer: async (languageAlias, value) => { @@ -105,7 +105,7 @@ export class MarkdownRenderer { }, asyncRenderCallback: () => this._onDidRenderAsync.fire(), actionHandler: { - callback: (content) => this._openerService.open(content, { fromUserGesture: true }).catch(onUnexpectedError), + callback: (content) => this._openerService.open(content, { fromUserGesture: true, allowContributedOpeners: true, allowCommands: markdown.isTrusted }).catch(onUnexpectedError), disposeables } }; diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 3b3106b8c3..745b162c83 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -21,10 +21,15 @@ class CommandOpener implements IOpener { constructor(@ICommandService private readonly _commandService: ICommandService) { } - async open(target: URI | string) { + async open(target: URI | string, options?: OpenOptions): Promise { if (!matchesScheme(target, Schemas.command)) { return false; } + if (!options?.allowCommands) { + // silently ignore commands when command-links are disabled, also + // surpress other openers by returning TRUE + return true; + } // run command or bail out if command isn't known if (typeof target === 'string') { target = URI.parse(target); diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index d61bee07bc..5ea26960ef 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -143,7 +143,7 @@ class MessageWidget { this._codeLink.setAttribute('href', `${code.target.toString()}`); this._codeLink.onclick = (e) => { - this._openerService.open(code.target); + this._openerService.open(code.target, { allowCommands: true }); e.preventDefault(); e.stopPropagation(); }; diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index edcd211cf5..c7ed60658f 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -536,7 +536,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._codeLink.setAttribute('href', code.target.toString()); this._codeLink.onclick = (e) => { - this._openerService.open(code.target); + this._openerService.open(code.target, { allowCommands: true }); e.preventDefault(); e.stopPropagation(); }; diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index fe49e8ac7a..085591a1f7 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -327,7 +327,7 @@ export class LinkDetector implements IEditorContribution { } } - return this.openerService.open(uri, { openToSide, fromUserGesture }); + return this.openerService.open(uri, { openToSide, fromUserGesture, allowContributedOpeners: true, allowCommands: true }); }, err => { const messageOrError = diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index 9c3356700e..9e37cbb064 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -81,20 +81,10 @@ suite('OpenerService', function () { const id = `aCommand${Math.random()}`; CommandsRegistry.registerCommand(id, function () { }); + assert.strictEqual(lastCommand, undefined); await openerService.open(URI.parse('command:' + id)); - assert.equal(lastCommand!.id, id); - assert.equal(lastCommand!.args.length, 0); - await openerService.open(URI.parse('command:' + id).with({ query: '123' })); - assert.equal(lastCommand!.id, id); - assert.equal(lastCommand!.args.length, 1); - assert.equal(lastCommand!.args[0], '123'); - - await openerService.open(URI.parse('command:' + id).with({ query: JSON.stringify([12, true]) })); - assert.equal(lastCommand!.id, id); - assert.equal(lastCommand!.args.length, 2); - assert.equal(lastCommand!.args[0], 12); - assert.equal(lastCommand!.args[1], true); + assert.strictEqual(lastCommand, undefined); }); test('links are protected by validators', async function () { @@ -108,6 +98,33 @@ suite('OpenerService', function () { assert.equal(httpsResult, false); }); + test('delegate to commandsService, command:someid', async function () { + const openerService = new OpenerService(editorService, commandService); + + const id = `aCommand${Math.random()}`; + CommandsRegistry.registerCommand(id, function () { }); + + await openerService.open(URI.parse('command:' + id).with({ query: '\"123\"' }), { allowCommands: true }); + assert.strictEqual(lastCommand!.id, id); + assert.strictEqual(lastCommand!.args.length, 1); + assert.strictEqual(lastCommand!.args[0], '123'); + + await openerService.open(URI.parse('command:' + id), { allowCommands: true }); + assert.strictEqual(lastCommand!.id, id); + assert.strictEqual(lastCommand!.args.length, 0); + + await openerService.open(URI.parse('command:' + id).with({ query: '123' }), { allowCommands: true }); + assert.strictEqual(lastCommand!.id, id); + assert.strictEqual(lastCommand!.args.length, 1); + assert.strictEqual(lastCommand!.args[0], 123); + + await openerService.open(URI.parse('command:' + id).with({ query: JSON.stringify([12, true]) }), { allowCommands: true }); + assert.strictEqual(lastCommand!.id, id); + assert.strictEqual(lastCommand!.args.length, 2); + assert.strictEqual(lastCommand!.args[0], 12); + assert.strictEqual(lastCommand!.args[1], true); + }); + test('links validated by validators go to openers', async function () { const openerService = new OpenerService(editorService, commandService); diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index 42f08981fb..a3a144c1ed 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -50,7 +50,7 @@ export class Link extends Disposable { this._register(onOpen(e => { EventHelper.stop(e, true); - openerService.open(link.href); + openerService.open(link.href, { allowCommands: true }); })); this.applyStyles(); diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 1ee866f09b..9e78b8f777 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -29,9 +29,18 @@ type OpenInternalOptions = { * action, such as keyboard or mouse usage. */ readonly fromUserGesture?: boolean; + + /** + * Allow command links to be handled. + */ + readonly allowCommands?: boolean; }; -type OpenExternalOptions = { readonly openExternal?: boolean; readonly allowTunneling?: boolean }; +export type OpenExternalOptions = { + readonly openExternal?: boolean; + readonly allowTunneling?: boolean; + readonly allowContributedOpeners?: boolean | string; +}; export type OpenOptions = OpenInternalOptions & OpenExternalOptions; diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts index 984d730c3d..5241294dbc 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -78,7 +78,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma private onDidClickLink(handle: extHostProtocol.WebviewHandle, link: string): void { const webview = this.getWebview(handle); if (this.isSupportedLink(webview, URI.parse(link))) { - this._openerService.open(link, { fromUserGesture: true }); + this._openerService.open(link, { fromUserGesture: true, allowContributedOpeners: true, allowCommands: true }); } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 118456e905..c05b529a36 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -371,7 +371,7 @@ export class NotificationTemplateRenderer extends Disposable { private renderMessage(notification: INotificationViewItem): boolean { clearNode(this.template.message); this.template.message.appendChild(NotificationMessageRenderer.render(notification.message, { - callback: link => this.openerService.open(URI.parse(link)), + callback: link => this.openerService.open(URI.parse(link), { allowCommands: true }), toDispose: this.inputDisposables })); diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index b3db036b82..521ef2e204 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -581,7 +581,7 @@ export abstract class ViewPane extends Pane implements IView { button.label = node.label; button.onDidClick(_ => { this.telemetryService.publicLog2<{ viewId: string, uri: string }, WelcomeActionClassification>('views.welcomeAction', { viewId: this.id, uri: node.href }); - this.openerService.open(node.href); + this.openerService.open(node.href, { allowCommands: true }); }, null, disposables); disposables.add(button); disposables.add(attachButtonStyler(button, this.themeService)); diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 692a63200e..0defcbaf1b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -127,7 +127,7 @@ export class CommentNodeRenderer implements IListRenderer inline: true, actionHandler: { callback: (content) => { - this.openerService.open(content).catch(onUnexpectedError); + this.openerService.open(content, { allowCommands: node.element.comment.body.isTrusted }).catch(onUnexpectedError); }, disposeables: disposables } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 10eb356ea4..5b4f6def46 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -625,11 +625,12 @@ export class ExtensionEditor extends EditorPane { return; } // Only allow links with specific schemes - if (matchesScheme(link, Schemas.http) || matchesScheme(link, Schemas.https) || matchesScheme(link, Schemas.mailto) - || (matchesScheme(link, Schemas.command) && URI.parse(link).path === ShowCurrentReleaseNotesActionId) - ) { + if (matchesScheme(link, Schemas.http) || matchesScheme(link, Schemas.https) || matchesScheme(link, Schemas.mailto)) { this.openerService.open(link); } + if (matchesScheme(link, Schemas.command) && URI.parse(link).path === ShowCurrentReleaseNotesActionId) { + this.openerService.open(link, { allowCommands: true }); // TODO@sandy081 use commands service + } }, null, this.contentDisposables)); return webview; diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 83aa7653b7..7211a97bbf 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -430,7 +430,7 @@ class MarkerWidget extends Disposable { this._register(onOpen(e => { dom.EventHelper.stop(e, true); - this._openerService.open(codeUri); + this._openerService.open(codeUri, { allowCommands: true }); })); const code = new HighlightedLabel(dom.append(this._codeLink, dom.$('.marker-code')), false); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 765ffa2e7b..84b6b232c0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -424,7 +424,7 @@ var requirejs = (function() { if (matchesScheme(link, Schemas.http) || matchesScheme(link, Schemas.https) || matchesScheme(link, Schemas.mailto) || matchesScheme(link, Schemas.command)) { - this.openerService.open(link, { fromUserGesture: true }); + this.openerService.open(link, { fromUserGesture: true, allowContributedOpeners: true, allowCommands: true }); } })); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index fbbe799575..296a446ffa 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -755,7 +755,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre }; this._onDidClickSettingLink.fire(e); } else { - this._openerService.open(content).catch(onUnexpectedError); + this._openerService.open(content, { allowCommands: true }).catch(onUnexpectedError); } }, disposeables diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index d43caed761..f31a5b6831 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -365,7 +365,7 @@ class HelpItem extends HelpItemBase { } protected async takeAction(extensionDescription: IExtensionDescription, url: string): Promise { - await this.openerService.open(URI.parse(url)); + await this.openerService.open(URI.parse(url), { allowCommands: true }); } } diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts index c6937b69e2..64dbfc0cc1 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts @@ -189,7 +189,7 @@ export class WalkThroughPart extends EditorPane { this.notificationService.info(localize('walkThrough.gitNotFound', "It looks like Git is not installed on your system.")); return; } - this.openerService.open(this.addFrom(uri)); + this.openerService.open(this.addFrom(uri), { allowCommands: true }); } private addFrom(uri: URI) {