Skip to content

Commit 9106843

Browse files
committed
IOpener must accept URI and URL
1 parent e24fefe commit 9106843

4 files changed

Lines changed: 124 additions & 90 deletions

File tree

src/vs/editor/browser/services/openerService.ts

Lines changed: 102 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as dom from 'vs/base/browser/dom';
7-
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
7+
import { IDisposable } from 'vs/base/common/lifecycle';
88
import { LinkedList } from 'vs/base/common/linkedList';
99
import { parse } from 'vs/base/common/marshalling';
1010
import { Schemas } from 'vs/base/common/network';
@@ -16,46 +16,123 @@ import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/c
1616
import { IOpener, IOpenerService, IValidator, IExternalUriResolver, OpenOptions, ResolveExternalUriOptions, IResolvedExternalUri, IExternalOpener } from 'vs/platform/opener/common/opener';
1717
import { EditorOpenContext } from 'vs/platform/editor/common/editor';
1818

19-
export class OpenerService extends Disposable implements IOpenerService {
19+
function hasScheme(target: URI | URL, scheme: string) {
20+
if (URI.isUri(target)) {
21+
return equalsIgnoreCase(target.scheme, scheme);
22+
} else {
23+
return equalsIgnoreCase(target.protocol, scheme + ':');
24+
}
25+
}
26+
27+
export class OpenerService implements IOpenerService {
2028

2129
_serviceBrand: undefined;
2230

2331
private readonly _openers = new LinkedList<IOpener>();
2432
private readonly _validators = new LinkedList<IValidator>();
2533
private readonly _resolvers = new LinkedList<IExternalUriResolver>();
34+
2635
private _externalOpener: IExternalOpener;
36+
private _openerAsExternal: IOpener;
37+
private _openerAsCommand: IOpener;
38+
private _openerAsEditor: IOpener;
2739

2840
constructor(
29-
@ICodeEditorService private readonly _editorService: ICodeEditorService,
30-
@ICommandService private readonly _commandService: ICommandService,
41+
@ICodeEditorService editorService: ICodeEditorService,
42+
@ICommandService commandService: ICommandService,
3143
) {
32-
super();
33-
3444
// Default external opener is going through window.open()
3545
this._externalOpener = {
3646
openExternal: href => {
3747
dom.windowOpenNoOpener(href);
38-
3948
return Promise.resolve(true);
4049
}
4150
};
51+
52+
// Default opener: maito, http(s), command, and catch-all-editors
53+
this._openerAsExternal = {
54+
open: async (target: URI | URL, options?: OpenOptions) => {
55+
if (options?.openExternal || hasScheme(target, Schemas.mailto) || hasScheme(target, Schemas.http) || hasScheme(target, Schemas.https)) {
56+
// open externally
57+
await this._doOpenExternal(target, options);
58+
return true;
59+
}
60+
return false;
61+
}
62+
};
63+
64+
this._openerAsCommand = {
65+
open: async (target) => {
66+
if (!hasScheme(target, Schemas.command)) {
67+
return false;
68+
}
69+
// run command or bail out if command isn't known
70+
if (!URI.isUri(target)) {
71+
target = URI.from(target);
72+
}
73+
if (!CommandsRegistry.getCommand(target.path)) {
74+
throw new Error(`command '${target.path}' NOT known`);
75+
}
76+
// execute as command
77+
let args: any = [];
78+
try {
79+
args = parse(target.query);
80+
if (!Array.isArray(args)) {
81+
args = [args];
82+
}
83+
} catch (e) {
84+
// ignore error
85+
}
86+
await commandService.executeCommand(target.path, ...args);
87+
return true;
88+
}
89+
};
90+
91+
this._openerAsEditor = {
92+
open: async (target, options: OpenOptions) => {
93+
if (!URI.isUri(target)) {
94+
target = URI.from(target);
95+
}
96+
let selection: { startLineNumber: number; startColumn: number; } | undefined = undefined;
97+
const match = /^L?(\d+)(?:,(\d+))?/.exec(target.fragment);
98+
if (match) {
99+
// support file:///some/file.js#73,84
100+
// support file:///some/file.js#L73
101+
selection = {
102+
startLineNumber: parseInt(match[1]),
103+
startColumn: match[2] ? parseInt(match[2]) : 1
104+
};
105+
// remove fragment
106+
target = target.with({ fragment: '' });
107+
}
108+
109+
if (target.scheme === Schemas.file) {
110+
target = resources.normalizePath(target); // workaround for non-normalized paths (https://github.com/Microsoft/vscode/issues/12954)
111+
}
112+
113+
await editorService.openCodeEditor(
114+
{ resource: target, options: { selection, context: options?.fromUserGesture ? EditorOpenContext.USER : EditorOpenContext.API } },
115+
editorService.getFocusedCodeEditor(),
116+
options?.openToSide
117+
);
118+
119+
return true;
120+
}
121+
};
42122
}
43123

44124
registerOpener(opener: IOpener): IDisposable {
45125
const remove = this._openers.push(opener);
46-
47126
return { dispose: remove };
48127
}
49128

50129
registerValidator(validator: IValidator): IDisposable {
51130
const remove = this._validators.push(validator);
52-
53131
return { dispose: remove };
54132
}
55133

56134
registerExternalUriResolver(resolver: IExternalUriResolver): IDisposable {
57135
const remove = this._resolvers.push(resolver);
58-
59136
return { dispose: remove };
60137
}
61138

@@ -86,7 +163,12 @@ export class OpenerService extends Disposable implements IOpenerService {
86163
}
87164

88165
// use default openers
89-
return this._doOpen(resource, options);
166+
for (const opener of [this._openerAsExternal, this._openerAsCommand, this._openerAsEditor]) {
167+
if (await opener.open(resource, options)) {
168+
break;
169+
}
170+
}
171+
return true;
90172
}
91173

92174
async resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise<IResolvedExternalUri> {
@@ -100,68 +182,16 @@ export class OpenerService extends Disposable implements IOpenerService {
100182
return { resolved: resource, dispose: () => { } };
101183
}
102184

103-
private async _doOpen(resource: URI, options: OpenOptions | undefined): Promise<boolean> {
104-
const { scheme, path, query, fragment } = resource;
105-
106-
if (options?.openExternal || equalsIgnoreCase(scheme, Schemas.mailto) || equalsIgnoreCase(scheme, Schemas.http) || equalsIgnoreCase(scheme, Schemas.https)) {
107-
// open externally
108-
return this._doOpenExternal(resource, options);
109-
}
110-
111-
if (equalsIgnoreCase(scheme, Schemas.command)) {
112-
// run command or bail out if command isn't known
113-
if (!CommandsRegistry.getCommand(path)) {
114-
throw new Error(`command '${path}' NOT known`);
115-
}
116-
// execute as command
117-
let args: any = [];
118-
try {
119-
args = parse(query);
120-
if (!Array.isArray(args)) {
121-
args = [args];
122-
}
123-
} catch (e) {
124-
// ignore error
125-
}
126-
127-
await this._commandService.executeCommand(path, ...args);
128-
129-
return true;
130-
}
131-
132-
// finally open in editor
133-
let selection: { startLineNumber: number; startColumn: number; } | undefined = undefined;
134-
const match = /^L?(\d+)(?:,(\d+))?/.exec(fragment);
135-
if (match) {
136-
// support file:///some/file.js#73,84
137-
// support file:///some/file.js#L73
138-
selection = {
139-
startLineNumber: parseInt(match[1]),
140-
startColumn: match[2] ? parseInt(match[2]) : 1
141-
};
142-
// remove fragment
143-
resource = resource.with({ fragment: '' });
144-
}
145-
146-
if (resource.scheme === Schemas.file) {
147-
resource = resources.normalizePath(resource); // workaround for non-normalized paths (https://github.com/Microsoft/vscode/issues/12954)
185+
private async _doOpenExternal(resource: URI | URL, options: OpenOptions | undefined): Promise<boolean> {
186+
if (URI.isUri(resource)) {
187+
const { resolved } = await this.resolveExternalUri(resource, options);
188+
// TODO@Jo neither encodeURI nor toString(true) should be needed
189+
// once we go with URL and not URI
190+
return this._externalOpener.openExternal(encodeURI(resolved.toString(true)));
191+
} else {
192+
//todo@joh what about resolveExternalUri?
193+
return this._externalOpener.openExternal(resource.href);
148194
}
149-
150-
await this._editorService.openCodeEditor(
151-
{ resource, options: { selection, context: options?.fromUserGesture ? EditorOpenContext.USER : EditorOpenContext.API } },
152-
this._editorService.getFocusedCodeEditor(),
153-
options?.openToSide
154-
);
155-
156-
return true;
157-
}
158-
159-
private async _doOpenExternal(resource: URI, options: OpenOptions | undefined): Promise<boolean> {
160-
const { resolved } = await this.resolveExternalUri(resource, options);
161-
162-
// TODO@Jo neither encodeURI nor toString(true) should be needed
163-
// once we go with URL and not URI
164-
return this._externalOpener.openExternal(encodeURI(resolved.toString(true)));
165195
}
166196

167197
dispose() {

src/vs/editor/test/browser/services/openerService.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,68 +28,68 @@ suite('OpenerService', function () {
2828
lastCommand = undefined;
2929
});
3030

31-
test('delegate to editorService, scheme:///fff', function () {
31+
test('delegate to editorService, scheme:///fff', async function () {
3232
const openerService = new OpenerService(editorService, NullCommandService);
33-
openerService.open(URI.parse('another:///somepath'));
33+
await openerService.open(URI.parse('another:///somepath'));
3434
assert.equal(editorService.lastInput!.options!.selection, undefined);
3535
});
3636

37-
test('delegate to editorService, scheme:///fff#L123', function () {
37+
test('delegate to editorService, scheme:///fff#L123', async function () {
3838
const openerService = new OpenerService(editorService, NullCommandService);
3939

40-
openerService.open(URI.parse('file:///somepath#L23'));
40+
await openerService.open(URI.parse('file:///somepath#L23'));
4141
assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23);
4242
assert.equal(editorService.lastInput!.options!.selection!.startColumn, 1);
4343
assert.equal(editorService.lastInput!.options!.selection!.endLineNumber, undefined);
4444
assert.equal(editorService.lastInput!.options!.selection!.endColumn, undefined);
4545
assert.equal(editorService.lastInput!.resource.fragment, '');
4646

47-
openerService.open(URI.parse('another:///somepath#L23'));
47+
await openerService.open(URI.parse('another:///somepath#L23'));
4848
assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23);
4949
assert.equal(editorService.lastInput!.options!.selection!.startColumn, 1);
5050

51-
openerService.open(URI.parse('another:///somepath#L23,45'));
51+
await openerService.open(URI.parse('another:///somepath#L23,45'));
5252
assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23);
5353
assert.equal(editorService.lastInput!.options!.selection!.startColumn, 45);
5454
assert.equal(editorService.lastInput!.options!.selection!.endLineNumber, undefined);
5555
assert.equal(editorService.lastInput!.options!.selection!.endColumn, undefined);
5656
assert.equal(editorService.lastInput!.resource.fragment, '');
5757
});
5858

59-
test('delegate to editorService, scheme:///fff#123,123', function () {
59+
test('delegate to editorService, scheme:///fff#123,123', async function () {
6060
const openerService = new OpenerService(editorService, NullCommandService);
6161

62-
openerService.open(URI.parse('file:///somepath#23'));
62+
await openerService.open(URI.parse('file:///somepath#23'));
6363
assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23);
6464
assert.equal(editorService.lastInput!.options!.selection!.startColumn, 1);
6565
assert.equal(editorService.lastInput!.options!.selection!.endLineNumber, undefined);
6666
assert.equal(editorService.lastInput!.options!.selection!.endColumn, undefined);
6767
assert.equal(editorService.lastInput!.resource.fragment, '');
6868

69-
openerService.open(URI.parse('file:///somepath#23,45'));
69+
await openerService.open(URI.parse('file:///somepath#23,45'));
7070
assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23);
7171
assert.equal(editorService.lastInput!.options!.selection!.startColumn, 45);
7272
assert.equal(editorService.lastInput!.options!.selection!.endLineNumber, undefined);
7373
assert.equal(editorService.lastInput!.options!.selection!.endColumn, undefined);
7474
assert.equal(editorService.lastInput!.resource.fragment, '');
7575
});
7676

77-
test('delegate to commandsService, command:someid', function () {
77+
test('delegate to commandsService, command:someid', async function () {
7878
const openerService = new OpenerService(editorService, commandService);
7979

8080
const id = `aCommand${Math.random()}`;
8181
CommandsRegistry.registerCommand(id, function () { });
8282

83-
openerService.open(URI.parse('command:' + id));
83+
await openerService.open(URI.parse('command:' + id));
8484
assert.equal(lastCommand!.id, id);
8585
assert.equal(lastCommand!.args.length, 0);
8686

87-
openerService.open(URI.parse('command:' + id).with({ query: '123' }));
87+
await openerService.open(URI.parse('command:' + id).with({ query: '123' }));
8888
assert.equal(lastCommand!.id, id);
8989
assert.equal(lastCommand!.args.length, 1);
9090
assert.equal(lastCommand!.args[0], '123');
9191

92-
openerService.open(URI.parse('command:' + id).with({ query: JSON.stringify([12, true]) }));
92+
await openerService.open(URI.parse('command:' + id).with({ query: JSON.stringify([12, true]) }));
9393
assert.equal(lastCommand!.id, id);
9494
assert.equal(lastCommand!.args.length, 2);
9595
assert.equal(lastCommand!.args[0], 12);

src/vs/platform/opener/common/opener.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ export interface IResolvedExternalUri extends IDisposable {
3535
}
3636

3737
export interface IOpener {
38-
open(resource: URI, options?: OpenInternalOptions): Promise<boolean>;
39-
open(resource: URI, options?: OpenExternalOptions): Promise<boolean>;
38+
open(resource: URI | URL, options?: OpenInternalOptions | OpenExternalOptions): Promise<boolean>;
4039
}
4140

4241
export interface IExternalOpener {

src/vs/workbench/services/url/electron-browser/urlService.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri';
88
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
99
import { URLHandlerChannel } from 'vs/platform/url/common/urlIpc';
1010
import { URLService } from 'vs/platform/url/node/urlService';
11-
import { IOpenerService } from 'vs/platform/opener/common/opener';
11+
import { IOpenerService, IOpener } from 'vs/platform/opener/common/opener';
1212
import product from 'vs/platform/product/common/product';
1313
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
1414
import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService';
@@ -20,7 +20,7 @@ export interface IRelayOpenURLOptions extends IOpenURLOptions {
2020
openExternal?: boolean;
2121
}
2222

23-
export class RelayURLService extends URLService implements IURLHandler {
23+
export class RelayURLService extends URLService implements IURLHandler, IOpener {
2424

2525
private urlService: IURLService;
2626

@@ -51,7 +51,12 @@ export class RelayURLService extends URLService implements IURLHandler {
5151
return uri.with({ query });
5252
}
5353

54-
async open(resource: URI, options?: IRelayOpenURLOptions): Promise<boolean> {
54+
async open(resource: URI | URL, options?: IRelayOpenURLOptions): Promise<boolean> {
55+
56+
if (resource instanceof URL) {
57+
resource = URI.from(resource);
58+
}
59+
5560
if (resource.scheme !== product.urlProtocol) {
5661
return false;
5762
}

0 commit comments

Comments
 (0)