提交 47a135e7 编写于 作者: M Matt Bierner

Rework opener api proposal

For #109277

- Add more explicit two phase structure to api
- Make opener pass along label when registered
上级 6184addc
...@@ -12,7 +12,6 @@ const localize = nls.loadMessageBundle(); ...@@ -12,7 +12,6 @@ const localize = nls.loadMessageBundle();
const openApiCommand = 'simpleBrowser.api.open'; const openApiCommand = 'simpleBrowser.api.open';
const showCommand = 'simpleBrowser.show'; const showCommand = 'simpleBrowser.show';
const internalOpenCommand = '_simpleBrowser.open';
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
...@@ -36,15 +35,11 @@ export function activate(context: vscode.ExtensionContext) { ...@@ -36,15 +35,11 @@ export function activate(context: vscode.ExtensionContext) {
manager.show(url.toString(), showOptions); manager.show(url.toString(), showOptions);
})); }));
context.subscriptions.push(vscode.commands.registerCommand(internalOpenCommand, (resolvedUrl: vscode.Uri, _originalUri: vscode.Uri) => {
manager.show(resolvedUrl.toString());
}));
context.subscriptions.push(vscode.window.registerExternalUriOpener(['http', 'https'], { context.subscriptions.push(vscode.window.registerExternalUriOpener(['http', 'https'], {
openExternalUri(uri: vscode.Uri): vscode.Command | undefined { canOpenExternalUri(uri: vscode.Uri) {
const configuration = vscode.workspace.getConfiguration('simpleBrowser'); const configuration = vscode.workspace.getConfiguration('simpleBrowser');
if (!configuration.get('opener.enabled', false)) { if (!configuration.get('opener.enabled', false)) {
return undefined; return false;
} }
const enabledHosts = configuration.get<string[]>('opener.enabledHosts', [ const enabledHosts = configuration.get<string[]>('opener.enabledHosts', [
...@@ -54,17 +49,18 @@ export function activate(context: vscode.ExtensionContext) { ...@@ -54,17 +49,18 @@ export function activate(context: vscode.ExtensionContext) {
try { try {
const originalUri = new URL(uri.toString()); const originalUri = new URL(uri.toString());
if (!enabledHosts.includes(originalUri.hostname)) { if (!enabledHosts.includes(originalUri.hostname)) {
return; return false;
} }
} catch { } catch {
return undefined; return false;
} }
return { return true;
title: localize('openTitle', "Open in simple browser"), },
command: internalOpenCommand, openExternalUri(_opener, resolveUri) {
arguments: [uri] return manager.show(resolveUri.toString());
};
} }
}, {
label: localize('openTitle', "Open in simple browser"),
})); }));
} }
...@@ -2303,32 +2303,60 @@ declare module 'vscode' { ...@@ -2303,32 +2303,60 @@ declare module 'vscode' {
//#region Opener service (https://github.com/microsoft/vscode/issues/109277) //#region Opener service (https://github.com/microsoft/vscode/issues/109277)
/** /**
* Handles opening external uris. * Handles opening uris to external resources, such as http(s) links.
* *
* An extension can use this to open a `http` link to a webserver inside of VS Code instead of * Extensions can implement an `ExternalUriOpener` to open `http` links to a webserver
* having the link be opened by the webbrowser. * inside of VS Code instead of having the link be opened by the webbrowser.
* *
* Currently openers may only be registered for `http` and `https` uris. * Currently openers may only be registered for `http` and `https` uris.
*/ */
export interface ExternalUriOpener { export interface ExternalUriOpener {
/** /**
* Try to open a given uri. * Check if the opener can handle a given uri.
* *
* @param uri The uri to open. This uri may have been transformed by port forwarding. To access * @param uri The uri being opened. This is the uri that the user clicked on. It has
* the original uri that triggered the open, use `ctx.original`. * not yet gone through port forwarding.
* @param ctx Additional metadata about the triggered open. * @param token Cancellation token indicating that the result is no longer needed.
* @param token Cancellation token.
* *
* @return Optional command that opens the uri. If no command is returned, VS Code will * @return True if the opener can open the external uri.
* continue checking to see if any other openers are available. */
canOpenExternalUri(uri: Uri, token: CancellationToken): ProviderResult<boolean>;
/**
* Open the given uri.
*
* @param resolvedUri The uri to open. This uri may have been transformed by port forwarding, so it
* may not match the original uri passed to `canOpenExternalUri`. Use `ctx.originalUri` to check the
* original uri.
* @param ctx Additional information about the uri being opened.
* @param token Cancellation token indicating that opening has been canceled.
*
* @return Thenable indicating that the opening has completed
*/
openExternalUri(resolvedUri: Uri, ctx: OpenExternalUriContext, token: CancellationToken): Thenable<void> | void;
}
/**
* Additional information about the uri being opened.
*/
interface OpenExternalUriContext {
/**
* The uri that triggered the open.
* *
* This command is given the resolved uri to open. This may be different from the original `uri` due * Due to port forwarding, this may not match the `resolvedUri` passed to `openExternalUri`
* to port forwarding. */
readonly sourceUri: Uri;
}
interface ExternalUriOpenerMetadata {
/**
* Text displayed to the user that explains what the opener does.
* *
* If multiple openers are available for a given uri, then the `Command.title` is shown in the UI. * For example, 'Open in browser preview'
*/ */
openExternalUri(uri: Uri, ctx: OpenExternalUriContext, token: CancellationToken): ProviderResult<Command>; readonly label: string;
} }
namespace window { namespace window {
...@@ -2343,7 +2371,7 @@ declare module 'vscode' { ...@@ -2343,7 +2371,7 @@ declare module 'vscode' {
* *
* @returns Disposable that unregisters the opener. * @returns Disposable that unregisters the opener.
*/ */
export function registerExternalUriOpener(schemes: readonly string[], opener: ExternalUriOpener,): Disposable; export function registerExternalUriOpener(schemes: readonly string[], opener: ExternalUriOpener, metadata: ExternalUriOpenerMetadata): Disposable;
} }
//#endregion //#endregion
......
...@@ -7,13 +7,16 @@ import { CancellationToken } from 'vs/base/common/cancellation'; ...@@ -7,13 +7,16 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network'; import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { ExtHostContext, ExtHostUriOpener, ExtHostUriOpenersShape, IExtHostContext, MainContext, MainThreadUriOpenersShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { ExternalOpenerEntry, ExternalOpenerSet, IExternalOpenerProvider, IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService'; import { ExtHostContext, ExtHostUriOpenersShape, IExtHostContext, MainContext, MainThreadUriOpenersShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExternalOpenerEntry, IExternalOpenerProvider, IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { extHostNamedCustomer } from '../common/extHostCustomers'; import { extHostNamedCustomer } from '../common/extHostCustomers';
interface RegisteredOpenerMetadata { interface RegisteredOpenerMetadata {
readonly schemes: ReadonlySet<string>; readonly schemes: ReadonlySet<string>;
readonly extensionId: ExtensionIdentifier;
readonly label: string;
} }
@extHostNamedCustomer(MainContext.MainThreadUriOpeners) @extHostNamedCustomer(MainContext.MainThreadUriOpeners)
...@@ -33,12 +36,12 @@ export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpe ...@@ -33,12 +36,12 @@ export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpe
this._register(this.externalUriOpenerService.registerExternalOpenerProvider(this)); this._register(this.externalUriOpenerService.registerExternalOpenerProvider(this));
} }
public async provideExternalOpeners(href: string | URI): Promise<ExternalOpenerSet | undefined> { public async provideExternalOpeners(href: string | URI): Promise<readonly ExternalOpenerEntry[]> {
const targetUri = typeof href === 'string' ? URI.parse(href) : href; const targetUri = typeof href === 'string' ? URI.parse(href) : href;
// Currently we only allow openers for http and https urls // Currently we only allow openers for http and https urls
if (targetUri.scheme !== Schemas.http && targetUri.scheme !== Schemas.https) { if (targetUri.scheme !== Schemas.http && targetUri.scheme !== Schemas.https) {
return undefined; return [];
} }
await this.extensionService.activateByEvent(`onUriOpen:${targetUri.scheme}`); await this.extensionService.activateByEvent(`onUriOpen:${targetUri.scheme}`);
...@@ -46,41 +49,38 @@ export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpe ...@@ -46,41 +49,38 @@ export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpe
// If there are no handlers there is no point in making a round trip // If there are no handlers there is no point in making a round trip
const hasHandler = Array.from(this.registeredOpeners.values()).some(x => x.schemes.has(targetUri.scheme)); const hasHandler = Array.from(this.registeredOpeners.values()).some(x => x.schemes.has(targetUri.scheme));
if (!hasHandler) { if (!hasHandler) {
return undefined; return [];
} }
const { openers, cacheId } = await this.proxy.$getOpenersForUri(targetUri, CancellationToken.None); const openerHandles = await this.proxy.$getOpenersForUri(targetUri, CancellationToken.None);
if (openers.length === 0) { return openerHandles.map(handle => this.openerForCommand(handle, targetUri));
this.proxy.$releaseOpener(cacheId);
return undefined;
} else {
return {
openers: openers.map(opener => this.openerForCommand(cacheId, opener)),
dispose: () => {
this.proxy.$releaseOpener(cacheId);
}
};
}
} }
private openerForCommand( private openerForCommand(openerHandle: number, sourceUri: URI): ExternalOpenerEntry {
cacheId: number, const metadata = this.registeredOpeners.get(openerHandle)!;
opener: ExtHostUriOpener
): ExternalOpenerEntry {
return { return {
id: opener.extensionId.value, id: metadata.extensionId.value,
label: opener.title, label: metadata.label,
openExternal: async (href) => { openExternal: async (href) => {
const targetUri = URI.parse(href); const resolveUri = URI.parse(href);
await this.proxy.$openUri([cacheId, opener.commandId], targetUri); await this.proxy.$openUri(openerHandle, { resolveUri, sourceUri }, CancellationToken.None);
return true; return true;
}, },
}; };
} }
async $registerUriOpener(handle: number, schemes: readonly string[]): Promise<void> { async $registerUriOpener(
this.registeredOpeners.set(handle, { schemes: new Set(schemes) }); handle: number,
schemes: readonly string[],
extensionId: ExtensionIdentifier,
label: string,
): Promise<void> {
this.registeredOpeners.set(handle, {
schemes: new Set(schemes),
label,
extensionId,
});
} }
async $unregisterUriOpener(handle: number): Promise<void> { async $unregisterUriOpener(handle: number): Promise<void> {
......
...@@ -158,7 +158,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ...@@ -158,7 +158,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels));
const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews));
const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, extHostDocumentsAndEditors, extHostWorkspace)); const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, extHostDocumentsAndEditors, extHostWorkspace));
const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol, extHostCommands)); const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol));
// Check that no named customers are missing // Check that no named customers are missing
const expected: ProxyIdentifier<any>[] = values(ExtHostContext); const expected: ProxyIdentifier<any>[] = values(ExtHostContext);
...@@ -672,9 +672,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ...@@ -672,9 +672,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension); checkProposedApiEnabled(extension);
return extHostNotebook.showNotebookDocument(document, options); return extHostNotebook.showNotebookDocument(document, options);
}, },
registerExternalUriOpener(schemes: readonly string[], opener: vscode.ExternalUriOpener) { registerExternalUriOpener(schemes: readonly string[], opener: vscode.ExternalUriOpener, metadata: vscode.ExternalUriOpenerMetadata) {
checkProposedApiEnabled(extension); checkProposedApiEnabled(extension);
return extHostUriOpeners.registerUriOpener(extension.identifier, schemes, opener); return extHostUriOpeners.registerUriOpener(extension.identifier, schemes, opener, metadata);
}, },
}; };
......
...@@ -801,20 +801,13 @@ export interface ExtHostUrlsShape { ...@@ -801,20 +801,13 @@ export interface ExtHostUrlsShape {
} }
export interface MainThreadUriOpenersShape extends IDisposable { export interface MainThreadUriOpenersShape extends IDisposable {
$registerUriOpener(handle: number, schemes: readonly string[], extensionId: ExtensionIdentifier): Promise<void>; $registerUriOpener(handle: number, schemes: readonly string[], extensionId: ExtensionIdentifier, label: string): Promise<void>;
$unregisterUriOpener(handle: number): Promise<void>; $unregisterUriOpener(handle: number): Promise<void>;
} }
export interface ExtHostUriOpener {
readonly extensionId: ExtensionIdentifier;
readonly commandId: number;
readonly title: string;
}
export interface ExtHostUriOpenersShape { export interface ExtHostUriOpenersShape {
$getOpenersForUri(uri: UriComponents, token: CancellationToken): Promise<{ cacheId: number, openers: ReadonlyArray<ExtHostUriOpener> }>; $getOpenersForUri(uri: UriComponents, token: CancellationToken): Promise<readonly number[]>;
$openUri(id: ChainedCacheId, uri: UriComponents): Promise<void>; $openUri(handle: number, context: { resolveUri: UriComponents, sourceUri: UriComponents }, token: CancellationToken): Promise<void>;
$releaseOpener(cacheId: number): void;
} }
export interface ITextSearchComplete { export interface ITextSearchComplete {
......
...@@ -8,15 +8,14 @@ import { CancellationToken } from 'vs/base/common/cancellation'; ...@@ -8,15 +8,14 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { toDisposable } from 'vs/base/common/lifecycle'; import { toDisposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri'; import { URI, UriComponents } from 'vs/base/common/uri';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
import type * as vscode from 'vscode'; import type * as vscode from 'vscode';
import { Cache } from './cache'; import { ExtHostUriOpenersShape, IMainContext, MainContext, MainThreadUriOpenersShape } from './extHost.protocol';
import { ChainedCacheId, ExtHostUriOpener, ExtHostUriOpenersShape, IMainContext, MainContext, MainThreadUriOpenersShape } from './extHost.protocol';
interface OpenerEntry { interface OpenerEntry {
readonly extension: ExtensionIdentifier; readonly extension: ExtensionIdentifier;
readonly schemes: ReadonlySet<string>; readonly schemes: ReadonlySet<string>;
readonly opener: vscode.ExternalUriOpener; readonly opener: vscode.ExternalUriOpener;
readonly metadata: vscode.ExternalUriOpenerMetadata;
} }
export class ExtHostUriOpeners implements ExtHostUriOpenersShape { export class ExtHostUriOpeners implements ExtHostUriOpenersShape {
...@@ -24,23 +23,20 @@ export class ExtHostUriOpeners implements ExtHostUriOpenersShape { ...@@ -24,23 +23,20 @@ export class ExtHostUriOpeners implements ExtHostUriOpenersShape {
private static HandlePool = 0; private static HandlePool = 0;
private readonly _proxy: MainThreadUriOpenersShape; private readonly _proxy: MainThreadUriOpenersShape;
private readonly _commands: ExtHostCommands;
private readonly _cache = new Cache<{ command: vscode.Command }>('CodeAction');
private readonly _openers = new Map<number, OpenerEntry>(); private readonly _openers = new Map<number, OpenerEntry>();
constructor( constructor(
mainContext: IMainContext, mainContext: IMainContext,
commands: ExtHostCommands,
) { ) {
this._proxy = mainContext.getProxy(MainContext.MainThreadUriOpeners); this._proxy = mainContext.getProxy(MainContext.MainThreadUriOpeners);
this._commands = commands;
} }
registerUriOpener( registerUriOpener(
extensionId: ExtensionIdentifier, extensionId: ExtensionIdentifier,
schemes: readonly string[], schemes: readonly string[],
opener: vscode.ExternalUriOpener, opener: vscode.ExternalUriOpener,
metadata: vscode.ExternalUriOpenerMetadata,
): vscode.Disposable { ): vscode.Disposable {
const handle = ExtHostUriOpeners.HandlePool++; const handle = ExtHostUriOpeners.HandlePool++;
...@@ -48,8 +44,9 @@ export class ExtHostUriOpeners implements ExtHostUriOpenersShape { ...@@ -48,8 +44,9 @@ export class ExtHostUriOpeners implements ExtHostUriOpenersShape {
opener, opener,
extension: extensionId, extension: extensionId,
schemes: new Set(schemes), schemes: new Set(schemes),
metadata
}); });
this._proxy.$registerUriOpener(handle, schemes, extensionId); this._proxy.$registerUriOpener(handle, schemes, extensionId, metadata.label);
return toDisposable(() => { return toDisposable(() => {
this._openers.delete(handle); this._openers.delete(handle);
...@@ -57,47 +54,35 @@ export class ExtHostUriOpeners implements ExtHostUriOpenersShape { ...@@ -57,47 +54,35 @@ export class ExtHostUriOpeners implements ExtHostUriOpenersShape {
}); });
} }
async $getOpenersForUri(uriComponents: UriComponents, token: CancellationToken): Promise<{ cacheId: number, openers: Array<ExtHostUriOpener> }> { async $getOpenersForUri(uriComponents: UriComponents, token: CancellationToken): Promise<readonly number[]> {
const uri = URI.revive(uriComponents); const uri = URI.revive(uriComponents);
const promises = Array.from(this._openers.values()).map(async ({ schemes, opener, extension }): Promise<{ command: vscode.Command, extension: ExtensionIdentifier } | undefined> => { const promises = Array.from(this._openers.entries()).map(async ([handle, { schemes, opener, }]): Promise<number | undefined> => {
if (!schemes.has(uri.scheme)) { if (!schemes.has(uri.scheme)) {
return undefined; return undefined;
} }
try { try {
const result = await opener.openExternalUri(uri, {}, token); if (await opener.canOpenExternalUri(uri, token)) {
return handle;
if (result) {
return {
command: result,
extension
};
} }
} catch (e) { } catch (e) {
console.log(e);
// noop // noop
} }
return undefined; return undefined;
}); });
const commands = coalesce(await Promise.all(promises)); return (await Promise.all(promises)).filter(handle => typeof handle === 'number') as number[];
const cacheId = this._cache.add(commands);
return {
cacheId,
openers: commands.map((entry, i) => ({ title: entry.command.title, commandId: i, extensionId: entry.extension })),
};
} }
async $openUri(id: ChainedCacheId, uri: UriComponents): Promise<void> { async $openUri(handle: number, context: { resolveUri: UriComponents, sourceUri: UriComponents }, token: CancellationToken): Promise<void> {
const entry = this._cache.get(id[0], id[1]); const entry = this._openers.get(handle);
if (!entry) { if (!entry) {
return; throw new Error(`Unknown opener handle: '${handle}'`);
} }
return entry.opener.openExternalUri(URI.revive(context.resolveUri), {
return this._commands.executeCommand(entry.command.command, URI.revive(uri), ...(entry.command.arguments || [])); sourceUri: URI.revive(context.sourceUri)
} }, token);
$releaseOpener(cacheId: number): void {
this._cache.delete(cacheId);
} }
} }
...@@ -3,16 +3,16 @@ ...@@ -3,16 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { LinkedList } from 'vs/base/common/linkedList'; import { LinkedList } from 'vs/base/common/linkedList';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExternalOpener, IOpenerService } from 'vs/platform/opener/common/opener'; import { IExternalOpener, IOpenerService } from 'vs/platform/opener/common/opener';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import * as nls from 'vs/nls';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ExternalUriOpenerConfiguration, externalUriOpenersSettingId } from 'vs/workbench/contrib/externalUriOpener/common/configuration'; import { ExternalUriOpenerConfiguration, externalUriOpenersSettingId } from 'vs/workbench/contrib/externalUriOpener/common/configuration';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
export const IExternalUriOpenerService = createDecorator<IExternalUriOpenerService>('externalUriOpenerService'); export const IExternalUriOpenerService = createDecorator<IExternalUriOpenerService>('externalUriOpenerService');
...@@ -21,13 +21,10 @@ export interface ExternalOpenerEntry extends IExternalOpener { ...@@ -21,13 +21,10 @@ export interface ExternalOpenerEntry extends IExternalOpener {
readonly label: string; readonly label: string;
} }
export interface ExternalOpenerSet {
readonly openers: readonly ExternalOpenerEntry[];
dispose(): void;
}
export interface IExternalOpenerProvider { export interface IExternalOpenerProvider {
provideExternalOpeners(resource: URI | string): Promise<ExternalOpenerSet | undefined>; provideExternalOpeners(resource: URI | string): Promise<readonly ExternalOpenerEntry[]>;
} }
export interface IExternalUriOpenerService { export interface IExternalUriOpenerService {
...@@ -64,71 +61,62 @@ export class ExternalUriOpenerService extends Disposable implements IExternalUri ...@@ -64,71 +61,62 @@ export class ExternalUriOpenerService extends Disposable implements IExternalUri
const targetUri = typeof href === 'string' ? URI.parse(href) : href; const targetUri = typeof href === 'string' ? URI.parse(href) : href;
const toDispose = new DisposableStore();
const openers: ExternalOpenerEntry[] = []; const openers: ExternalOpenerEntry[] = [];
for (const provider of this._externalOpenerProviders) { for (const provider of this._externalOpenerProviders) {
const set = await provider.provideExternalOpeners(targetUri); openers.push(...(await provider.provideExternalOpeners(targetUri)));
if (set) {
toDispose.add(set);
openers.push(...set.openers);
}
} }
try { if (openers.length === 0) {
if (openers.length === 0) { return false;
return false; }
}
const url = new URL(targetUri.toString()); const authority = targetUri.authority;
const config = this.configurationService.getValue<readonly ExternalUriOpenerConfiguration[]>(externalUriOpenersSettingId) || []; const config = this.configurationService.getValue<readonly ExternalUriOpenerConfiguration[]>(externalUriOpenersSettingId) || [];
for (const entry of config) { for (const entry of config) {
if (entry.hostname === url.hostname) { if (entry.hostname === authority) {
const opener = openers.find(opener => opener.id === entry.id); const opener = openers.find(opener => opener.id === entry.id);
if (opener) { if (opener) {
return opener.openExternal(href); return opener.openExternal(href);
}
} }
} }
}
type PickItem = IQuickPickItem & { opener?: IExternalOpener | 'configureDefault' }; type PickItem = IQuickPickItem & { opener?: IExternalOpener | 'configureDefault' };
const items: Array<PickItem | IQuickPickSeparator> = openers.map((opener, i): PickItem => { const items: Array<PickItem | IQuickPickSeparator> = openers.map((opener, i): PickItem => {
return { return {
label: opener.label, label: opener.label,
opener: opener opener: opener
}; };
}); });
items.push( items.push(
{ {
label: 'Default', label: 'Default',
opener: undefined opener: undefined
}, },
{ type: 'separator' }, { type: 'separator' },
{ {
label: nls.localize('selectOpenerConfigureTitle', "Configure default opener..."), label: nls.localize('selectOpenerConfigureTitle', "Configure default opener..."),
opener: 'configureDefault' opener: 'configureDefault'
});
const picked = await this.quickInputService.pick(items, {
placeHolder: nls.localize('selectOpenerPlaceHolder', "Select opener for {0}", targetUri.toString())
}); });
if (!picked) { const picked = await this.quickInputService.pick(items, {
// Still cancel the default opener here since we prompted the user placeHolder: nls.localize('selectOpenerPlaceHolder', "Select opener for {0}", targetUri.toString())
return true; });
}
if (typeof picked.opener === 'undefined') { if (!picked) {
return true; // Still cancel the default opener here since we prompted the user
} else if (picked.opener === 'configureDefault') { return true;
await this.preferencesService.openGlobalSettings(true, { }
revealSetting: { key: externalUriOpenersSettingId, edit: true }
}); if (typeof picked.opener === 'undefined') {
return true; return true;
} else { } else if (picked.opener === 'configureDefault') {
return picked.opener.openExternal(href); await this.preferencesService.openGlobalSettings(true, {
} revealSetting: { key: externalUriOpenersSettingId, edit: true }
} finally { });
toDispose.dispose(); return true;
} else {
return picked.opener.openExternal(href);
} }
} }
} }
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册