diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 57144d67ea6..971addf8f00 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -50,19 +50,19 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende const _href = function (href: string, isDomUri: boolean): string { const data = markdown.uris && markdown.uris[href]; if (!data) { - return href; + return href; // no uri exists } let uri = URI.revive(data); + if (URI.parse(href).toString() === uri.toString()) { + return href; // no tranformation performed + } if (isDomUri) { uri = DOM.asDomUri(uri); } if (uri.query) { uri = uri.with({ query: _uriMassage(uri.query) }); } - if (data) { - href = uri.toString(true); - } - return href; + return uri.toString(); }; // signal to code-block render that the diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 48175adfb42..ead9bd31bfb 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -4,58 +4,132 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; import { parse } from 'vs/base/common/marshalling'; import { Schemas } from 'vs/base/common/network'; -import * as resources from 'vs/base/common/resources'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { normalizePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { IOpener, IOpenerService, IValidator, IExternalUriResolver, OpenOptions, ResolveExternalUriOptions, IResolvedExternalUri, IExternalOpener } from 'vs/platform/opener/common/opener'; +import { IOpener, IOpenerService, IValidator, IExternalUriResolver, OpenOptions, ResolveExternalUriOptions, IResolvedExternalUri, IExternalOpener, matchesScheme } from 'vs/platform/opener/common/opener'; import { EditorOpenContext } from 'vs/platform/editor/common/editor'; -export class OpenerService extends Disposable implements IOpenerService { + +class CommandOpener implements IOpener { + + constructor(@ICommandService private readonly _commandService: ICommandService) { } + + async open(target: URI | string) { + if (!matchesScheme(target, Schemas.command)) { + return false; + } + // run command or bail out if command isn't known + if (typeof target === 'string') { + target = URI.parse(target); + } + if (!CommandsRegistry.getCommand(target.path)) { + throw new Error(`command '${target.path}' NOT known`); + } + // execute as command + let args: any = []; + try { + args = parse(target.query); + if (!Array.isArray(args)) { + args = [args]; + } + } catch (e) { + // ignore error + } + await this._commandService.executeCommand(target.path, ...args); + return true; + } +} + +class EditorOpener implements IOpener { + + constructor(@ICodeEditorService private readonly _editorService: ICodeEditorService) { } + + async open(target: URI | string, options: OpenOptions) { + if (typeof target === 'string') { + target = URI.parse(target); + } + let selection: { startLineNumber: number; startColumn: number; } | undefined = undefined; + const match = /^L?(\d+)(?:,(\d+))?/.exec(target.fragment); + if (match) { + // support file:///some/file.js#73,84 + // support file:///some/file.js#L73 + selection = { + startLineNumber: parseInt(match[1]), + startColumn: match[2] ? parseInt(match[2]) : 1 + }; + // remove fragment + target = target.with({ fragment: '' }); + } + + if (target.scheme === Schemas.file) { + target = normalizePath(target); // workaround for non-normalized paths (https://github.com/Microsoft/vscode/issues/12954) + } + + await this._editorService.openCodeEditor( + { resource: target, options: { selection, context: options?.fromUserGesture ? EditorOpenContext.USER : EditorOpenContext.API } }, + this._editorService.getFocusedCodeEditor(), + options?.openToSide + ); + + return true; + } +} + +export class OpenerService implements IOpenerService { _serviceBrand: undefined; private readonly _openers = new LinkedList(); private readonly _validators = new LinkedList(); private readonly _resolvers = new LinkedList(); + private _externalOpener: IExternalOpener; constructor( - @ICodeEditorService private readonly _editorService: ICodeEditorService, - @ICommandService private readonly _commandService: ICommandService, + @ICodeEditorService editorService: ICodeEditorService, + @ICommandService commandService: ICommandService, ) { - super(); - // Default external opener is going through window.open() this._externalOpener = { openExternal: href => { dom.windowOpenNoOpener(href); - return Promise.resolve(true); } }; + + // Default opener: maito, http(s), command, and catch-all-editors + this._openers.push({ + open: async (target: URI | string, options?: OpenOptions) => { + if (options?.openExternal || matchesScheme(target, Schemas.mailto) || matchesScheme(target, Schemas.http) || matchesScheme(target, Schemas.https)) { + // open externally + await this._doOpenExternal(target, options); + return true; + } + return false; + } + }); + this._openers.push(new CommandOpener(commandService)); + this._openers.push(new EditorOpener(editorService)); } registerOpener(opener: IOpener): IDisposable { - const remove = this._openers.push(opener); - + const remove = this._openers.unshift(opener); return { dispose: remove }; } registerValidator(validator: IValidator): IDisposable { const remove = this._validators.push(validator); - return { dispose: remove }; } registerExternalUriResolver(resolver: IExternalUriResolver): IDisposable { const remove = this._resolvers.push(resolver); - return { dispose: remove }; } @@ -63,30 +137,24 @@ export class OpenerService extends Disposable implements IOpenerService { this._externalOpener = externalOpener; } - async open(resource: URI, options?: OpenOptions): Promise { - - // no scheme ?!? - if (!resource.scheme) { - return Promise.resolve(false); - } + async open(target: URI | string, options?: OpenOptions): Promise { // check with contributed validators for (const validator of this._validators.toArray()) { - if (!(await validator.shouldOpen(resource))) { + if (!(await validator.shouldOpen(target))) { return false; } } // check with contributed openers for (const opener of this._openers.toArray()) { - const handled = await opener.open(resource, options); + const handled = await opener.open(target, options); if (handled) { return true; } } - // use default openers - return this._doOpen(resource, options); + return false; } async resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise { @@ -100,68 +168,19 @@ export class OpenerService extends Disposable implements IOpenerService { return { resolved: resource, dispose: () => { } }; } - private async _doOpen(resource: URI, options: OpenOptions | undefined): Promise { - const { scheme, path, query, fragment } = resource; + private async _doOpenExternal(resource: URI | string, options: OpenOptions | undefined): Promise { - if (options?.openExternal || equalsIgnoreCase(scheme, Schemas.mailto) || equalsIgnoreCase(scheme, Schemas.http) || equalsIgnoreCase(scheme, Schemas.https)) { - // open externally - return this._doOpenExternal(resource, options); + //todo@joh IExternalUriResolver should support `uri: URI | string` + const uri = typeof resource === 'string' ? URI.parse(resource) : resource; + const { resolved } = await this.resolveExternalUri(uri, options); + + if (typeof resource === 'string' && uri.toString() === resolved.toString()) { + // open the url-string AS IS + return this._externalOpener.openExternal(resource); + } else { + // open URI using the toString(noEncode)+encodeURI-trick + return this._externalOpener.openExternal(encodeURI(resolved.toString(true))); } - - if (equalsIgnoreCase(scheme, Schemas.command)) { - // run command or bail out if command isn't known - if (!CommandsRegistry.getCommand(path)) { - throw new Error(`command '${path}' NOT known`); - } - // execute as command - let args: any = []; - try { - args = parse(query); - if (!Array.isArray(args)) { - args = [args]; - } - } catch (e) { - // ignore error - } - - await this._commandService.executeCommand(path, ...args); - - return true; - } - - // finally open in editor - let selection: { startLineNumber: number; startColumn: number; } | undefined = undefined; - const match = /^L?(\d+)(?:,(\d+))?/.exec(fragment); - if (match) { - // support file:///some/file.js#73,84 - // support file:///some/file.js#L73 - selection = { - startLineNumber: parseInt(match[1]), - startColumn: match[2] ? parseInt(match[2]) : 1 - }; - // remove fragment - resource = resource.with({ fragment: '' }); - } - - if (resource.scheme === Schemas.file) { - resource = resources.normalizePath(resource); // workaround for non-normalized paths (https://github.com/Microsoft/vscode/issues/12954) - } - - await this._editorService.openCodeEditor( - { resource, options: { selection, context: options?.fromUserGesture ? EditorOpenContext.USER : EditorOpenContext.API } }, - this._editorService.getFocusedCodeEditor(), - options?.openToSide - ); - - return true; - } - - private async _doOpenExternal(resource: URI, options: OpenOptions | undefined): Promise { - const { resolved } = await this.resolveExternalUri(resource, options); - - // TODO@Jo neither encodeURI nor toString(true) should be needed - // once we go with URL and not URI - return this._externalOpener.openExternal(encodeURI(resolved.toString(true))); } dispose() { diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index f37254d776f..ca710263b24 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -207,7 +207,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { private readonly _themeService: IThemeService, private readonly _keybindingService: IKeybindingService, private readonly _modeService: IModeService, - private readonly _openerService: IOpenerService | null = NullOpenerService, + private readonly _openerService: IOpenerService = NullOpenerService, ) { super(ModesContentHoverWidget.ID, editor); diff --git a/src/vs/editor/contrib/hover/modesGlyphHover.ts b/src/vs/editor/contrib/hover/modesGlyphHover.ts index 85dece77b64..32c6cf14656 100644 --- a/src/vs/editor/contrib/hover/modesGlyphHover.ts +++ b/src/vs/editor/contrib/hover/modesGlyphHover.ts @@ -97,7 +97,7 @@ export class ModesGlyphHoverWidget extends GlyphHoverWidget { constructor( editor: ICodeEditor, modeService: IModeService, - openerService: IOpenerService | null = NullOpenerService, + openerService: IOpenerService = NullOpenerService, ) { super(ModesGlyphHoverWidget.ID, editor); diff --git a/src/vs/editor/contrib/links/getLinks.ts b/src/vs/editor/contrib/links/getLinks.ts index df34eed5600..a85e7f8c942 100644 --- a/src/vs/editor/contrib/links/getLinks.ts +++ b/src/vs/editor/contrib/links/getLinks.ts @@ -44,17 +44,9 @@ export class Link implements ILink { return this._link.tooltip; } - resolve(token: CancellationToken): Promise { + async resolve(token: CancellationToken): Promise { if (this._link.url) { - try { - if (typeof this._link.url === 'string') { - return Promise.resolve(URI.parse(this._link.url)); - } else { - return Promise.resolve(this._link.url); - } - } catch (e) { - return Promise.reject(new Error('invalid')); - } + return this._link.url; } if (typeof this._provider.resolveLink === 'function') { diff --git a/src/vs/editor/contrib/markdown/markdownRenderer.ts b/src/vs/editor/contrib/markdown/markdownRenderer.ts index eb17228cfa8..b4bfe8ddd9e 100644 --- a/src/vs/editor/contrib/markdown/markdownRenderer.ts +++ b/src/vs/editor/contrib/markdown/markdownRenderer.ts @@ -7,7 +7,6 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { renderMarkdown, MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { URI } from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -29,7 +28,7 @@ export class MarkdownRenderer extends Disposable { constructor( private readonly _editor: ICodeEditor, @IModeService private readonly _modeService: IModeService, - @optional(IOpenerService) private readonly _openerService: IOpenerService | null = NullOpenerService, + @optional(IOpenerService) private readonly _openerService: IOpenerService = NullOpenerService, ) { super(); } @@ -64,15 +63,7 @@ export class MarkdownRenderer extends Disposable { codeBlockRenderCallback: () => this._onDidRenderCodeBlock.fire(), actionHandler: { callback: (content) => { - let uri: URI | undefined; - try { - uri = URI.parse(content); - } catch { - // ignore - } - if (uri && this._openerService) { - this._openerService.open(uri, { fromUserGesture: true }).catch(onUnexpectedError); - } + this._openerService.open(content, { fromUserGesture: true }).catch(onUnexpectedError); }, disposeables } diff --git a/src/vs/editor/test/browser/services/openerService.test.ts b/src/vs/editor/test/browser/services/openerService.test.ts index 6f03b911e37..697dc99402d 100644 --- a/src/vs/editor/test/browser/services/openerService.test.ts +++ b/src/vs/editor/test/browser/services/openerService.test.ts @@ -8,6 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { OpenerService } from 'vs/editor/browser/services/openerService'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; import { CommandsRegistry, ICommandService, NullCommandService } from 'vs/platform/commands/common/commands'; +import { matchesScheme } from 'vs/platform/opener/common/opener'; suite('OpenerService', function () { const editorService = new TestCodeEditorService(); @@ -28,27 +29,27 @@ suite('OpenerService', function () { lastCommand = undefined; }); - test('delegate to editorService, scheme:///fff', function () { + test('delegate to editorService, scheme:///fff', async function () { const openerService = new OpenerService(editorService, NullCommandService); - openerService.open(URI.parse('another:///somepath')); + await openerService.open(URI.parse('another:///somepath')); assert.equal(editorService.lastInput!.options!.selection, undefined); }); - test('delegate to editorService, scheme:///fff#L123', function () { + test('delegate to editorService, scheme:///fff#L123', async function () { const openerService = new OpenerService(editorService, NullCommandService); - openerService.open(URI.parse('file:///somepath#L23')); + await openerService.open(URI.parse('file:///somepath#L23')); assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23); assert.equal(editorService.lastInput!.options!.selection!.startColumn, 1); assert.equal(editorService.lastInput!.options!.selection!.endLineNumber, undefined); assert.equal(editorService.lastInput!.options!.selection!.endColumn, undefined); assert.equal(editorService.lastInput!.resource.fragment, ''); - openerService.open(URI.parse('another:///somepath#L23')); + await openerService.open(URI.parse('another:///somepath#L23')); assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23); assert.equal(editorService.lastInput!.options!.selection!.startColumn, 1); - openerService.open(URI.parse('another:///somepath#L23,45')); + await openerService.open(URI.parse('another:///somepath#L23,45')); assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23); assert.equal(editorService.lastInput!.options!.selection!.startColumn, 45); assert.equal(editorService.lastInput!.options!.selection!.endLineNumber, undefined); @@ -56,17 +57,17 @@ suite('OpenerService', function () { assert.equal(editorService.lastInput!.resource.fragment, ''); }); - test('delegate to editorService, scheme:///fff#123,123', function () { + test('delegate to editorService, scheme:///fff#123,123', async function () { const openerService = new OpenerService(editorService, NullCommandService); - openerService.open(URI.parse('file:///somepath#23')); + await openerService.open(URI.parse('file:///somepath#23')); assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23); assert.equal(editorService.lastInput!.options!.selection!.startColumn, 1); assert.equal(editorService.lastInput!.options!.selection!.endLineNumber, undefined); assert.equal(editorService.lastInput!.options!.selection!.endColumn, undefined); assert.equal(editorService.lastInput!.resource.fragment, ''); - openerService.open(URI.parse('file:///somepath#23,45')); + await openerService.open(URI.parse('file:///somepath#23,45')); assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23); assert.equal(editorService.lastInput!.options!.selection!.startColumn, 45); assert.equal(editorService.lastInput!.options!.selection!.endLineNumber, undefined); @@ -74,22 +75,22 @@ suite('OpenerService', function () { assert.equal(editorService.lastInput!.resource.fragment, ''); }); - test('delegate to commandsService, command:someid', function () { + test('delegate to commandsService, command:someid', async function () { const openerService = new OpenerService(editorService, commandService); const id = `aCommand${Math.random()}`; CommandsRegistry.registerCommand(id, function () { }); - openerService.open(URI.parse('command:' + id)); + await openerService.open(URI.parse('command:' + id)); assert.equal(lastCommand!.id, id); assert.equal(lastCommand!.args.length, 0); - openerService.open(URI.parse('command:' + id).with({ query: '123' })); + 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'); - openerService.open(URI.parse('command:' + id).with({ query: JSON.stringify([12, true]) })); + 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); @@ -199,4 +200,18 @@ suite('OpenerService', function () { assert.equal(v1, 2); assert.equal(v2, 0); }); + + test('matchesScheme', function () { + assert.ok(matchesScheme('https://microsoft.com', 'https')); + assert.ok(matchesScheme('http://microsoft.com', 'http')); + assert.ok(matchesScheme('hTTPs://microsoft.com', 'https')); + assert.ok(matchesScheme('httP://microsoft.com', 'http')); + assert.ok(matchesScheme(URI.parse('https://microsoft.com'), 'https')); + assert.ok(matchesScheme(URI.parse('http://microsoft.com'), 'http')); + assert.ok(matchesScheme(URI.parse('hTTPs://microsoft.com'), 'https')); + assert.ok(matchesScheme(URI.parse('httP://microsoft.com'), 'http')); + assert.ok(!matchesScheme(URI.parse('https://microsoft.com'), 'http')); + assert.ok(!matchesScheme(URI.parse('htt://microsoft.com'), 'http')); + assert.ok(!matchesScheme(URI.parse('z://microsoft.com'), 'http')); + }); }); diff --git a/src/vs/platform/opener/common/opener.ts b/src/vs/platform/opener/common/opener.ts index 3b60521677a..8f7921d1581 100644 --- a/src/vs/platform/opener/common/opener.ts +++ b/src/vs/platform/opener/common/opener.ts @@ -6,6 +6,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { equalsIgnoreCase, startsWithIgnoreCase } from 'vs/base/common/strings'; export const IOpenerService = createDecorator('openerService'); @@ -35,8 +36,7 @@ export interface IResolvedExternalUri extends IDisposable { } export interface IOpener { - open(resource: URI, options?: OpenInternalOptions): Promise; - open(resource: URI, options?: OpenExternalOptions): Promise; + open(resource: URI | string, options?: OpenInternalOptions | OpenExternalOptions): Promise; } export interface IExternalOpener { @@ -44,7 +44,7 @@ export interface IExternalOpener { } export interface IValidator { - shouldOpen(resource: URI): Promise; + shouldOpen(resource: URI | string): Promise; } export interface IExternalUriResolver { @@ -83,8 +83,7 @@ export interface IOpenerService { * @param resource A resource * @return A promise that resolves when the opening is done. */ - open(resource: URI, options?: OpenInternalOptions): Promise; - open(resource: URI, options?: OpenExternalOptions): Promise; + open(resource: URI | string, options?: OpenInternalOptions | OpenExternalOptions): Promise; /** * Resolve a resource to its external form. @@ -101,3 +100,11 @@ export const NullOpenerService: IOpenerService = Object.freeze({ async open() { return false; }, async resolveExternalUri(uri: URI) { return { resolved: uri, dispose() { } }; }, }); + +export function matchesScheme(target: URI | string, scheme: string) { + if (URI.isUri(target)) { + return equalsIgnoreCase(target.scheme, scheme); + } else { + return startsWithIgnoreCase(target, scheme + ':'); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index bbce19a3388..2a431111e93 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -42,9 +42,17 @@ export class MainThreadWindow implements MainThreadWindowShape { return Promise.resolve(this.hostService.hasFocus); } - async $openUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { + async $openUri(uriComponents: UriComponents, uriString: string | undefined, options: IOpenUriOptions): Promise { const uri = URI.from(uriComponents); - return this.openerService.open(uri, { openExternal: true, allowTunneling: options.allowTunneling }); + let target: URI | string; + if (uriString && URI.parse(uriString).toString() === uri.toString()) { + // called with string and no transformation happened -> keep string + target = uriString; + } else { + // called with URI or transformed -> use uri + target = uri; + } + return this.openerService.open(target, { openExternal: true, allowTunneling: options.allowTunneling }); } async $asExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9bc433a1727..5df456ab6c7 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -759,7 +759,7 @@ export interface IOpenUriOptions { export interface MainThreadWindowShape extends IDisposable { $getWindowVisibility(): Promise; - $openUri(uri: UriComponents, options: IOpenUriOptions): Promise; + $openUri(uri: UriComponents, uriString: string | undefined, options: IOpenUriOptions): Promise; $asExternalUri(uri: UriComponents, options: IOpenUriOptions): Promise; } diff --git a/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts index b6f94048fbd..c06e86c8305 100644 --- a/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -245,9 +245,9 @@ class OpenNodeModuleFactory implements INodeModuleFactory { return this.callOriginal(target, options); } if (uri.scheme === 'http' || uri.scheme === 'https') { - return mainThreadWindow.$openUri(uri, { allowTunneling: true }); + return mainThreadWindow.$openUri(uri, target, { allowTunneling: true }); } else if (uri.scheme === 'mailto' || uri.scheme === this._appUriScheme) { - return mainThreadWindow.$openUri(uri, {}); + return mainThreadWindow.$openUri(uri, target, {}); } return this.callOriginal(target, options); }; diff --git a/src/vs/workbench/api/common/extHostWindow.ts b/src/vs/workbench/api/common/extHostWindow.ts index 8ce82ac8b2d..f8c5d5fa3d7 100644 --- a/src/vs/workbench/api/common/extHostWindow.ts +++ b/src/vs/workbench/api/common/extHostWindow.ts @@ -39,7 +39,9 @@ export class ExtHostWindow implements ExtHostWindowShape { } openUri(stringOrUri: string | URI, options: IOpenUriOptions): Promise { + let uriAsString: string | undefined; if (typeof stringOrUri === 'string') { + uriAsString = stringOrUri; try { stringOrUri = URI.parse(stringOrUri); } catch (e) { @@ -51,7 +53,7 @@ export class ExtHostWindow implements ExtHostWindowShape { } else if (stringOrUri.scheme === Schemas.command) { return Promise.reject(`Invalid scheme '${stringOrUri.scheme}'`); } - return this._proxy.$openUri(stringOrUri, options); + return this._proxy.$openUri(stringOrUri, uriAsString, options); } async asExternalUri(uri: URI, options: IOpenUriOptions): Promise { diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 299a7271b2b..c499a52051e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -8,7 +8,6 @@ import * as nls from 'vs/nls'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { CommentNode, CommentsModel, ResourceWithCommentThreads } from 'vs/workbench/contrib/comments/common/commentModel'; @@ -127,12 +126,7 @@ export class CommentNodeRenderer implements IListRenderer inline: true, actionHandler: { callback: (content) => { - try { - const uri = URI.parse(content); - this.openerService.open(uri).catch(onUnexpectedError); - } catch (err) { - // ignore - } + this.openerService.open(content).catch(onUnexpectedError); }, disposeables: disposables } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 329d660bca6..d026d0da526 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -28,7 +28,6 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ISpliceable } from 'vs/base/common/sequence'; import { escapeRegExpCharacters, startsWith } from 'vs/base/common/strings'; -import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -485,15 +484,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre }; this._onDidClickSettingLink.fire(e); } else { - let uri: URI | undefined; - try { - uri = URI.parse(content); - } catch (err) { - // ignore - } - if (uri) { - this._openerService.open(uri).catch(onUnexpectedError); - } + this._openerService.open(content).catch(onUnexpectedError); } }, disposeables diff --git a/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts b/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts index 544cabcde0d..6cb08ae0b26 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts @@ -266,8 +266,7 @@ export class TerminalLinkHandler { } private _handleHypertextLink(url: string): void { - const uri = URI.parse(url); - this._openerService.open(uri, { allowTunneling: !!(this._processManager && this._processManager.remoteAuthority) }); + this._openerService.open(url, { allowTunneling: !!(this._processManager && this._processManager.remoteAuthority) }); } private _isLinkActivationModifierDown(event: MouseEvent): boolean { diff --git a/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts index f930d68bccf..d8b03dc04d0 100644 --- a/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts +++ b/src/vs/workbench/contrib/url/common/trustedDomainsValidator.ts @@ -5,11 +5,10 @@ import { Schemas } from 'vs/base/common/network'; import Severity from 'vs/base/common/severity'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -21,6 +20,7 @@ import { import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; + export class OpenerValidatorContributions implements IWorkbenchContribution { constructor( @IOpenerService private readonly _openerService: IOpenerService, @@ -34,13 +34,16 @@ export class OpenerValidatorContributions implements IWorkbenchContribution { this._openerService.registerValidator({ shouldOpen: r => this.validateLink(r) }); } - async validateLink(resource: URI): Promise { - const { scheme, authority, path, query, fragment } = resource; - - if (!equalsIgnoreCase(scheme, Schemas.http) && !equalsIgnoreCase(scheme, Schemas.https)) { + async validateLink(resource: URI | string): Promise { + if (!matchesScheme(resource, Schemas.http) && !matchesScheme(resource, Schemas.https)) { return true; } + if (typeof resource === 'string') { + resource = URI.parse(resource); + } + const { scheme, authority, path, query, fragment } = resource; + const domainToOpen = `${scheme}://${authority}`; const { defaultTrustedDomains, trustedDomains } = readTrustedDomains(this._storageService, this._productService); const allTrustedDomains = [...defaultTrustedDomains, ...trustedDomains]; diff --git a/src/vs/workbench/services/url/electron-browser/urlService.ts b/src/vs/workbench/services/url/electron-browser/urlService.ts index 73fa15e1f8e..c1485a6ec59 100644 --- a/src/vs/workbench/services/url/electron-browser/urlService.ts +++ b/src/vs/workbench/services/url/electron-browser/urlService.ts @@ -8,7 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; import { URLHandlerChannel } from 'vs/platform/url/common/urlIpc'; import { URLService } from 'vs/platform/url/node/urlService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IOpenerService, IOpener, matchesScheme } from 'vs/platform/opener/common/opener'; import product from 'vs/platform/product/common/product'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService'; @@ -20,7 +20,7 @@ export interface IRelayOpenURLOptions extends IOpenURLOptions { openExternal?: boolean; } -export class RelayURLService extends URLService implements IURLHandler { +export class RelayURLService extends URLService implements IURLHandler, IOpener { private urlService: IURLService; @@ -51,11 +51,15 @@ export class RelayURLService extends URLService implements IURLHandler { return uri.with({ query }); } - async open(resource: URI, options?: IRelayOpenURLOptions): Promise { - if (resource.scheme !== product.urlProtocol) { + async open(resource: URI | string, options?: IRelayOpenURLOptions): Promise { + + if (!matchesScheme(resource, product.urlProtocol)) { return false; } + if (typeof resource === 'string') { + resource = URI.parse(resource); + } return await this.urlService.open(resource, options); }