提交 df85a072 编写于 作者: M Matt Bierner 提交者: Ladislau Szomoru

Continue work on url opener api

For #109277

- Add `option` opener priority. This means the oper will only be shown if requested but will not replace the default opener
- Persist registered openers for IntelliSense suggestions
上级 e9e29a4f
# Simple Browser files
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
a $a + b = c$ b
a $\pm\sqrt{a^2 + b^2}$ b
......@@ -39,12 +39,6 @@
"default": true,
"title": "Focus Lock Indicator Enabled",
"description": "%configuration.focusLockIndicator.enabled.description%"
},
"simpleBrowser.opener.enabled": {
"type": "boolean",
"default": false,
"title": "Opener Enabled",
"description": "%configuration.opener.enabled.description%"
}
}
}
......
......@@ -2,6 +2,5 @@
"displayName": "Simple Browser",
"description": "A very basic built-in webview for displaying web content.",
"configuration.focusLockIndicator.enabled.description": "Enable/disable ",
"configuration.opener.enabled.description": "(Experimental) Enables using the built-in simple browser when opening http and https urls.",
"configuration.opener.enabledHosts.description": "Lists of hosts to open in the simple browser. For example: `localhost`."
"configuration.opener.enabled.description": "(Experimental) Enables using the built-in simple browser when opening http and https urls."
}
......@@ -45,27 +45,26 @@ export function activate(context: vscode.ExtensionContext) {
manager.show(url.toString(), showOptions);
}));
context.subscriptions.push(vscode.window.registerExternalUriOpener(['http', 'https'], {
context.subscriptions.push(vscode.window.registerExternalUriOpener(openerId, ['http', 'https'], {
canOpenExternalUri(uri: vscode.Uri) {
const configuration = vscode.workspace.getConfiguration('simpleBrowser');
if (!configuration.get('opener.enabled', false)) {
return vscode.ExternalUriOpenerEnablement.Disabled;
return vscode.ExternalUriOpenerPriority.None;
}
const originalUri = new URL(uri.toString());
if (enabledHosts.has(originalUri.hostname)) {
return isWeb()
? vscode.ExternalUriOpenerEnablement.Preferred
: vscode.ExternalUriOpenerEnablement.Enabled;
? vscode.ExternalUriOpenerPriority.Preferred
: vscode.ExternalUriOpenerPriority.Option;
}
return vscode.ExternalUriOpenerEnablement.Disabled;
return vscode.ExternalUriOpenerPriority.None;
},
openExternalUri(resolveUri: vscode.Uri) {
return manager.show(resolveUri.toString());
}
}, {
id: openerId,
label: localize('openTitle', "Open in simple browser"),
}));
}
......
......@@ -1881,8 +1881,9 @@ export const TokenizationRegistry = new TokenizationRegistryImpl();
/**
* @internal
*/
export enum ExternalUriOpenerEnablement {
Disabled,
Enabled,
Preferred
export enum ExternalUriOpenerPriority {
None = 0,
Option = 1,
Default = 2,
Preferred = 3,
}
......@@ -2294,21 +2294,39 @@ declare module 'vscode' {
//#region Opener service (https://github.com/microsoft/vscode/issues/109277)
export enum ExternalUriOpenerEnablement {
export enum ExternalUriOpenerPriority {
/**
* The opener cannot handle the uri.
* The opener is disabled and will not be shown to users.
*
* Note that the opener can still be used if the user
* specifically configures it in their settings.
*/
Disabled = 0,
None = 0,
/**
* The opener can handle the uri.
* The opener can open the uri but will not be shown by default when a
* user clicks on the uri.
*
* If only optional openers are enabled, then VS Code's default opener
* will be automatically used.
*/
Enabled = 1,
Option = 1,
/**
* The opener can handle the uri and should be automatically selected if possible.
* The opener can open the uri.
*
* When the user clicks on a uri, they will be prompted to select the opener
* they wish to use for it.
*/
Preferred = 2
Default = 2,
/**
* The opener can open the uri and should be automatically selected if possible.
*
* Preferred openers will be automatically selected if no other preferred openers
* are available.
*/
Preferred = 3,
}
/**
......@@ -2330,7 +2348,7 @@ declare module 'vscode' {
*
* @return If the opener can open the external uri.
*/
canOpenExternalUri(uri: Uri, token: CancellationToken): ProviderResult<ExternalUriOpenerEnablement>;
canOpenExternalUri(uri: Uri, token: CancellationToken): ExternalUriOpenerPriority | Thenable<ExternalUriOpenerPriority>;
/**
* Open the given uri.
......@@ -2368,12 +2386,6 @@ declare module 'vscode' {
* Additional metadata about the registered opener.
*/
interface ExternalUriOpenerMetadata {
/**
* Unique id of the opener, such as `myExtension.browserPreview`
*
* This is used in settings and commands to identifier the opener.
*/
readonly id: string;
/**
* Text displayed to the user that explains what the opener does.
......@@ -2389,13 +2401,16 @@ declare module 'vscode' {
*
* When a uri is about to be opened, an `onUriOpen:SCHEME` activation event is fired.
*
* @param id Unique id of the opener, such as `myExtension.browserPreview`. This is used in settings
* and commands to identify the opener.
* @param schemes List of uri schemes the opener is triggered for. Currently only `http`
* and `https` are supported.
* @param opener Opener to register.
* @param metadata Additional information about the opener.
*
* @returns Disposable that unregisters the opener.
*/
export function registerExternalUriOpener(schemes: readonly string[], opener: ExternalUriOpener, metadata: ExternalUriOpenerMetadata): Disposable;
export function registerExternalUriOpener(id: string, schemes: readonly string[], opener: ExternalUriOpener, metadata: ExternalUriOpenerMetadata): Disposable;
}
//#endregion
......
......@@ -10,8 +10,9 @@ import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ExtHostContext, ExtHostUriOpenersShape, IExtHostContext, MainContext, MainThreadUriOpenersShape } from 'vs/workbench/api/common/extHost.protocol';
import { externalUriOpenerIdSchemaAddition } from 'vs/workbench/contrib/externalUriOpener/common/configuration';
import { ContributedExternalUriOpenersStore } from 'vs/workbench/contrib/externalUriOpener/common/contributedOpeners';
import { IExternalOpenerProvider, IExternalUriOpener, IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { extHostNamedCustomer } from '../common/extHostCustomers';
......@@ -27,9 +28,11 @@ export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpe
private readonly proxy: ExtHostUriOpenersShape;
private readonly _registeredOpeners = new Map<string, RegisteredOpenerMetadata>();
private readonly _contributedExternalUriOpenersStore: ContributedExternalUriOpenersStore;
constructor(
context: IExtHostContext,
@IStorageService storageService: IStorageService,
@IExternalUriOpenerService externalUriOpenerService: IExternalUriOpenerService,
@IExtensionService private readonly extensionService: IExtensionService,
@INotificationService private readonly notificationService: INotificationService,
......@@ -38,6 +41,8 @@ export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpe
this.proxy = context.getProxy(ExtHostContext.ExtHostUriOpeners);
this._register(externalUriOpenerService.registerExternalOpenerProvider(this));
this._contributedExternalUriOpenersStore = this._register(new ContributedExternalUriOpenersStore(storageService, extensionService));
}
public async *getOpeners(targetUri: URI): AsyncIterable<IExternalUriOpener> {
......@@ -92,11 +97,12 @@ export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpe
extensionId,
});
externalUriOpenerIdSchemaAddition.enum?.push(id);
this._contributedExternalUriOpenersStore.add(id, extensionId.value);
}
async $unregisterUriOpener(id: string): Promise<void> {
this._registeredOpeners.delete(id);
this._contributedExternalUriOpenersStore.delete(id);
}
dispose(): void {
......
......@@ -672,9 +672,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension);
return extHostNotebook.showNotebookDocument(document, options);
},
registerExternalUriOpener(schemes: readonly string[], opener: vscode.ExternalUriOpener, metadata: vscode.ExternalUriOpenerMetadata) {
registerExternalUriOpener(id: string, schemes: readonly string[], opener: vscode.ExternalUriOpener, metadata: vscode.ExternalUriOpenerMetadata) {
checkProposedApiEnabled(extension);
return extHostUriOpeners.registerUriOpener(extension.identifier, schemes, opener, metadata);
return extHostUriOpeners.registerUriOpener(extension.identifier, id, schemes, opener, metadata);
},
};
......@@ -1133,7 +1133,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
EventEmitter: Emitter,
ExtensionKind: extHostTypes.ExtensionKind,
ExtensionMode: extHostTypes.ExtensionMode,
ExternalUriOpenerEnablement: extHostTypes.ExternalUriOpenerEnablement,
ExternalUriOpenerPriority: extHostTypes.ExternalUriOpenerPriority,
FileChangeType: extHostTypes.FileChangeType,
FileDecoration: extHostTypes.FileDecoration,
FileSystemError: extHostTypes.FileSystemError,
......
......@@ -808,7 +808,7 @@ export interface MainThreadUriOpenersShape extends IDisposable {
}
export interface ExtHostUriOpenersShape {
$canOpenUri(id: string, uri: UriComponents, token: CancellationToken): Promise<modes.ExternalUriOpenerEnablement>;
$canOpenUri(id: string, uri: UriComponents, token: CancellationToken): Promise<modes.ExternalUriOpenerPriority>;
$openUri(id: string, context: { resolvedUri: UriComponents, sourceUri: UriComponents }, token: CancellationToken): Promise<void>;
}
......
......@@ -2978,8 +2978,9 @@ export type RequiredTestItem = {
//#endregion
export enum ExternalUriOpenerEnablement {
Disabled = 0,
Enabled = 1,
Preferred = 2
export enum ExternalUriOpenerPriority {
None = 0,
Option = 1,
Default = 2,
Preferred = 3,
}
......@@ -32,11 +32,11 @@ export class ExtHostUriOpeners implements ExtHostUriOpenersShape {
registerUriOpener(
extensionId: ExtensionIdentifier,
id: string,
schemes: readonly string[],
opener: vscode.ExternalUriOpener,
metadata: vscode.ExternalUriOpenerMetadata,
): vscode.Disposable {
const id = metadata.id;
if (this._openers.has(id)) {
throw new Error(`Opener with id already registered: '${id}'`);
}
......@@ -55,15 +55,14 @@ export class ExtHostUriOpeners implements ExtHostUriOpenersShape {
});
}
async $canOpenUri(id: string, uriComponents: UriComponents, token: CancellationToken): Promise<modes.ExternalUriOpenerEnablement> {
async $canOpenUri(id: string, uriComponents: UriComponents, token: CancellationToken): Promise<modes.ExternalUriOpenerPriority> {
const entry = this._openers.get(id);
if (!entry) {
throw new Error(`Unknown opener with id: ${id}`);
}
const uri = URI.revive(uriComponents);
const result = await entry.opener.canOpenExternalUri(uri, token);
return result ? result : modes.ExternalUriOpenerEnablement.Disabled;
return entry.opener.canOpenExternalUri(uri, token);
}
async $openUri(id: string, context: { resolvedUri: UriComponents, sourceUri: UriComponents }, token: CancellationToken): Promise<void> {
......
......@@ -3,10 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
import { IConfigurationNode, IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
import * as nls from 'vs/nls';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Registry } from 'vs/platform/registry/common/platform';
export const externalUriOpenersSettingId = 'workbench.externalUriOpeners';
......@@ -15,7 +16,7 @@ export interface ExternalUriOpenerConfiguration {
readonly id: string;
}
export const externalUriOpenerIdSchemaAddition: IJSONSchema = {
const externalUriOpenerIdSchemaAddition: IJSONSchema = {
type: 'string',
enum: []
};
......@@ -66,3 +67,11 @@ export const externalUriOpenersConfigurationNode: IConfigurationNode = {
}
}
};
export function updateContributedOpeners(enumValues: string[], enumDescriptions: string[]): void {
externalUriOpenerIdSchemaAddition.enum = enumValues;
externalUriOpenerIdSchemaAddition.enumDescriptions = enumDescriptions;
Registry.as<IConfigurationRegistry>(Extensions.Configuration)
.notifyConfigurationSchemaUpdated(externalUriOpenersConfigurationNode);
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { Memento } from 'vs/workbench/common/memento';
import { updateContributedOpeners } from 'vs/workbench/contrib/externalUriOpener/common/configuration';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
export const defaultExternalUriOpenerId = 'default';
interface RegisteredExternalOpener {
readonly extensionId: string;
}
interface OpenersMemento {
[id: string]: RegisteredExternalOpener;
}
/**
*/
export class ContributedExternalUriOpenersStore extends Disposable {
private static readonly STORAGE_ID = 'externalUriOpeners';
private readonly _openers = new Map<string, RegisteredExternalOpener>();
private readonly _memento: Memento;
private _mementoObject: OpenersMemento;
constructor(
@IStorageService storageService: IStorageService,
@IExtensionService private readonly _extensionService: IExtensionService
) {
super();
this._memento = new Memento(ContributedExternalUriOpenersStore.STORAGE_ID, storageService);
this._mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE);
for (const id of Object.keys(this._mementoObject || {})) {
this.add(id, this._mementoObject[id].extensionId);
}
this.invalidateOpenersForUninstalledExtension();
this._register(this._extensionService.onDidChangeExtensions(() => this.invalidateOpenersForUninstalledExtension()));
}
public add(id: string, extensionId: string): void {
this._openers.set(id, { extensionId });
this._mementoObject[id] = { extensionId };
this._memento.saveMemento();
this.updateSchema();
}
public delete(id: string): void {
this._openers.delete(id);
delete this._mementoObject[id];
this._memento.saveMemento();
this.updateSchema();
}
private async invalidateOpenersForUninstalledExtension() {
const registeredExtensions = await this._extensionService.getExtensions();
for (const [id, entry] of this._openers) {
const isExtensionRegistered = registeredExtensions.some(r => r.identifier.value === entry.extensionId);
if (!isExtensionRegistered) {
this.delete(id);
}
}
}
private updateSchema() {
const ids: string[] = [];
const descriptions: string[] = [];
for (const [id, entry] of this._openers) {
ids.push(id);
descriptions.push(entry.extensionId);
}
updateContributedOpeners(ids, descriptions);
}
}
......@@ -15,6 +15,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExternalOpener, IOpenerService } from 'vs/platform/opener/common/opener';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ExternalUriOpenerConfiguration, externalUriOpenersSettingId } from 'vs/workbench/contrib/externalUriOpener/common/configuration';
import { testUrlMatchesGlob } from 'vs/workbench/contrib/url/common/urlGlob';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
......@@ -30,7 +31,7 @@ export interface IExternalUriOpener {
readonly id: string;
readonly label: string;
canOpen(uri: URI, token: CancellationToken): Promise<modes.ExternalUriOpenerEnablement>;
canOpen(uri: URI, token: CancellationToken): Promise<modes.ExternalUriOpenerPriority>;
openExternalUri(uri: URI, ctx: { sourceUri: URI }, token: CancellationToken): Promise<boolean>;
}
......@@ -51,6 +52,7 @@ export class ExternalUriOpenerService extends Disposable implements IExternalUri
constructor(
@IOpenerService openerService: IOpenerService,
@IStorageService storageService: IStorageService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IPreferencesService private readonly preferencesService: IPreferencesService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
......@@ -86,16 +88,16 @@ export class ExternalUriOpenerService extends Disposable implements IExternalUri
}
// Then check to see if there is a valid opener
const validOpeners: Array<{ opener: IExternalUriOpener, preferred: boolean }> = [];
const validOpeners: Array<{ opener: IExternalUriOpener, priority: modes.ExternalUriOpenerPriority }> = [];
await Promise.all(Array.from(allOpeners.values()).map(async opener => {
switch (await opener.canOpen(targetUri, token)) {
case modes.ExternalUriOpenerEnablement.Enabled:
validOpeners.push({ opener, preferred: false });
const priority = await opener.canOpen(targetUri, token);
switch (priority) {
case modes.ExternalUriOpenerPriority.Option:
case modes.ExternalUriOpenerPriority.Default:
case modes.ExternalUriOpenerPriority.Preferred:
validOpeners.push({ opener, priority });
break;
case modes.ExternalUriOpenerEnablement.Preferred:
validOpeners.push({ opener, preferred: true });
break;
}
}));
if (validOpeners.length === 0) {
......@@ -103,13 +105,18 @@ export class ExternalUriOpenerService extends Disposable implements IExternalUri
}
// See if we have a preferred opener first
const preferred = firstOrDefault(validOpeners.filter(x => x.preferred));
const preferred = firstOrDefault(validOpeners.filter(x => x.priority === modes.ExternalUriOpenerPriority.Preferred));
if (preferred) {
return preferred.opener.openExternalUri(targetUri, ctx, token);
}
// See if we only have optional openers, use the default opener
if (validOpeners.every(x => x.priority === modes.ExternalUriOpenerPriority.Option)) {
return false;
}
// Otherwise prompt
return this.showOpenerPrompt(validOpeners, targetUri, ctx, token);
return this.showOpenerPrompt(validOpeners.map(x => x.opener), targetUri, ctx, token);
}
private getConfiguredOpenerForUri(openers: Map<string, IExternalUriOpener>, targetUri: URI): IExternalUriOpener | undefined {
......@@ -127,17 +134,17 @@ export class ExternalUriOpenerService extends Disposable implements IExternalUri
}
private async showOpenerPrompt(
openers: ReadonlyArray<{ opener: IExternalUriOpener, preferred: boolean }>,
openers: ReadonlyArray<IExternalUriOpener>,
targetUri: URI,
ctx: { sourceUri: URI },
token: CancellationToken
): Promise<boolean> {
type PickItem = IQuickPickItem & { opener?: IExternalUriOpener | 'configureDefault' };
const items: Array<PickItem | IQuickPickSeparator> = openers.map((entry): PickItem => {
const items: Array<PickItem | IQuickPickSeparator> = openers.map((opener): PickItem => {
return {
label: entry.opener.label,
opener: entry.opener
label: opener.label,
opener: opener
};
});
items.push(
......
......@@ -7,7 +7,7 @@ import * as assert from 'assert';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ExternalUriOpenerEnablement } from 'vs/editor/common/modes';
import { ExternalUriOpenerPriority } from 'vs/editor/common/modes';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
......@@ -73,13 +73,13 @@ suite('ExternalUriOpenerService', () => {
yield {
id: 'disabled-id',
label: 'disabled',
canOpen: async () => ExternalUriOpenerEnablement.Disabled,
canOpen: async () => ExternalUriOpenerPriority.None,
openExternalUri: async () => true,
};
yield {
id: 'enabled-id',
label: 'enabled',
canOpen: async () => ExternalUriOpenerEnablement.Enabled,
canOpen: async () => ExternalUriOpenerPriority.Default,
openExternalUri: async () => {
openedWithEnabled = true;
return true;
......@@ -103,7 +103,7 @@ suite('ExternalUriOpenerService', () => {
yield {
id: 'other-id',
label: 'other',
canOpen: async () => ExternalUriOpenerEnablement.Enabled,
canOpen: async () => ExternalUriOpenerPriority.Default,
openExternalUri: async () => {
return true;
}
......@@ -111,7 +111,7 @@ suite('ExternalUriOpenerService', () => {
yield {
id: 'preferred-id',
label: 'preferred',
canOpen: async () => ExternalUriOpenerEnablement.Preferred,
canOpen: async () => ExternalUriOpenerPriority.Preferred,
openExternalUri: async () => {
openedWithPreferred = true;
return true;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册