Add new external uri opener service

This workbench service handles opening external uris. Unlike the core OpenerService, it also has logic for configuring a default opener and prompting if multiple openers are availble for a given uri
This commit is contained in:
Matt Bierner
2021-01-11 19:40:43 -08:00
parent 60e46eb875
commit 0227681492
10 changed files with 277 additions and 67 deletions

View File

@@ -7,30 +7,33 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { IExternalOpener, IExternalOpenerProvider, IOpenerService } from 'vs/platform/opener/common/opener';
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { ExtHostContext, ExtHostUriOpenersShape, IExtHostContext, MainContext, MainThreadUriOpenersShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostContext, ExtHostUriOpener, ExtHostUriOpenersShape, IExtHostContext, MainContext, MainThreadUriOpenersShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExternalOpenerEntry, ExternalOpenerSet, IExternalOpenerProvider, IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { extHostNamedCustomer } from '../common/extHostCustomers';
interface RegisteredOpenerMetadata {
readonly schemes: ReadonlySet<string>;
}
@extHostNamedCustomer(MainContext.MainThreadUriOpeners)
export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpenersShape, IExternalOpenerProvider {
private readonly proxy: ExtHostUriOpenersShape;
private readonly handlers = new Map<number, { schemes: ReadonlySet<string> }>();
private readonly registeredOpeners = new Map<number, RegisteredOpenerMetadata>();
constructor(
context: IExtHostContext,
@IOpenerService private readonly openerService: IOpenerService,
@IExternalUriOpenerService private readonly externalUriOpenerService: IExternalUriOpenerService,
@IExtensionService private readonly extensionService: IExtensionService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
) {
super();
this.proxy = context.getProxy(ExtHostContext.ExtHostUriOpeners);
this._register(this.openerService.registerExternalOpenerProvider(this));
this._register(this.externalUriOpenerService.registerExternalOpenerProvider(this));
}
public async provideExternalOpener(href: string | URI): Promise<IExternalOpener | undefined> {
public async provideExternalOpeners(href: string | URI): Promise<ExternalOpenerSet | undefined> {
const targetUri = typeof href === 'string' ? URI.parse(href) : href;
// Currently we only allow openers for http and https urls
@@ -41,61 +44,51 @@ export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpe
await this.extensionService.activateByEvent(`onUriOpen:${targetUri.scheme}`);
// If there are no handlers there is no point in making a round trip
const hasHandler = Array.from(this.handlers.values()).some(x => x.schemes.has(targetUri.scheme));
const hasHandler = Array.from(this.registeredOpeners.values()).some(x => x.schemes.has(targetUri.scheme));
if (!hasHandler) {
return undefined;
}
const { openers, cacheId } = await this.proxy.$getOpenersForUri(targetUri, CancellationToken.None);
if (openers.length === 0) {
return undefined;
} else if (openers.length === 1) {
return this.openerForCommand(cacheId, openers[0].id);
} else {
type PickItem = IQuickPickItem & { index: number };
const items = openers.map((opener, i): PickItem => {
return {
label: opener.title,
index: i
};
});
const picked = await this.quickInputService.pick(items, {});
if (picked) {
const opener = openers[(picked as PickItem).index];
return this.openerForCommand(cacheId, opener.id);
}
this.proxy.$releaseOpener(cacheId);
return undefined;
} else {
return {
openers: openers.map(opener => this.openerForCommand(cacheId, opener)),
dispose: () => {
this.proxy.$releaseOpener(cacheId);
}
};
}
}
private openerForCommand(cacheId: number, commandId: number): IExternalOpener {
private openerForCommand(
cacheId: number,
opener: ExtHostUriOpener
): ExternalOpenerEntry {
return {
id: opener.extensionId.value,
label: opener.title,
openExternal: async (href) => {
const targetUri = URI.parse(href);
try {
await this.proxy.$openUri([cacheId, commandId], targetUri);
} finally {
this.proxy.$releaseOpener(cacheId);
}
await this.proxy.$openUri([cacheId, opener.commandId], targetUri);
return true;
}
},
};
}
async $registerUriOpener(handle: number, schemes: readonly string[]): Promise<void> {
this.handlers.set(handle, { schemes: new Set(schemes) });
this.registeredOpeners.set(handle, { schemes: new Set(schemes) });
}
async $unregisterUriOpener(handle: number): Promise<void> {
this.handlers.delete(handle);
this.registeredOpeners.delete(handle);
}
dispose(): void {
super.dispose();
this.handlers.clear();
this.registeredOpeners.clear();
}
}