Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VSX: Add 'Install Another Version...' Command #11303

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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