Skip to content

Commit 7445e23

Browse files
colin-grant-workseantan22
andauthored
VSX: Add 'Install Another Version...' Command (#11303)
* 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]>
1 parent 14fe691 commit 7445e23

20 files changed

+292
-120
lines changed

dev-packages/ovsx-client/src/ovsx-client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ export class OVSXClient {
7575
return new URL(`${url}${searchUri}`, this.options!.apiUrl).toString();
7676
}
7777

78-
async getExtension(id: string): Promise<VSXExtensionRaw> {
78+
async getExtension(id: string, queryParam?: VSXQueryParam): Promise<VSXExtensionRaw> {
7979
const param: VSXQueryParam = {
80+
...queryParam,
8081
extensionId: id
8182
};
8283
const apiUri = this.buildQueryUri(param);

packages/git/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"dugite-extra": "0.1.15",
1818
"find-git-exec": "^0.0.4",
1919
"find-git-repositories": "^0.1.1",
20-
"moment": "2.29.2",
20+
"luxon": "^2.4.0",
2121
"octicons": "^7.1.0",
2222
"p-queue": "^2.4.2",
2323
"ts-md5": "^1.2.2"
@@ -66,6 +66,7 @@
6666
},
6767
"devDependencies": {
6868
"@theia/ext-scripts": "1.26.0",
69+
"@types/luxon": "^2.3.2",
6970
"upath": "^1.0.2"
7071
},
7172
"nyc": {

packages/git/src/browser/blame/blame-decorator.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
import { inject, injectable } from '@theia/core/shared/inversify';
1818
import { EditorManager, TextEditor, EditorDecoration, EditorDecorationOptions, Range, Position, EditorDecorationStyle } from '@theia/editor/lib/browser';
1919
import { GitFileBlame } from '../../common';
20-
import { Disposable, DisposableCollection } from '@theia/core';
21-
import * as moment from 'moment';
20+
import { Disposable, DisposableCollection, nls } from '@theia/core';
21+
import { DateTime } from 'luxon';
2222
import URI from '@theia/core/lib/common/uri';
2323
import { DecorationStyle } from '@theia/core/lib/browser';
2424
import * as monaco from '@theia/monaco-editor-core';
@@ -126,10 +126,10 @@ export class BlameDecorator implements monaco.languages.HoverProvider {
126126
const commits = blame.commits;
127127
for (const commit of commits) {
128128
const sha = commit.sha;
129-
const commitTime = moment(commit.author.timestamp);
129+
const commitTime = DateTime.fromISO(commit.author.timestamp);
130130
const heat = this.getHeatColor(commitTime);
131131
const content = commit.summary.replace('\n', '↩︎').replace(/'/g, "\\'");
132-
const short = sha.substr(0, 7);
132+
const short = sha.substring(0, 7);
133133
new EditorDecorationStyle('.git-' + short, style => {
134134
Object.assign(style, BlameDecorator.defaultGutterStyles);
135135
style.borderColor = heat;
@@ -140,7 +140,9 @@ export class BlameDecorator implements monaco.languages.HoverProvider {
140140
}, this.blameDecorationsStyleSheet));
141141
new EditorDecorationStyle('.git-' + short + '::after', style => {
142142
Object.assign(style, BlameDecorator.defaultGutterAfterStyles);
143-
style.content = `'${commitTime.fromNow()}'`;
143+
style.content = (this.now.diff(commitTime, 'seconds').toObject().seconds ?? 0) < 60
144+
? `'${nls.localize('theia/git/aFewSecondsAgo', 'a few seconds ago')}'`
145+
: `'${commitTime.toRelative({ locale: nls.locale })}'`;
144146
}, this.blameDecorationsStyleSheet);
145147
}
146148
const commitLines = blame.lines;
@@ -168,9 +170,9 @@ export class BlameDecorator implements monaco.languages.HoverProvider {
168170
return { editorDecorations, styles };
169171
}
170172

171-
protected now = moment();
172-
protected getHeatColor(commitTime: moment.Moment): string {
173-
const daysFromNow = this.now.diff(commitTime, 'days');
173+
protected now = DateTime.now();
174+
protected getHeatColor(commitTime: DateTime): string {
175+
const daysFromNow = this.now.diff(commitTime, 'days').toObject().days ?? 0;
174176
if (daysFromNow <= 2) {
175177
return 'var(--md-orange-50)';
176178
}

packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export class PluginVsCodeFileHandler implements PluginDeployerFileHandler {
7474
fs.copyFile(currentPath, newPath, error => error ? reject(error) : resolve());
7575
});
7676
context.pluginEntry().updatePath(newPath);
77+
context.pluginEntry().storeValue('sourceLocations', [newPath]);
7778
} catch (e) {
7879
console.error(`[${context.pluginEntry().id}]: Failed to copy to user directory. Future sessions may not have access to this plugin.`);
7980
}

packages/plugin-ext/src/common/plugin-protocol.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ export interface PluginDeployerResolver {
343343

344344
accept(pluginSourceId: string): boolean;
345345

346-
resolve(pluginResolverContext: PluginDeployerResolverContext): Promise<void>;
346+
resolve(pluginResolverContext: PluginDeployerResolverContext, options?: PluginDeployOptions): Promise<void>;
347347

348348
}
349349

@@ -853,8 +853,8 @@ export interface PluginDependencies {
853853

854854
export const PluginDeployerHandler = Symbol('PluginDeployerHandler');
855855
export interface PluginDeployerHandler {
856-
deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<void>;
857-
deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<void>;
856+
deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<number | undefined>;
857+
deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<number | undefined>;
858858

859859
getDeployedPluginsById(pluginId: string): DeployedPlugin[];
860860

@@ -910,6 +910,12 @@ export interface WorkspaceStorageKind {
910910
export type GlobalStorageKind = undefined;
911911
export type PluginStorageKind = GlobalStorageKind | WorkspaceStorageKind;
912912

913+
export interface PluginDeployOptions {
914+
version: string;
915+
/** Instructs the deployer to ignore any existing plugins with different versions */
916+
ignoreOtherVersions?: boolean;
917+
}
918+
913919
/**
914920
* The JSON-RPC workspace interface.
915921
*/
@@ -922,7 +928,7 @@ export interface PluginServer {
922928
*
923929
* @param type whether a plugin is installed by a system or a user, defaults to a user
924930
*/
925-
deploy(pluginEntry: string, type?: PluginType): Promise<void>;
931+
deploy(pluginEntry: string, type?: PluginType, options?: PluginDeployOptions): Promise<void>;
926932
uninstall(pluginId: PluginIdentifiers.VersionedId): Promise<void>;
927933
undeploy(pluginId: PluginIdentifiers.VersionedId): Promise<void>;
928934

packages/plugin-ext/src/hosted/browser/hosted-plugin.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -331,10 +331,15 @@ export class HostedPluginSupport {
331331
for (const versionedId of uninstalledPluginIds) {
332332
const plugin = this.getPlugin(PluginIdentifiers.unversionedFromVersioned(versionedId));
333333
if (plugin && PluginIdentifiers.componentsToVersionedId(plugin.metadata.model) === versionedId && !plugin.metadata.outOfSync) {
334-
didChangeInstallationStatus = true;
335334
plugin.metadata.outOfSync = didChangeInstallationStatus = true;
336335
}
337336
}
337+
for (const contribution of this.contributions.values()) {
338+
if (contribution.plugin.metadata.outOfSync && !uninstalledPluginIds.includes(PluginIdentifiers.componentsToVersionedId(contribution.plugin.metadata.model))) {
339+
contribution.plugin.metadata.outOfSync = false;
340+
didChangeInstallationStatus = true;
341+
}
342+
}
338343
if (newPluginIds.length) {
339344
const plugins = await this.server.getDeployedPlugins({ pluginIds: newPluginIds });
340345
for (const plugin of plugins) {
@@ -573,11 +578,7 @@ export class HostedPluginSupport {
573578
return;
574579
}
575580
this.activationEvents.add(activationEvent);
576-
const activation: Promise<void>[] = [];
577-
for (const manager of this.managers.values()) {
578-
activation.push(manager.$activateByEvent(activationEvent));
579-
}
580-
await Promise.all(activation);
581+
await Promise.all(Array.from(this.managers.values(), manager => manager.$activateByEvent(activationEvent)));
581582
}
582583

583584
async activateByViewContainer(viewContainerId: string): Promise<void> {

packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
4646
protected readonly uninstallationManager: PluginUninstallationManager;
4747

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

5151
/**
5252
* Managed plugin metadata backend entries.
@@ -80,7 +80,7 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
8080
const matches: DeployedPlugin[] = [];
8181
const handle = (plugins: Iterable<DeployedPlugin>): void => {
8282
for (const plugin of plugins) {
83-
if (PluginIdentifiers.componentsToVersionWithId(plugin.metadata.model).version === pluginId) {
83+
if (PluginIdentifiers.componentsToVersionWithId(plugin.metadata.model).id === pluginId) {
8484
matches.push(plugin);
8585
}
8686
}
@@ -117,28 +117,33 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
117117
}
118118
}
119119

120-
async deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<void> {
120+
async deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<number> {
121+
let successes = 0;
121122
for (const plugin of frontendPlugins) {
122-
await this.deployPlugin(plugin, 'frontend');
123+
if (await this.deployPlugin(plugin, 'frontend')) { successes++; }
123124
}
124125
// resolve on first deploy
125126
this.frontendPluginsMetadataDeferred.resolve(undefined);
127+
return successes;
126128
}
127129

128-
async deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<void> {
130+
async deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<number> {
131+
let successes = 0;
129132
for (const plugin of backendPlugins) {
130-
await this.deployPlugin(plugin, 'backend');
133+
if (await this.deployPlugin(plugin, 'backend')) { successes++; }
131134
}
132135
// rebuild translation config after deployment
133136
this.localizationService.buildTranslationConfig([...this.deployedBackendPlugins.values()]);
134137
// resolve on first deploy
135138
this.backendPluginsMetadataDeferred.resolve(undefined);
139+
return successes;
136140
}
137141

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

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

156161
id = PluginIdentifiers.componentsToVersionedId(metadata.model);
157162

158-
const deployedLocations = this.deployedLocations.get(id) || new Set<string>();
163+
const deployedLocations = this.deployedLocations.get(id) ?? new Set<string>();
159164
deployedLocations.add(entry.rootPath);
160165
this.deployedLocations.set(id, deployedLocations);
161-
this.originalLocations.set(id, entry.originalPath());
166+
this.setSourceLocationsForPlugin(id, entry);
162167

163168
const deployedPlugins = entryPoint === 'backend' ? this.deployedBackendPlugins : this.deployedFrontendPlugins;
164169
if (deployedPlugins.has(id)) {
165170
deployPlugin.debug(`Skipped ${entryPoint} plugin ${metadata.model.name} already deployed`);
166-
return;
171+
return true;
167172
}
168173

169174
const { type } = entry;
@@ -173,23 +178,25 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
173178
deployedPlugins.set(id, deployed);
174179
deployPlugin.log(`Deployed ${entryPoint} plugin "${id}" from "${metadata.model.entryPoint[entryPoint] || pluginPath}"`);
175180
} catch (e) {
176-
success = false;
177181
deployPlugin.error(`Failed to deploy ${entryPoint} plugin from '${pluginPath}' path`, e);
182+
return success = false;
178183
} finally {
179184
if (success && id) {
180-
this.uninstallationManager.markAsInstalled(id);
185+
this.markAsInstalled(id);
181186
}
182187
}
188+
return success;
183189
}
184190

185191
async uninstallPlugin(pluginId: PluginIdentifiers.VersionedId): Promise<boolean> {
186192
try {
187-
const originalPath = this.originalLocations.get(pluginId);
188-
if (!originalPath) {
193+
const sourceLocations = this.sourceLocations.get(pluginId);
194+
if (!sourceLocations) {
189195
return false;
190196
}
191-
await fs.remove(originalPath);
192-
this.originalLocations.delete(pluginId);
197+
await Promise.all(Array.from(sourceLocations,
198+
location => fs.remove(location).catch(err => console.error(`Failed to remove source for ${pluginId} at ${location}`, err))));
199+
this.sourceLocations.delete(pluginId);
193200
this.uninstallationManager.markAsUninstalled(pluginId);
194201
return true;
195202
} catch (e) {
@@ -198,6 +205,26 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
198205
}
199206
}
200207

208+
protected markAsInstalled(id: PluginIdentifiers.VersionedId): void {
209+
const metadata = PluginIdentifiers.idAndVersionFromVersionedId(id);
210+
if (metadata) {
211+
const toMarkAsUninstalled: PluginIdentifiers.VersionedId[] = [];
212+
const checkForDifferentVersions = (others: Iterable<PluginIdentifiers.VersionedId>) => {
213+
for (const other of others) {
214+
const otherMetadata = PluginIdentifiers.idAndVersionFromVersionedId(other);
215+
if (metadata.id === otherMetadata?.id && metadata.version !== otherMetadata.version) {
216+
toMarkAsUninstalled.push(other);
217+
}
218+
}
219+
};
220+
checkForDifferentVersions(this.deployedFrontendPlugins.keys());
221+
checkForDifferentVersions(this.deployedBackendPlugins.keys());
222+
this.uninstallationManager.markAsUninstalled(...toMarkAsUninstalled);
223+
this.uninstallationManager.markAsInstalled(id);
224+
toMarkAsUninstalled.forEach(pluginToUninstall => this.uninstallPlugin(pluginToUninstall));
225+
}
226+
}
227+
201228
async undeployPlugin(pluginId: PluginIdentifiers.VersionedId): Promise<boolean> {
202229
this.deployedBackendPlugins.delete(pluginId);
203230
this.deployedFrontendPlugins.delete(pluginId);
@@ -220,4 +247,14 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler {
220247

221248
return true;
222249
}
250+
251+
protected setSourceLocationsForPlugin(id: PluginIdentifiers.VersionedId, entry: PluginDeployerEntry): void {
252+
const knownLocations = this.sourceLocations.get(id) ?? new Set();
253+
const maybeStoredLocations = entry.getValue('sourceLocations');
254+
const storedLocations = Array.isArray(maybeStoredLocations) && maybeStoredLocations.every(location => typeof location === 'string')
255+
? maybeStoredLocations.concat(entry.originalPath())
256+
: [entry.originalPath()];
257+
storedLocations.forEach(location => knownLocations.add(location));
258+
this.sourceLocations.set(id, knownLocations);
259+
}
223260
}

packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export class PluginTheiaFileHandler implements PluginDeployerFileHandler {
6666
fs.copyFile(currentPath, newPath, error => error ? reject(error) : resolve());
6767
});
6868
context.pluginEntry().updatePath(newPath);
69+
context.pluginEntry().storeValue('sourceLocations', [newPath]);
6970
} catch (e) {
7071
console.error(`[${context.pluginEntry().id}]: Failed to copy to user directory. Future sessions may not have access to this plugin.`);
7172
}

0 commit comments

Comments
 (0)