Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/services/openerService.ts
5256 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../base/browser/dom.js';
7
import { mainWindow } from '../../../base/browser/window.js';
8
import { CancellationToken } from '../../../base/common/cancellation.js';
9
import { IDisposable } from '../../../base/common/lifecycle.js';
10
import { LinkedList } from '../../../base/common/linkedList.js';
11
import { ResourceMap } from '../../../base/common/map.js';
12
import { parse } from '../../../base/common/marshalling.js';
13
import { matchesScheme, matchesSomeScheme, Schemas } from '../../../base/common/network.js';
14
import { normalizePath } from '../../../base/common/resources.js';
15
import { URI } from '../../../base/common/uri.js';
16
import { ICodeEditorService } from './codeEditorService.js';
17
import { ICommandService } from '../../../platform/commands/common/commands.js';
18
import { EditorOpenSource } from '../../../platform/editor/common/editor.js';
19
import { extractSelection, IExternalOpener, IExternalUriResolver, IOpener, IOpenerService, IResolvedExternalUri, IValidator, OpenOptions, ResolveExternalUriOptions } from '../../../platform/opener/common/opener.js';
20
21
class CommandOpener implements IOpener {
22
23
constructor(@ICommandService private readonly _commandService: ICommandService) { }
24
25
async open(target: URI | string, options?: OpenOptions): Promise<boolean> {
26
if (!matchesScheme(target, Schemas.command)) {
27
return false;
28
}
29
30
if (!options?.allowCommands) {
31
// silently ignore commands when command-links are disabled, also
32
// suppress other openers by returning TRUE
33
return true;
34
}
35
36
if (typeof target === 'string') {
37
target = URI.parse(target);
38
}
39
40
if (Array.isArray(options.allowCommands)) {
41
// Only allow specific commands
42
if (!options.allowCommands.includes(target.path)) {
43
// Suppress other openers by returning TRUE
44
return true;
45
}
46
}
47
48
// execute as command
49
let args: unknown[] = [];
50
try {
51
args = parse(decodeURIComponent(target.query));
52
} catch {
53
// ignore and retry
54
try {
55
args = parse(target.query);
56
} catch {
57
// ignore error
58
}
59
}
60
if (!Array.isArray(args)) {
61
args = [args];
62
}
63
await this._commandService.executeCommand(target.path, ...args);
64
return true;
65
}
66
}
67
68
class EditorOpener implements IOpener {
69
70
constructor(@ICodeEditorService private readonly _editorService: ICodeEditorService) { }
71
72
async open(target: URI | string, options: OpenOptions) {
73
if (typeof target === 'string') {
74
target = URI.parse(target);
75
}
76
77
const { selection, uri } = extractSelection(target);
78
target = uri;
79
80
if (target.scheme === Schemas.file) {
81
target = normalizePath(target); // workaround for non-normalized paths (https://github.com/microsoft/vscode/issues/12954)
82
}
83
84
await this._editorService.openCodeEditor(
85
{
86
resource: target,
87
options: {
88
selection,
89
source: options?.fromUserGesture ? EditorOpenSource.USER : EditorOpenSource.API,
90
...options?.editorOptions
91
}
92
},
93
this._editorService.getFocusedCodeEditor(),
94
options?.openToSide
95
);
96
97
return true;
98
}
99
}
100
101
export class OpenerService implements IOpenerService {
102
103
declare readonly _serviceBrand: undefined;
104
105
private readonly _openers = new LinkedList<IOpener>();
106
private readonly _validators = new LinkedList<IValidator>();
107
private readonly _resolvers = new LinkedList<IExternalUriResolver>();
108
private readonly _resolvedUriTargets = new ResourceMap<URI>(uri => uri.with({ path: null, fragment: null, query: null }).toString());
109
110
private _defaultExternalOpener: IExternalOpener;
111
private readonly _externalOpeners = new LinkedList<IExternalOpener>();
112
113
constructor(
114
@ICodeEditorService editorService: ICodeEditorService,
115
@ICommandService commandService: ICommandService
116
) {
117
// Default external opener is going through window.open()
118
this._defaultExternalOpener = {
119
openExternal: async href => {
120
// ensure to open HTTP/HTTPS links into new windows
121
// to not trigger a navigation. Any other link is
122
// safe to be set as HREF to prevent a blank window
123
// from opening.
124
if (matchesSomeScheme(href, Schemas.http, Schemas.https)) {
125
dom.windowOpenNoOpener(href);
126
} else {
127
mainWindow.location.href = href;
128
}
129
return true;
130
}
131
};
132
133
// Default opener: any external, maito, http(s), command, and catch-all-editors
134
this._openers.push({
135
open: async (target: URI | string, options?: OpenOptions) => {
136
if (options?.openExternal || matchesSomeScheme(target, Schemas.mailto, Schemas.http, Schemas.https, Schemas.vsls)) {
137
// open externally
138
await this._doOpenExternal(target, options);
139
return true;
140
}
141
return false;
142
}
143
});
144
this._openers.push(new CommandOpener(commandService));
145
this._openers.push(new EditorOpener(editorService));
146
}
147
148
registerOpener(opener: IOpener): IDisposable {
149
const remove = this._openers.unshift(opener);
150
return { dispose: remove };
151
}
152
153
registerValidator(validator: IValidator): IDisposable {
154
const remove = this._validators.push(validator);
155
return { dispose: remove };
156
}
157
158
registerExternalUriResolver(resolver: IExternalUriResolver): IDisposable {
159
const remove = this._resolvers.push(resolver);
160
return { dispose: remove };
161
}
162
163
setDefaultExternalOpener(externalOpener: IExternalOpener): void {
164
this._defaultExternalOpener = externalOpener;
165
}
166
167
registerExternalOpener(opener: IExternalOpener): IDisposable {
168
const remove = this._externalOpeners.push(opener);
169
return { dispose: remove };
170
}
171
172
async open(target: URI | string, options?: OpenOptions): Promise<boolean> {
173
const targetURI = typeof target === 'string' ? URI.parse(target) : target;
174
175
// Internal schemes are not openable and must instead be handled in event listeners
176
if (targetURI.scheme === Schemas.internal) {
177
return false;
178
}
179
180
// check with contributed validators
181
if (!options?.skipValidation) {
182
const validationTarget = this._resolvedUriTargets.get(targetURI) ?? target; // validate against the original URI that this URI resolves to, if one exists
183
for (const validator of this._validators) {
184
if (!(await validator.shouldOpen(validationTarget, options))) {
185
return false;
186
}
187
}
188
}
189
190
// check with contributed openers
191
for (const opener of this._openers) {
192
const handled = await opener.open(target, options);
193
if (handled) {
194
return true;
195
}
196
}
197
198
return false;
199
}
200
201
async resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise<IResolvedExternalUri> {
202
for (const resolver of this._resolvers) {
203
try {
204
const result = await resolver.resolveExternalUri(resource, options);
205
if (result) {
206
if (!this._resolvedUriTargets.has(result.resolved)) {
207
this._resolvedUriTargets.set(result.resolved, resource);
208
}
209
return result;
210
}
211
} catch {
212
// noop
213
}
214
}
215
216
throw new Error('Could not resolve external URI: ' + resource.toString());
217
}
218
219
private async _doOpenExternal(resource: URI | string, options: OpenOptions | undefined): Promise<boolean> {
220
221
//todo@jrieken IExternalUriResolver should support `uri: URI | string`
222
const uri = typeof resource === 'string' ? URI.parse(resource) : resource;
223
let externalUri: URI;
224
225
try {
226
externalUri = (await this.resolveExternalUri(uri, options)).resolved;
227
} catch {
228
externalUri = uri;
229
}
230
231
let href: string;
232
if (typeof resource === 'string' && uri.toString() === externalUri.toString()) {
233
// open the url-string AS IS
234
href = resource;
235
} else {
236
// open URI using the toString(noEncode)+encodeURI-trick
237
href = encodeURI(externalUri.toString(true));
238
}
239
240
if (options?.allowContributedOpeners) {
241
const preferredOpenerId = typeof options?.allowContributedOpeners === 'string' ? options?.allowContributedOpeners : undefined;
242
for (const opener of this._externalOpeners) {
243
const didOpen = await opener.openExternal(href, {
244
sourceUri: uri,
245
preferredOpenerId,
246
}, CancellationToken.None);
247
if (didOpen) {
248
return true;
249
}
250
}
251
}
252
253
return this._defaultExternalOpener.openExternal(href, { sourceUri: uri }, CancellationToken.None);
254
}
255
256
dispose() {
257
this._validators.clear();
258
}
259
}
260
261