externalUriOpenerService.ts ×9

Frontier kind: Code frontier

unlabeled · c_6413fd8b4119

3 tests · 20988 LOC · 79 files · introduces 0 tests · 172 LOC · 2 files

Introduces — evidence that enters the hierarchy at this concept

Code
10 ranges172 lines · 2 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
2147 ranges20988 lines · 79 files · Browse complete extent
All tests (intent)
3 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

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 native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

2 files ranked by introduced lines: 172 introduced LOC across 10 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts 105 introduced LOC · 9 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- externalUriOpenerService.ts
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) {
80 return [];
81 }
132
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
135 return [];
136 }
137
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;
148 > } else if (allOpeners.length === 1) { externalUriOpenerService.ts
149 return allOpeners[0].openExternalUri(targetUri, ctx, token);
150 }
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) {
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);
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) || {};
176 for (const [uriGlob, id] of Object.entries(config)) {
188 return undefined;
189 }
191 > private async showOpenerPrompt(
192 openers: ReadonlyArray<IExternalUriOpener>,
193 targetUri: URI,
src/vs/workbench/contrib/externalUriOpener/common/configuration.ts 67 introduced LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- configuration.ts
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 { IConfigurationNode, IConfigurationRegistry, Extensions } from '../../../../platform/configuration/common/configurationRegistry.js';
7 > import { workbenchConfigurationNodeBase } from '../../../common/configuration.js';
8 > import * as nls from '../../../../nls.js';
9 > import { IJSONSchema } from '../../../../base/common/jsonSchema.js';
10 > import { Registry } from '../../../../platform/registry/common/platform.js';
11 >
12 > export const defaultExternalUriOpenerId = 'default';
13 >
14 > export const externalUriOpenersSettingId = 'workbench.externalUriOpeners';
15 >
16 > export interface ExternalUriOpenersConfiguration {
17 > readonly [uriGlob: string]: string;
18 > }
19 >
20 > const externalUriOpenerIdSchemaAddition: IJSONSchema = {
21 > type: 'string',
22 > enum: []
23 > };
24 >
25 > const exampleUriPatterns = `
26 > - \`https://microsoft.com\`: Matches this specific domain using https
27 > - \`https://microsoft.com:8080\`: Matches this specific domain on this port using https
28 > - \`https://microsoft.com:*\`: Matches this specific domain on any port using https
29 > - \`https://microsoft.com/foo\`: Matches \`https://microsoft.com/foo\` and \`https://microsoft.com/foo/bar\`, but not \`https://microsoft.com/foobar\` or \`https://microsoft.com/bar\`
30 > - \`https://*.microsoft.com\`: Match all domains ending in \`microsoft.com\` using https
31 > - \`microsoft.com\`: Match this specific domain using either http or https
32 > - \`*.microsoft.com\`: Match all domains ending in \`microsoft.com\` using either http or https
33 > - \`http://192.168.0.1\`: Matches this specific IP using http
34 > - \`http://192.168.0.*\`: Matches all IP's with this prefix using http
35 > - \`*\`: Match all domains using either http or https`;
36 >
37 > export const externalUriOpenersConfigurationNode: IConfigurationNode = {
38 > ...workbenchConfigurationNodeBase,
39 > properties: {
40 > [externalUriOpenersSettingId]: {
41 > type: 'object',
42 > markdownDescription: nls.localize('externalUriOpeners', "Configure the opener to use for external URIs (http, https)."),
43 > defaultSnippets: [{
44 > body: {
45 > 'example.com': '$1'
46 > }
47 > }],
48 > additionalProperties: {
49 > anyOf: [
50 > {
51 > type: 'string',
52 > markdownDescription: nls.localize('externalUriOpeners.uri', "Map URI pattern to an opener id.\nExample patterns: \n{0}", exampleUriPatterns),
53 > },
54 > {
55 > type: 'string',
56 > markdownDescription: nls.localize('externalUriOpeners.uri', "Map URI pattern to an opener id.\nExample patterns: \n{0}", exampleUriPatterns),
57 > enum: [defaultExternalUriOpenerId],
58 > enumDescriptions: [nls.localize('externalUriOpeners.defaultId', "Open using VS Code's standard opener.")],
59 > },
60 > externalUriOpenerIdSchemaAddition
61 > ]
62 > }
63 > }
64 > }
65 > };
66 >
67 > export function updateContributedOpeners(enumValues: string[], enumDescriptions: string[]): void {
68 externalUriOpenerIdSchemaAddition.enum = enumValues;
69 externalUriOpenerIdSchemaAddition.enumDescriptions = enumDescriptions;