src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts

239 LOC · 152 covered · 87 uncovered · 23 ranges · 5 concepts · 5 introducers · 3 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/workbench/contrib/externalUriOpener/common/configuration.ts · 73 LOCcommon/configuration.tsexternalUriOpenerService.ts ×2 · 4 introduced LOCexternalUriOpenerService…externalUriOpenerService.ts ×1 · 2 introduced LOCexternalUriOpenerService…externalUriOpenerService.ts ×9 · 38 introduced LOCexternalUriOpenerService…externalUriOpenerService.ts ×2 · 3 introduced LOCexternalUriOpenerService…externalUriOpenerService.ts ×9 · 172 introduced LOCexternalUriOpenerService…externalUriOpenerService.test|title=ExternalUriOpenerService Should automatically pick single preferred opener without prompt|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test|title=ExternalUriOpenerService Should automatically pick single preferred opener without prompt|occurrence=1externalUriOpenerService…externalUriOpenerService.test|title=ExternalUriOpenerService Should not open if there are no openers|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test|title=ExternalUriOpenerService Should not open if there are no openers|occurrence=1externalUriOpenerService…externalUriOpenerService.test|title=ExternalUriOpenerService Should prompt if there is at least one enabled opener|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/contrib/externalUriOpener/test/common/externalUriOpenerService.test|title=ExternalUriOpenerService Should prompt if there is at least one enabled opener|occurrence=1externalUriOpenerService…Focused file · src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts · 239 LOCcommon/externalUriOpener…

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- externalUriOpenerService.ts ×9
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 { CancellationToken } from '../../../../base/common/cancellation.js';
7 > import { Iterable } from '../../../../base/common/iterator.js';
8 > import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
9 > import { LinkedList } from '../../../../base/common/linkedList.js';
10 > import { isWeb } from '../../../../base/common/platform.js';
11 > import { URI } from '../../../../base/common/uri.js';
12 > import * as languages from '../../../../editor/common/languages.js';
13 > import * as nls from '../../../../nls.js';
14 > import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
15 > import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
16 > import { ILogService } from '../../../../platform/log/common/log.js';
17 > import { IExternalOpener, IOpenerService } from '../../../../platform/opener/common/opener.js';
18 > import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
19 > import { defaultExternalUriOpenerId, ExternalUriOpenersConfiguration, externalUriOpenersSettingId } from './configuration.js';
20 > import { testUrlMatchesGlob } from '../../../../platform/url/common/urlGlob.js';
21 > import { IPreferencesService } from '../../../services/preferences/common/preferences.js';
22 >
23 >
24 > export const IExternalUriOpenerService = createDecorator<IExternalUriOpenerService>('externalUriOpenerService');
25 >
26 >
27 > export interface IExternalOpenerProvider {
28 > getOpeners(targetUri: URI): AsyncIterable<IExternalUriOpener>;
29 > }
30 >
31 > export interface IExternalUriOpener {
32 > readonly id: string;
33 > readonly label: string;
34 >
35 > canOpen(uri: URI, token: CancellationToken): Promise<languages.ExternalUriOpenerPriority>;
36 > openExternalUri(uri: URI, ctx: { sourceUri: URI }, token: CancellationToken): Promise<boolean>;
37 > }
38 >
39 > export interface IExternalUriOpenerService {
40 > readonly _serviceBrand: undefined;
41 >
42 > /**
43 > * Registers a provider for external resources openers.
44 > */
45 > registerExternalOpenerProvider(provider: IExternalOpenerProvider): IDisposable;
46 >
47 > /**
48 > * Get the configured IExternalUriOpener for the uri.
49 > * If there is no opener configured, then returns the first opener that can handle the uri.
50 > */
51 > getOpener(uri: URI, ctx: { sourceUri: URI; preferredOpenerId?: string }, token: CancellationToken): Promise<IExternalUriOpener | undefined>;
52 > }
53 >
54 > export class ExternalUriOpenerService extends Disposable implements IExternalUriOpenerService, IExternalOpener {
55 >
56 > public readonly _serviceBrand: undefined;
57 >
58 > private readonly _providers = new LinkedList<IExternalOpenerProvider>();
59 >
60 > constructor(
61 > @IOpenerService openerService: IOpenerService,
62 > @IConfigurationService private readonly configurationService: IConfigurationService,
63 > @ILogService private readonly logService: ILogService,
64 > @IPreferencesService private readonly preferencesService: IPreferencesService,
65 > @IQuickInputService private readonly quickInputService: IQuickInputService,
66 > ) {
67 > super();
68 > this._register(openerService.registerExternalOpener(this));
69 > }
70 >
71 > registerExternalOpenerProvider(provider: IExternalOpenerProvider): IDisposable {
72 > const remove = this._providers.push(provider);
73 > return { dispose: remove };
74 > }
75 >
76 > private async getOpeners(targetUri: URI, allowOptional: boolean, ctx: { sourceUri: URI; preferredOpenerId?: string }, token: CancellationToken): Promise<IExternalUriOpener[]> {
77 > const allOpeners = await this.getAllOpenersForUri(targetUri);
78 >
79 > if (allOpeners.size === 0) {
81 > }
83 > // First see if we have a preferredOpener
84 > if (ctx.preferredOpenerId) {
85 if (ctx.preferredOpenerId === defaultExternalUriOpenerId) {
86 return [];
87 }
88
89 const preferredOpener = allOpeners.get(ctx.preferredOpenerId);
90 if (preferredOpener) {
91 // Skip the `canOpen` check here since the opener was specifically requested.
92 return [preferredOpener];
93 }
94 }
96 > // Check to see if we have a configured opener
97 > const configuredOpener = this.getConfiguredOpenerForUri(allOpeners, targetUri);
98 > if (configuredOpener) {
99 // Skip the `canOpen` check here since the opener was specifically requested.
100 return configuredOpener === defaultExternalUriOpenerId ? [] : [configuredOpener];
101 }
103 > // Then check to see if there is a valid opener
104 > const validOpeners: Array<{ opener: IExternalUriOpener; priority: languages.ExternalUriOpenerPriority }> = [];
105 > await Promise.all(Array.from(allOpeners.values()).map(async opener => {
106 > let priority: languages.ExternalUriOpenerPriority;
107 > try {
108 > priority = await opener.canOpen(ctx.sourceUri, token);
109 > } catch (e) {
110 this.logService.error(e);
111 return;
112 }
114 > switch (priority) {
115 > case languages.ExternalUriOpenerPriority.Option:
116 > case languages.ExternalUriOpenerPriority.Default:
117 > case languages.ExternalUriOpenerPriority.Preferred:
118 > validOpeners.push({ opener, priority });
119 > break;
120 > }
121 > }));
122 >
123 > if (validOpeners.length === 0) {
124 return [];
125 }
127 > // See if we have a preferred opener first
128 > const preferred = validOpeners.filter(x => x.priority === languages.ExternalUriOpenerPriority.Preferred).at(0);
129 > if (preferred) {
130 > return [preferred.opener]; externalUriOpenerService.ts ×1
131 > }
133 > // See if we only have optional openers, use the default opener
134 > if (!allowOptional && validOpeners.every(x => x.priority === languages.ExternalUriOpenerPriority.Option)) { externalUriOpenerService.ts ×9
135 return [];
136 }
138 > return validOpeners.map(value => value.opener);
140 >
141 > async openExternal(href: string, ctx: { sourceUri: URI; preferredOpenerId?: string }, token: CancellationToken): Promise<boolean> {
142 >
143 > const targetUri = typeof href === 'string' ? URI.parse(href) : href;
144 >
145 > const allOpeners = await this.getOpeners(targetUri, false, ctx, token);
146 > if (allOpeners.length === 0) {
147 > return false; externalUriOpenerService.ts ×2
148 > } else if (allOpeners.length === 1) { externalUriOpenerService.ts ×9
149 > return allOpeners[0].openExternalUri(targetUri, ctx, token); externalUriOpenerService.ts ×9
150 > }
151
152 // Otherwise prompt
153 return this.showOpenerPrompt(allOpeners, targetUri, ctx, token);
155 >
156 > async getOpener(targetUri: URI, ctx: { sourceUri: URI; preferredOpenerId?: string }, token: CancellationToken): Promise<IExternalUriOpener | undefined> {
157 const allOpeners = await this.getOpeners(targetUri, true, ctx, token);
158 if (allOpeners.length >= 1) {
159 return allOpeners[0];
160 }
161 return undefined;
162 }
164 > private async getAllOpenersForUri(targetUri: URI): Promise<Map<string, IExternalUriOpener>> {
165 > const allOpeners = new Map<string, IExternalUriOpener>();
166 > await Promise.all(Iterable.map(this._providers, async (provider) => {
167 > for await (const opener of provider.getOpeners(targetUri)) {
168 > allOpeners.set(opener.id, opener); externalUriOpenerService.ts ×9
169 > }
171 > return allOpeners;
172 > }
173 >
174 > private getConfiguredOpenerForUri(openers: Map<string, IExternalUriOpener>, targetUri: URI): IExternalUriOpener | 'default' | undefined {
175 > const config = this.configurationService.getValue<ExternalUriOpenersConfiguration>(externalUriOpenersSettingId) || {}; externalUriOpenerService.ts ×9
176 > for (const [uriGlob, id] of Object.entries(config)) {
177 if (testUrlMatchesGlob(targetUri, uriGlob)) {
178 if (id === defaultExternalUriOpenerId) {
179 return 'default';
180 }
181
182 const entry = openers.get(id);
183 if (entry) {
184 return entry;
185 }
186 }
187 }
188 > return undefined; externalUriOpenerService.ts ×9
189 > }
191 > private async showOpenerPrompt(
192 openers: ReadonlyArray<IExternalUriOpener>,
193 targetUri: URI,
194 ctx: { sourceUri: URI },
195 token: CancellationToken
196 ): Promise<boolean> {
197 type PickItem = IQuickPickItem & { opener?: IExternalUriOpener | 'configureDefault' };
198
199 const items: Array<PickItem | IQuickPickSeparator> = openers.map((opener): PickItem => {
200 return {
201 label: opener.label,
202 opener: opener
203 };
204 });
205 items.push(
206 {
207 label: isWeb
208 ? nls.localize('selectOpenerDefaultLabel.web', 'Open in new browser window')
209 : nls.localize('selectOpenerDefaultLabel', 'Open in default browser'),
210 opener: undefined
211 },
212 { type: 'separator' },
213 {
214 label: nls.localize('selectOpenerConfigureTitle', "Configure default opener..."),
215 opener: 'configureDefault'
216 });
217
218 const picked = await this.quickInputService.pick(items, {
219 placeHolder: nls.localize('selectOpenerPlaceHolder', "How would you like to open: {0}", targetUri.toString())
220 });
221
222 if (!picked) {
223 // Still cancel the default opener here since we prompted the user
224 return true;
225 }
226
227 if (typeof picked.opener === 'undefined') {
228 return false; // Fallback to default opener
229 } else if (picked.opener === 'configureDefault') {
230 await this.preferencesService.openUserSettings({
231 jsonEditor: true,
232 revealSetting: { key: externalUriOpenersSettingId, edit: true }
233 });
234 return true;
235 } else {
236 return picked.opener.openExternalUri(targetUri, ctx, token);
237 }
238 }