Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/services/openerService.ts
3294 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: any = [];
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
174
// check with contributed validators
175
if (!options?.skipValidation) {
176
const targetURI = typeof target === 'string' ? URI.parse(target) : target;
177
const validationTarget = this._resolvedUriTargets.get(targetURI) ?? target; // validate against the original URI that this URI resolves to, if one exists
178
for (const validator of this._validators) {
179
if (!(await validator.shouldOpen(validationTarget, options))) {
180
return false;
181
}
182
}
183
}
184
185
// check with contributed openers
186
for (const opener of this._openers) {
187
const handled = await opener.open(target, options);
188
if (handled) {
189
return true;
190
}
191
}
192
193
return false;
194
}
195
196
async resolveExternalUri(resource: URI, options?: ResolveExternalUriOptions): Promise<IResolvedExternalUri> {
197
for (const resolver of this._resolvers) {
198
try {
199
const result = await resolver.resolveExternalUri(resource, options);
200
if (result) {
201
if (!this._resolvedUriTargets.has(result.resolved)) {
202
this._resolvedUriTargets.set(result.resolved, resource);
203
}
204
return result;
205
}
206
} catch {
207
// noop
208
}
209
}
210
211
throw new Error('Could not resolve external URI: ' + resource.toString());
212
}
213
214
private async _doOpenExternal(resource: URI | string, options: OpenOptions | undefined): Promise<boolean> {
215
216
//todo@jrieken IExternalUriResolver should support `uri: URI | string`
217
const uri = typeof resource === 'string' ? URI.parse(resource) : resource;
218
let externalUri: URI;
219
220
try {
221
externalUri = (await this.resolveExternalUri(uri, options)).resolved;
222
} catch {
223
externalUri = uri;
224
}
225
226
let href: string;
227
if (typeof resource === 'string' && uri.toString() === externalUri.toString()) {
228
// open the url-string AS IS
229
href = resource;
230
} else {
231
// open URI using the toString(noEncode)+encodeURI-trick
232
href = encodeURI(externalUri.toString(true));
233
}
234
235
if (options?.allowContributedOpeners) {
236
const preferredOpenerId = typeof options?.allowContributedOpeners === 'string' ? options?.allowContributedOpeners : undefined;
237
for (const opener of this._externalOpeners) {
238
const didOpen = await opener.openExternal(href, {
239
sourceUri: uri,
240
preferredOpenerId,
241
}, CancellationToken.None);
242
if (didOpen) {
243
return true;
244
}
245
}
246
}
247
248
return this._defaultExternalOpener.openExternal(href, { sourceUri: uri }, CancellationToken.None);
249
}
250
251
dispose() {
252
this._validators.clear();
253
}
254
}
255
256