Skip to content

Commit

Permalink
VSX: Add 'Install Another Version...' Command
Browse files Browse the repository at this point in the history
What It Does

- Supports the ability to install any compatible version of an user-installed extension, provided that the extension is available in the Open VSX Registry.

How To Test

1. Open the extensions view and locate the `Installed` view of all user-installed extensions.
2. Pick an extension --> Click the `manage` gear icon --> Click `Install Another Version...` command. Note that the command is disabled fo any extension not available in the Open VSX Registry.
3. Select a compatible version to install from the Quick Pick dropdown.
4. Observe that the previous version is uninstalled and replaced with the newly selected version.

Signed-off-by: seantan22 <[email protected]>
  • Loading branch information
seantan22 committed Apr 12, 2021
1 parent b722e4d commit f4074d8
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 47 deletions.
8 changes: 6 additions & 2 deletions packages/plugin-ext/src/common/plugin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export interface PluginDeployerResolver {

accept(pluginSourceId: string): boolean;

resolve(pluginResolverContext: PluginDeployerResolverContext): Promise<void>;
resolve(pluginResolverContext: PluginDeployerResolverContext, options?: PluginDeployOptions): Promise<void>;

}

Expand Down Expand Up @@ -835,6 +835,10 @@ export interface WorkspaceStorageKind {
export type GlobalStorageKind = undefined;
export type PluginStorageKind = GlobalStorageKind | WorkspaceStorageKind;

export interface PluginDeployOptions {
version: string;
}

/**
* The JSON-RPC workspace interface.
*/
Expand All @@ -847,7 +851,7 @@ export interface PluginServer {
*
* @param type whether a plugin is installed by a system or a user, defaults to a user
*/
deploy(pluginEntry: string, type?: PluginType): Promise<void>;
deploy(pluginEntry: string, type?: PluginType, options?: PluginDeployOptions): Promise<void>;

undeploy(pluginId: string): Promise<void>;

Expand Down
21 changes: 11 additions & 10 deletions packages/plugin-ext/src/main/node/plugin-deployer-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
PluginDeployerResolver, PluginDeployerFileHandler, PluginDeployerDirectoryHandler,
PluginDeployerEntry, PluginDeployer, PluginDeployerParticipant, PluginDeployerStartContext,
PluginDeployerResolverInit, PluginDeployerFileHandlerContext,
PluginDeployerDirectoryHandlerContext, PluginDeployerEntryType, PluginDeployerHandler, PluginType
PluginDeployerDirectoryHandlerContext, PluginDeployerEntryType, PluginDeployerHandler, PluginType, PluginDeployOptions
} from '../../common/plugin-protocol';
import { PluginDeployerEntryImpl } from './plugin-deployer-entry-impl';
import {
Expand Down Expand Up @@ -132,14 +132,15 @@ export class PluginDeployerImpl implements PluginDeployer {
}
}

async deploy(pluginEntry: string, type: PluginType = PluginType.System): Promise<void> {
async deploy(pluginEntry: string, type: PluginType = PluginType.System, options?: PluginDeployOptions): Promise<void> {
const startDeployTime = performance.now();
await this.deployMultipleEntries([pluginEntry], type);
await this.deployMultipleEntries([pluginEntry], type, options);
this.logMeasurement('Deploy plugin entry', startDeployTime);
}

protected async deployMultipleEntries(pluginEntries: ReadonlyArray<string>, type: PluginType = PluginType.System): Promise<void> {
const pluginsToDeploy = await this.resolvePlugins(pluginEntries, type);
protected async deployMultipleEntries(pluginEntries: ReadonlyArray<string>, type: PluginType = PluginType.System, options?: PluginDeployOptions): Promise<void> {
let pluginsToDeploy = [];
pluginsToDeploy = await this.resolvePlugins(pluginEntries, type, options);
await this.deployPlugins(pluginsToDeploy);
}

Expand All @@ -155,7 +156,7 @@ export class PluginDeployerImpl implements PluginDeployer {
* ]);
* ```
*/
async resolvePlugins(pluginEntries: ReadonlyArray<string>, type: PluginType): Promise<PluginDeployerEntry[]> {
async resolvePlugins(pluginEntries: ReadonlyArray<string>, type: PluginType, options?: PluginDeployOptions): Promise<PluginDeployerEntry[]> {
const visited = new Set<string>();
const pluginsToDeploy = new Map<string, PluginDeployerEntry>();

Expand All @@ -175,7 +176,8 @@ export class PluginDeployerImpl implements PluginDeployer {
queue = [];
await Promise.all(workload.map(async current => {
try {
const pluginDeployerEntries = await this.resolvePlugin(current, type);
let pluginDeployerEntries = [];
pluginDeployerEntries = await this.resolvePlugin(current, type, options);
await this.applyFileHandlers(pluginDeployerEntries);
await this.applyDirectoryFileHandlers(pluginDeployerEntries);
for (const deployerEntry of pluginDeployerEntries) {
Expand Down Expand Up @@ -273,16 +275,15 @@ export class PluginDeployerImpl implements PluginDeployer {
/**
* Check a plugin ID see if there are some resolvers that can handle it. If there is a matching resolver, then we resolve the plugin
*/
public async resolvePlugin(pluginId: string, type: PluginType = PluginType.System): Promise<PluginDeployerEntry[]> {
public async resolvePlugin(pluginId: string, type: PluginType = PluginType.System, options?: PluginDeployOptions): Promise<PluginDeployerEntry[]> {
const pluginDeployerEntries: PluginDeployerEntry[] = [];
const foundPluginResolver = this.pluginResolvers.find(pluginResolver => pluginResolver.accept(pluginId));
// there is a resolver for the input
if (foundPluginResolver) {

// create context object
const context = new PluginDeployerResolverContextImpl(foundPluginResolver, pluginId);

await foundPluginResolver.resolve(context);
await foundPluginResolver.resolve(context, options);

context.getPlugins().forEach(entry => {
entry.type = type;
Expand Down
10 changes: 5 additions & 5 deletions packages/plugin-ext/src/main/node/plugin-server-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { injectable, inject } from '@theia/core/shared/inversify';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import { PluginDeployerImpl } from './plugin-deployer-impl';
import { PluginsKeyValueStorage } from './plugins-key-value-storage';
import { PluginServer, PluginDeployer, PluginStorageKind, PluginType } from '../../common/plugin-protocol';
import { PluginServer, PluginDeployer, PluginStorageKind, PluginType, PluginDeployOptions } from '../../common/plugin-protocol';
import { KeysToAnyValues, KeysToKeysToAnyValue } from '../../common/types';

@injectable()
Expand All @@ -30,12 +30,12 @@ export class PluginServerHandler implements PluginServer {
@inject(PluginsKeyValueStorage)
protected readonly pluginsKeyValueStorage: PluginsKeyValueStorage;

deploy(pluginEntry: string, arg2?: PluginType | CancellationToken): Promise<void> {
deploy(pluginEntry: string, arg2?: PluginType | CancellationToken, options?: PluginDeployOptions): Promise<void> {
const type = typeof arg2 === 'number' ? arg2 as PluginType : undefined;
return this.doDeploy(pluginEntry, type);
return this.doDeploy(pluginEntry, type, options);
}
protected doDeploy(pluginEntry: string, type: PluginType = PluginType.User): Promise<void> {
return this.pluginDeployer.deploy(pluginEntry, type);
protected doDeploy(pluginEntry: string, type: PluginType = PluginType.User, options?: PluginDeployOptions): Promise<void> {
return this.pluginDeployer.deploy(pluginEntry, type, options);
}

undeploy(pluginId: string): Promise<void> {
Expand Down
18 changes: 11 additions & 7 deletions packages/vsx-registry/src/browser/vsx-extension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,21 @@ import URI from '@theia/core/lib/common/uri';
import { TreeElement } from '@theia/core/lib/browser/source-tree';
import { OpenerService, open, OpenerOptions } from '@theia/core/lib/browser/opener-service';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { PluginServer, DeployedPlugin, PluginType } from '@theia/plugin-ext/lib/common/plugin-protocol';
import { PluginServer, DeployedPlugin, PluginType, PluginDeployOptions } from '@theia/plugin-ext/lib/common/plugin-protocol';
import { VSXExtensionUri } from '../common/vsx-extension-uri';
import { ProgressService } from '@theia/core/lib/common/progress-service';
import { Endpoint } from '@theia/core/lib/browser/endpoint';
import { VSXEnvironment } from '../common/vsx-environment';
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
import { VSXExtensionNamespaceAccess, VSXUser } from '../common/vsx-registry-types';
import { MenuPath } from '@theia/core/lib/common';
import { CommandRegistry, MenuPath } from '@theia/core/lib/common';
import { ContextMenuRenderer } from '@theia/core/lib/browser';

export const EXTENSIONS_CONTEXT_MENU: MenuPath = ['extensions_context_menu'];

export namespace VSXExtensionsContextMenu {
export const COPY = [...EXTENSIONS_CONTEXT_MENU, '1_copy'];
export const INSTALL = [...EXTENSIONS_CONTEXT_MENU, '1_install'];
export const COPY = [...EXTENSIONS_CONTEXT_MENU, '2_copy'];
}

@injectable()
Expand Down Expand Up @@ -102,15 +103,18 @@ export class VSXExtension implements VSXExtensionData, TreeElement {
@inject(ProgressService)
protected readonly progressService: ProgressService;

@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;

@inject(VSXEnvironment)
readonly environment: VSXEnvironment;

@inject(VSXExtensionsSearchModel)
readonly search: VSXExtensionsSearchModel;

@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;

protected readonly data: Partial<VSXExtensionData> = {};

get uri(): URI {
Expand Down Expand Up @@ -248,11 +252,11 @@ export class VSXExtension implements VSXExtensionData, TreeElement {
return !!this._busy;
}

async install(): Promise<void> {
async install(options?: PluginDeployOptions): Promise<void> {
this._busy++;
try {
await this.progressService.withProgress(`"Installing '${this.id}' extension...`, 'extensions', () =>
this.pluginServer.deploy(this.uri.toString())
this.pluginServer.deploy(this.uri.toString(), undefined, options)
);
} finally {
this._busy--;
Expand Down
71 changes: 67 additions & 4 deletions packages/vsx-registry/src/browser/vsx-extensions-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,23 @@ import { ColorContribution } from '@theia/core/lib/browser/color-application-con
import { ColorRegistry, Color } from '@theia/core/lib/browser/color-registry';
import { TabBarToolbarContribution, TabBarToolbarItem, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { MenuModelRegistry, MessageService, Mutable } from '@theia/core/lib/common';
import { MessageService, Mutable } from '@theia/core/lib/common';
import { FileDialogService, OpenFileDialogProps } from '@theia/filesystem/lib/browser';
import { LabelProvider } from '@theia/core/lib/browser';
import { VscodeCommands } from '@theia/plugin-ext-vscode/lib/browser/plugin-vscode-commands-contribution';
import { VSXExtensionsContextMenu, VSXExtension } from './vsx-extension';
import { MenuContribution, MenuModelRegistry } from '@theia/core/lib/common/menu';
import { VSXExtension, VSXExtensionsContextMenu } from './vsx-extension';
import { QuickPickItem, QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import { VSXRegistryAPI } from '../common/vsx-registry-api';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as moment from 'moment';
import { PluginServer } from '@theia/plugin-ext/lib/common';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { VSXExtensionRaw } from '../common/vsx-registry-types';

interface QuickPickVersionItem {
version: string;
}

export namespace VSXExtensionsCommands {

Expand All @@ -47,6 +58,9 @@ export namespace VSXExtensionsCommands {
label: 'Install from VSIX...',
dialogLabel: 'Install from VSIX'
};
export const INSTALL_ANOTHER_VERSION: Command = {
id: 'vsxExtensions.installAnotherVersion'
};
export const COPY: Command = {
id: 'vsxExtensions.copy'
};
Expand All @@ -57,14 +71,17 @@ export namespace VSXExtensionsCommands {

@injectable()
export class VSXExtensionsContribution extends AbstractViewContribution<VSXExtensionsViewContainer>
implements ColorContribution, FrontendApplicationContribution, TabBarToolbarContribution {
implements ColorContribution, FrontendApplicationContribution, TabBarToolbarContribution, MenuContribution {

@inject(VSXExtensionsModel) protected readonly model: VSXExtensionsModel;
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry;
@inject(TabBarToolbarRegistry) protected readonly tabbarToolbarRegistry: TabBarToolbarRegistry;
@inject(FileDialogService) protected readonly fileDialogService: FileDialogService;
@inject(MessageService) protected readonly messageService: MessageService;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
@inject(QuickPickService) protected readonly quickPickService: QuickPickService;
@inject(VSXRegistryAPI) protected readonly vsxRegistryAPI: VSXRegistryAPI;
@inject(PluginServer) protected readonly pluginServer: PluginServer;
@inject(ClipboardService) protected readonly clipboardService: ClipboardService;

constructor() {
Expand Down Expand Up @@ -96,6 +113,11 @@ export class VSXExtensionsContribution extends AbstractViewContribution<VSXExten
execute: () => this.installFromVSIX()
});

commands.registerCommand({ id: VSXExtensionsCommands.INSTALL_ANOTHER_VERSION.id }, {
execute: async (extension: VSXExtension) => this.installAnotherVersion(extension),
isEnabled: (extension: VSXExtension) => !extension.builtin && !!extension.downloadUrl
});

commands.registerCommand(VSXExtensionsCommands.COPY, {
execute: (extension: VSXExtension) => this.copy(extension)
});
Expand Down Expand Up @@ -145,6 +167,11 @@ export class VSXExtensionsContribution extends AbstractViewContribution<VSXExten

registerMenus(menus: MenuModelRegistry): void {
super.registerMenus(menus);
menus.registerMenuAction(VSXExtensionsContextMenu.INSTALL, {
commandId: VSXExtensionsCommands.INSTALL_ANOTHER_VERSION.id,
label: 'Install Another Version...'
});

menus.registerMenuAction(VSXExtensionsContextMenu.COPY, {
commandId: VSXExtensionsCommands.COPY.id,
label: 'Copy',
Expand Down Expand Up @@ -189,7 +216,7 @@ export class VSXExtensionsContribution extends AbstractViewContribution<VSXExten
}

/**
* Installs a local .vsix file after prompting the `Open File` dialog. Resolves to the URI of the file.
* Installs a local .vsix file after prompting the `Open File` dialog.
*/
protected async installFromVSIX(): Promise<void> {
const props: OpenFileDialogProps = {
Expand All @@ -215,6 +242,42 @@ export class VSXExtensionsContribution extends AbstractViewContribution<VSXExten
}
}

/**
* Given an extension, displays a quick pick of other compatible versions and installs the selected version.
*
* @param extension a VSX extension.
*/
protected async installAnotherVersion(extension: VSXExtension): Promise<void> {
const extensionId = extension.id;
const currentVersion = extension.version;
const extensions = await this.vsxRegistryAPI.getAllVersions(extensionId);
const latestCompatible = await this.vsxRegistryAPI.getLatestCompatibleExtensionVersion(extensionId);
let compatibleExtensions: VSXExtensionRaw[] = [];
if (latestCompatible) {
compatibleExtensions = extensions.slice(extensions.findIndex(ext => ext.version === latestCompatible.version));
}
const items: QuickPickItem<QuickPickVersionItem>[] = [];
compatibleExtensions.forEach(ext => {
let publishedDate = moment(ext.timestamp).fromNow();
if (currentVersion === ext.version) {
publishedDate += ' (Current)';
}
items.push({
label: ext.version,
value: { version: ext.version },
description: publishedDate
});
});
const selectedVersion = await this.quickPickService.show(items, { placeholder: 'Select Version to Install', runIfSingle: false });
if (selectedVersion && selectedVersion.version !== currentVersion) {
const selectedExtension = this.model.getExtension(extensionId);
if (selectedExtension) {
selectedExtension.uninstall();
selectedExtension.install({ version: selectedVersion.version });
}
}
}

protected async copy(extension: VSXExtension): Promise<void> {
this.clipboardService.writeText(await extension.serialize());
}
Expand Down
21 changes: 15 additions & 6 deletions packages/vsx-registry/src/browser/vsx-extensions-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import * as sanitize from 'sanitize-html';
import { Emitter } from '@theia/core/lib/common/event';
import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import { VSXRegistryAPI, VSXResponseError } from '../common/vsx-registry-api';
import { VSXSearchParam } from '../common/vsx-registry-types';
import { VSXExtensionRaw, VSXSearchParam } from '../common/vsx-registry-types';
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
import { VSXExtension, VSXExtensionFactory } from './vsx-extension';
import { ProgressService } from '@theia/core/lib/common/progress-service';
Expand Down Expand Up @@ -164,16 +164,21 @@ export class VSXExtensionsModel {
const currInstalled = new Set<string>();
const refreshing = [];
for (const plugin of plugins) {
const version = plugin.model.version;
if (plugin.model.engine.type === 'vscode') {
const id = plugin.model.id;
this._installed.delete(id);
const extension = this.setExtension(id);
currInstalled.add(extension.id);
refreshing.push(this.refresh(id));
refreshing.push(this.refresh(id, version));
}
}
for (const id of this._installed) {
refreshing.push(this.refresh(id));
const extension = this.getExtension(id);
if (!extension) {
return;
}
refreshing.push(this.refresh(id, extension.version));
}
Promise.all(refreshing);
const installed = new Set([...prevInstalled, ...currInstalled]);
Expand Down Expand Up @@ -217,13 +222,18 @@ export class VSXExtensionsModel {
});
}

protected async refresh(id: string): Promise<VSXExtension | undefined> {
protected async refresh(id: string, version?: string): Promise<VSXExtension | undefined> {
try {
let extension = this.getExtension(id);
if (!this.shouldRefresh(extension)) {
return extension;
}
const data = await this.api.getLatestCompatibleExtensionVersion(id);
let data: VSXExtensionRaw | undefined;
if (version) {
data = await this.api.getExtension(id, { extensionVersion: version });
} else {
data = await this.api.getLatestCompatibleExtensionVersion(id);
}
if (!data) {
return;
}
Expand Down Expand Up @@ -285,5 +295,4 @@ export class VSXExtensionsModel {
}
return 0;
}

}
5 changes: 3 additions & 2 deletions packages/vsx-registry/src/common/vsx-registry-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,11 @@ export class VSXRegistryAPI {
return searchUri;
}

async getExtension(id: string): Promise<VSXExtensionRaw> {
async getExtension(id: string, queryParam?: QueryParam): Promise<VSXExtensionRaw> {
const apiUri = await this.environment.getRegistryApiUri();
const param: QueryParam = {
extensionId: id
...queryParam,
extensionId: id,
};
const result = await this.postJson<QueryParam, QueryResult>(apiUri.resolve('-/query').toString(), param);
if (result.extensions && result.extensions.length > 0) {
Expand Down
Loading

0 comments on commit f4074d8

Please sign in to comment.