Skip to content

Commit

Permalink
VSX: Add 'Install Another Version...' Command (#11303)
Browse files Browse the repository at this point in the history
* VSX: Add 'Install Another Version...' Command

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

Co-authored-by: seantan22 <[email protected]>
Co-authored-by: Colin Grant <[email protected]>
  • Loading branch information
colin-grant-work and seantan22 authored Jun 30, 2022
1 parent 14fe691 commit 7445e23
Show file tree
Hide file tree
Showing 20 changed files with 292 additions and 120 deletions.
3 changes: 2 additions & 1 deletion dev-packages/ovsx-client/src/ovsx-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ export class OVSXClient {
return new URL(`${url}${searchUri}`, this.options!.apiUrl).toString();
}

async getExtension(id: string): Promise<VSXExtensionRaw> {
async getExtension(id: string, queryParam?: VSXQueryParam): Promise<VSXExtensionRaw> {
const param: VSXQueryParam = {
...queryParam,
extensionId: id
};
const apiUri = this.buildQueryUri(param);
Expand Down
3 changes: 2 additions & 1 deletion packages/git/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"dugite-extra": "0.1.15",
"find-git-exec": "^0.0.4",
"find-git-repositories": "^0.1.1",
"moment": "2.29.2",
"luxon": "^2.4.0",
"octicons": "^7.1.0",
"p-queue": "^2.4.2",
"ts-md5": "^1.2.2"
Expand Down Expand Up @@ -66,6 +66,7 @@
},
"devDependencies": {
"@theia/ext-scripts": "1.26.0",
"@types/luxon": "^2.3.2",
"upath": "^1.0.2"
},
"nyc": {
Expand Down
18 changes: 10 additions & 8 deletions packages/git/src/browser/blame/blame-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { EditorManager, TextEditor, EditorDecoration, EditorDecorationOptions, Range, Position, EditorDecorationStyle } from '@theia/editor/lib/browser';
import { GitFileBlame } from '../../common';
import { Disposable, DisposableCollection } from '@theia/core';
import * as moment from 'moment';
import { Disposable, DisposableCollection, nls } from '@theia/core';
import { DateTime } from 'luxon';
import URI from '@theia/core/lib/common/uri';
import { DecorationStyle } from '@theia/core/lib/browser';
import * as monaco from '@theia/monaco-editor-core';
Expand Down Expand Up @@ -126,10 +126,10 @@ export class BlameDecorator implements monaco.languages.HoverProvider {
const commits = blame.commits;
for (const commit of commits) {
const sha = commit.sha;
const commitTime = moment(commit.author.timestamp);
const commitTime = DateTime.fromISO(commit.author.timestamp);
const heat = this.getHeatColor(commitTime);
const content = commit.summary.replace('\n', '↩︎').replace(/'/g, "\\'");
const short = sha.substr(0, 7);
const short = sha.substring(0, 7);
new EditorDecorationStyle('.git-' + short, style => {
Object.assign(style, BlameDecorator.defaultGutterStyles);
style.borderColor = heat;
Expand All @@ -140,7 +140,9 @@ export class BlameDecorator implements monaco.languages.HoverProvider {
}, this.blameDecorationsStyleSheet));
new EditorDecorationStyle('.git-' + short + '::after', style => {
Object.assign(style, BlameDecorator.defaultGutterAfterStyles);
style.content = `'${commitTime.fromNow()}'`;
style.content = (this.now.diff(commitTime, 'seconds').toObject().seconds ?? 0) < 60
? `'${nls.localize('theia/git/aFewSecondsAgo', 'a few seconds ago')}'`
: `'${commitTime.toRelative({ locale: nls.locale })}'`;
}, this.blameDecorationsStyleSheet);
}
const commitLines = blame.lines;
Expand Down Expand Up @@ -168,9 +170,9 @@ export class BlameDecorator implements monaco.languages.HoverProvider {
return { editorDecorations, styles };
}

protected now = moment();
protected getHeatColor(commitTime: moment.Moment): string {
const daysFromNow = this.now.diff(commitTime, 'days');
protected now = DateTime.now();
protected getHeatColor(commitTime: DateTime): string {
const daysFromNow = this.now.diff(commitTime, 'days').toObject().days ?? 0;
if (daysFromNow <= 2) {
return 'var(--md-orange-50)';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export class PluginVsCodeFileHandler implements PluginDeployerFileHandler {
fs.copyFile(currentPath, newPath, error => error ? reject(error) : resolve());
});
context.pluginEntry().updatePath(newPath);
context.pluginEntry().storeValue('sourceLocations', [newPath]);
} catch (e) {
console.error(`[${context.pluginEntry().id}]: Failed to copy to user directory. Future sessions may not have access to this plugin.`);
}
Expand Down
14 changes: 10 additions & 4 deletions packages/plugin-ext/src/common/plugin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export interface PluginDeployerResolver {

accept(pluginSourceId: string): boolean;

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

}

Expand Down Expand Up @@ -853,8 +853,8 @@ export interface PluginDependencies {

export const PluginDeployerHandler = Symbol('PluginDeployerHandler');
export interface PluginDeployerHandler {
deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<void>;
deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<void>;
deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<number | undefined>;
deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<number | undefined>;

getDeployedPluginsById(pluginId: string): DeployedPlugin[];

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

export interface PluginDeployOptions {
version: string;
/** Instructs the deployer to ignore any existing plugins with different versions */
ignoreOtherVersions?: boolean;
}

/**
* The JSON-RPC workspace interface.
*/
Expand All @@ -922,7 +928,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>;
uninstall(pluginId: PluginIdentifiers.VersionedId): Promise<void>;
undeploy(pluginId: PluginIdentifiers.VersionedId): Promise<void>;

Expand Down
13 changes: 7 additions & 6 deletions packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,10 +331,15 @@ export class HostedPluginSupport {
for (const versionedId of uninstalledPluginIds) {
const plugin = this.getPlugin(PluginIdentifiers.unversionedFromVersioned(versionedId));
if (plugin && PluginIdentifiers.componentsToVersionedId(plugin.metadata.model) === versionedId && !plugin.metadata.outOfSync) {
didChangeInstallationStatus = true;
plugin.metadata.outOfSync = didChangeInstallationStatus = true;
}
}
for (const contribution of this.contributions.values()) {
if (contribution.plugin.metadata.outOfSync && !uninstalledPluginIds.includes(PluginIdentifiers.componentsToVersionedId(contribution.plugin.metadata.model))) {
contribution.plugin.metadata.outOfSync = false;
didChangeInstallationStatus = true;
}
}
if (newPluginIds.length) {
const plugins = await this.server.getDeployedPlugins({ pluginIds: newPluginIds });
for (const plugin of plugins) {
Expand Down Expand Up @@ -573,11 +578,7 @@ export class HostedPluginSupport {
return;
}
this.activationEvents.add(activationEvent);
const activation: Promise<void>[] = [];
for (const manager of this.managers.values()) {
activation.push(manager.$activateByEvent(activationEvent));
}
await Promise.all(activation);
await Promise.all(Array.from(this.managers.values(), manager => manager.$activateByEvent(activationEvent)));
}

async activateByViewContainer(viewContainerId: string): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
protected readonly uninstallationManager: PluginUninstallationManager;

private readonly deployedLocations = new Map<PluginIdentifiers.VersionedId, Set<string>>();
protected readonly originalLocations = new Map<PluginIdentifiers.VersionedId, string>();
protected readonly sourceLocations = new Map<PluginIdentifiers.VersionedId, Set<string>>();

/**
* Managed plugin metadata backend entries.
Expand Down Expand Up @@ -80,7 +80,7 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
const matches: DeployedPlugin[] = [];
const handle = (plugins: Iterable<DeployedPlugin>): void => {
for (const plugin of plugins) {
if (PluginIdentifiers.componentsToVersionWithId(plugin.metadata.model).version === pluginId) {
if (PluginIdentifiers.componentsToVersionWithId(plugin.metadata.model).id === pluginId) {
matches.push(plugin);
}
}
Expand Down Expand Up @@ -117,28 +117,33 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
}
}

async deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<void> {
async deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<number> {
let successes = 0;
for (const plugin of frontendPlugins) {
await this.deployPlugin(plugin, 'frontend');
if (await this.deployPlugin(plugin, 'frontend')) { successes++; }
}
// resolve on first deploy
this.frontendPluginsMetadataDeferred.resolve(undefined);
return successes;
}

async deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<void> {
async deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<number> {
let successes = 0;
for (const plugin of backendPlugins) {
await this.deployPlugin(plugin, 'backend');
if (await this.deployPlugin(plugin, 'backend')) { successes++; }
}
// rebuild translation config after deployment
this.localizationService.buildTranslationConfig([...this.deployedBackendPlugins.values()]);
// resolve on first deploy
this.backendPluginsMetadataDeferred.resolve(undefined);
return successes;
}

/**
* @throws never! in order to isolate plugin deployment
* @throws never! in order to isolate plugin deployment.
* @returns whether the plugin is deployed after running this function. If the plugin was already installed, will still return `true`.
*/
protected async deployPlugin(entry: PluginDeployerEntry, entryPoint: keyof PluginEntryPoint): Promise<void> {
protected async deployPlugin(entry: PluginDeployerEntry, entryPoint: keyof PluginEntryPoint): Promise<boolean> {
const pluginPath = entry.path();
const deployPlugin = this.stopwatch.start('deployPlugin');
let id;
Expand All @@ -147,23 +152,23 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
const manifest = await this.reader.readPackage(pluginPath);
if (!manifest) {
deployPlugin.error(`Failed to read ${entryPoint} plugin manifest from '${pluginPath}''`);
return;
return success = false;
}

const metadata = this.reader.readMetadata(manifest);
metadata.isUnderDevelopment = entry.getValue('isUnderDevelopment') ?? false;

id = PluginIdentifiers.componentsToVersionedId(metadata.model);

const deployedLocations = this.deployedLocations.get(id) || new Set<string>();
const deployedLocations = this.deployedLocations.get(id) ?? new Set<string>();
deployedLocations.add(entry.rootPath);
this.deployedLocations.set(id, deployedLocations);
this.originalLocations.set(id, entry.originalPath());
this.setSourceLocationsForPlugin(id, entry);

const deployedPlugins = entryPoint === 'backend' ? this.deployedBackendPlugins : this.deployedFrontendPlugins;
if (deployedPlugins.has(id)) {
deployPlugin.debug(`Skipped ${entryPoint} plugin ${metadata.model.name} already deployed`);
return;
return true;
}

const { type } = entry;
Expand All @@ -173,23 +178,25 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
deployedPlugins.set(id, deployed);
deployPlugin.log(`Deployed ${entryPoint} plugin "${id}" from "${metadata.model.entryPoint[entryPoint] || pluginPath}"`);
} catch (e) {
success = false;
deployPlugin.error(`Failed to deploy ${entryPoint} plugin from '${pluginPath}' path`, e);
return success = false;
} finally {
if (success && id) {
this.uninstallationManager.markAsInstalled(id);
this.markAsInstalled(id);
}
}
return success;
}

async uninstallPlugin(pluginId: PluginIdentifiers.VersionedId): Promise<boolean> {
try {
const originalPath = this.originalLocations.get(pluginId);
if (!originalPath) {
const sourceLocations = this.sourceLocations.get(pluginId);
if (!sourceLocations) {
return false;
}
await fs.remove(originalPath);
this.originalLocations.delete(pluginId);
await Promise.all(Array.from(sourceLocations,
location => fs.remove(location).catch(err => console.error(`Failed to remove source for ${pluginId} at ${location}`, err))));
this.sourceLocations.delete(pluginId);
this.uninstallationManager.markAsUninstalled(pluginId);
return true;
} catch (e) {
Expand All @@ -198,6 +205,26 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
}
}

protected markAsInstalled(id: PluginIdentifiers.VersionedId): void {
const metadata = PluginIdentifiers.idAndVersionFromVersionedId(id);
if (metadata) {
const toMarkAsUninstalled: PluginIdentifiers.VersionedId[] = [];
const checkForDifferentVersions = (others: Iterable<PluginIdentifiers.VersionedId>) => {
for (const other of others) {
const otherMetadata = PluginIdentifiers.idAndVersionFromVersionedId(other);
if (metadata.id === otherMetadata?.id && metadata.version !== otherMetadata.version) {
toMarkAsUninstalled.push(other);
}
}
};
checkForDifferentVersions(this.deployedFrontendPlugins.keys());
checkForDifferentVersions(this.deployedBackendPlugins.keys());
this.uninstallationManager.markAsUninstalled(...toMarkAsUninstalled);
this.uninstallationManager.markAsInstalled(id);
toMarkAsUninstalled.forEach(pluginToUninstall => this.uninstallPlugin(pluginToUninstall));
}
}

async undeployPlugin(pluginId: PluginIdentifiers.VersionedId): Promise<boolean> {
this.deployedBackendPlugins.delete(pluginId);
this.deployedFrontendPlugins.delete(pluginId);
Expand All @@ -220,4 +247,14 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {

return true;
}

protected setSourceLocationsForPlugin(id: PluginIdentifiers.VersionedId, entry: PluginDeployerEntry): void {
const knownLocations = this.sourceLocations.get(id) ?? new Set();
const maybeStoredLocations = entry.getValue('sourceLocations');
const storedLocations = Array.isArray(maybeStoredLocations) && maybeStoredLocations.every(location => typeof location === 'string')
? maybeStoredLocations.concat(entry.originalPath())
: [entry.originalPath()];
storedLocations.forEach(location => knownLocations.add(location));
this.sourceLocations.set(id, knownLocations);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class PluginTheiaFileHandler implements PluginDeployerFileHandler {
fs.copyFile(currentPath, newPath, error => error ? reject(error) : resolve());
});
context.pluginEntry().updatePath(newPath);
context.pluginEntry().storeValue('sourceLocations', [newPath]);
} catch (e) {
console.error(`[${context.pluginEntry().id}]: Failed to copy to user directory. Future sessions may not have access to this plugin.`);
}
Expand Down
Loading

0 comments on commit 7445e23

Please sign in to comment.