diff --git a/CHANGELOG.md b/CHANGELOG.md index 5966158e4baab..5dcc1a00304b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ - [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/) +## 1.60.0 - + +[Breaking Changes:](#breaking_changes_1.61.0) + +- [core] allow to disable plugins. The PR includes a couple of renamings: `HostedPluginDeployerHandler` => `PluginDeployerHandlerImpl` and +`PluginServerHandler` => `PluginServerImpl`. Also removed the ability of `HostedPluginProcess` to add extra deployed plugins. [#15205](https://github.com/eclipse-theia/theia/pull/15205) - contributed on behalf of STMicroelectronics + ## 1.60.0 - 04/03/2025 - [ai] add dummy preference descriptions to open AI config widget [#15166](https://github.com/eclipse-theia/theia/pull/15166) diff --git a/packages/core/src/node/backend-application-module.ts b/packages/core/src/node/backend-application-module.ts index f5a17db9bd514..24e3759e1901f 100644 --- a/packages/core/src/node/backend-application-module.ts +++ b/packages/core/src/node/backend-application-module.ts @@ -43,6 +43,7 @@ import { BackendRequestFacade } from './request/backend-request-facade'; import { FileSystemLocking, FileSystemLockingImpl } from './filesystem-locking'; import { BackendRemoteService } from './remote/backend-remote-service'; import { RemoteCliContribution } from './remote/remote-cli-contribution'; +import { SettingService, SettingServiceImpl } from './setting-service'; decorate(injectable(), ApplicationPackage); @@ -136,4 +137,7 @@ export const backendApplicationModule = new ContainerModule(bind => { bindBackendStopwatchServer(bind); bind(FileSystemLocking).to(FileSystemLockingImpl).inSingletonScope(); + + bind(SettingServiceImpl).toSelf().inSingletonScope(); + bind(SettingService).toService(SettingServiceImpl); }); diff --git a/packages/core/src/node/index.ts b/packages/core/src/node/index.ts index c7d735f4f03d6..182e8c82edbce 100644 --- a/packages/core/src/node/index.ts +++ b/packages/core/src/node/index.ts @@ -19,4 +19,5 @@ export * from './debug'; export * from '../common/file-uri'; export * from './messaging'; export * from './cli'; +export * from './setting-service'; export { FileSystemLocking } from './filesystem-locking'; diff --git a/packages/core/src/node/setting-service.ts b/packages/core/src/node/setting-service.ts new file mode 100644 index 0000000000000..2edb048f79296 --- /dev/null +++ b/packages/core/src/node/setting-service.ts @@ -0,0 +1,78 @@ +// ***************************************************************************** +// Copyright (C) 2025 STMicroelectronics and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable, postConstruct } from 'inversify'; +import { EnvVariablesServer } from '../common/env-variables'; +import { Deferred } from '../common/promise-util'; +import { promises as fs } from 'fs'; +import { URI } from '../common'; + +export const SettingService = Symbol('SettingService'); + +/** + * A service providing a simple user-level, persistent key-value store on the back end + */ +export interface SettingService { + set(key: string, value: string): Promise; + get(key: string): Promise; +} + +@injectable() +export class SettingServiceImpl implements SettingService { + + @inject(EnvVariablesServer) + protected readonly envVarServer: EnvVariablesServer; + + protected readonly ready = new Deferred(); + protected values: Record = {}; + + @postConstruct() + protected init(): void { + const asyncInit = async () => { + const configDir = new URI(await this.envVarServer.getConfigDirUri()); + const path: string = configDir.resolve('backend-settings.json').path.fsPath(); + try { + const contents = await fs.readFile(path, { + encoding: 'utf-8' + }); + this.values = JSON.parse(contents); + } catch (e) { + console.log(e); + } finally { + this.ready.resolve(); + } + }; + asyncInit(); + } + + async set(key: string, value: string): Promise { + await this.ready.promise; + this.values[key] = value; + await this.writeFile(); + } + + async writeFile(): Promise { + const configDir = new URI(await this.envVarServer.getConfigDirUri()); + const path: string = configDir.resolve('backend-settings.json').path.fsPath(); + const values = JSON.stringify(this.values); + await fs.writeFile(path, values); + } + + async get(key: string): Promise { + await this.ready.promise; + return this.values[key]; + } +} diff --git a/packages/plugin-dev/src/node/hosted-plugin-reader.ts b/packages/plugin-dev/src/node/hosted-plugin-reader.ts index d4bcbf87061c0..d5377b1cee00f 100644 --- a/packages/plugin-dev/src/node/hosted-plugin-reader.ts +++ b/packages/plugin-dev/src/node/hosted-plugin-reader.ts @@ -18,9 +18,8 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; import { HostedPluginReader as PluginReaderHosted } from '@theia/plugin-ext/lib/hosted/node/plugin-reader'; import { Deferred } from '@theia/core/lib/common/promise-util'; -import { PluginMetadata } from '@theia/plugin-ext/lib/common/plugin-protocol'; +import { PluginDeployerHandler, PluginMetadata } from '@theia/plugin-ext/lib/common/plugin-protocol'; import { PluginDeployerEntryImpl } from '@theia/plugin-ext/lib/main/node/plugin-deployer-entry-impl'; -import { HostedPluginDeployerHandler } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin-deployer-handler'; @injectable() export class HostedPluginReader implements BackendApplicationContribution { @@ -30,8 +29,8 @@ export class HostedPluginReader implements BackendApplicationContribution { private readonly hostedPlugin = new Deferred(); - @inject(HostedPluginDeployerHandler) - protected deployerHandler: HostedPluginDeployerHandler; + @inject(PluginDeployerHandler) + protected deployerHandler: PluginDeployerHandler; async initialize(): Promise { this.pluginReader.getPluginMetadata(process.env.HOSTED_PLUGIN) diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts index 8df4042207dad..7cbbf8e953cf5 100755 --- a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts +++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts @@ -378,7 +378,7 @@ export class PluginVscodeCommandsContribution implements CommandContribution { commands.registerCommand(VscodeCommands.INSTALL_EXTENSION_FROM_ID_OR_URI, { execute: async (vsixUriOrExtensionId: TheiaURI | UriComponents | string) => { if (typeof vsixUriOrExtensionId === 'string') { - await this.pluginServer.deploy(VSCodeExtensionUri.fromId(vsixUriOrExtensionId).toString()); + await this.pluginServer.install(VSCodeExtensionUri.fromId(vsixUriOrExtensionId).toString()); } else { await this.deployPlugin(vsixUriOrExtensionId); } @@ -1001,7 +1001,7 @@ export class PluginVscodeCommandsContribution implements CommandContribution { private async deployPlugin(uri: TheiaURI | UriComponents): Promise { const uriPath = isUriComponents(uri) ? URI.revive(uri).fsPath : await this.fileService.fsPath(uri); - return this.pluginServer.deploy(`local-file:${uriPath}`); + return this.pluginServer.install(`local-file:${uriPath}`); } private async resolveLanguageId(resource: URI): Promise { diff --git a/packages/plugin-ext/README.md b/packages/plugin-ext/README.md index 13d4f63de0577..2274581d941f4 100644 --- a/packages/plugin-ext/README.md +++ b/packages/plugin-ext/README.md @@ -33,10 +33,11 @@ The implementation is inspired from: ; deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise; + getDeployedPluginIds(): Promise; getDeployedPlugins(): Promise; getDeployedPluginsById(pluginId: string): DeployedPlugin[]; @@ -995,6 +996,7 @@ export interface PluginDeployerHandler { * Unless `--uncompressed-plugins-in-place` is passed to the CLI, this operation is safe. */ uninstallPlugin(pluginId: PluginIdentifiers.VersionedId): Promise; + /** * Removes the plugin from the locations to which it had been deployed. * This operation is not safe - references to deleted assets may remain. @@ -1002,10 +1004,22 @@ export interface PluginDeployerHandler { undeployPlugin(pluginId: PluginIdentifiers.VersionedId): Promise; getPluginDependencies(pluginToBeInstalled: PluginDeployerEntry): Promise; -} -export interface GetDeployedPluginsParams { - pluginIds: PluginIdentifiers.VersionedId[] + /** + * Marks the given plugins as "disabled". While the plugin remains installed, it will no longer + * be used. Has no effect if the plugin is not installed + * @param pluginId the plugin to disable + * @returns whether the plugin was installed, enabled and could be disabled + */ + disablePlugin(pluginId: PluginIdentifiers.VersionedId): Promise; + + /** + * Marks the given plugins as "enabled". Has no effect if the plugin is not installed. + * @param pluginId the plugin to enabled + * @returns whether the plugin was installed, disabled and could be enabled + */ + enablePlugin(pluginId: PluginIdentifiers.VersionedId): Promise; + } export interface DeployedPlugin { @@ -1024,7 +1038,9 @@ export interface HostedPluginServer extends RpcServer { getUninstalledPluginIds(): Promise; - getDeployedPlugins(params: GetDeployedPluginsParams): Promise; + getDisabledPluginIds(): Promise; + + getDeployedPlugins(ids: PluginIdentifiers.VersionedId[]): Promise; getExtPluginAPI(): Promise; @@ -1047,9 +1063,6 @@ export interface PluginDeployOptions { ignoreOtherVersions?: boolean; } -/** - * The JSON-RPC workspace interface. - */ export const pluginServerJsonRpcPath = '/services/plugin-ext'; export const PluginServer = Symbol('PluginServer'); export interface PluginServer { @@ -1059,9 +1072,15 @@ 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, options?: PluginDeployOptions): Promise; + install(pluginEntry: string, type?: PluginType, options?: PluginDeployOptions): Promise; uninstall(pluginId: PluginIdentifiers.VersionedId): Promise; - undeploy(pluginId: PluginIdentifiers.VersionedId): Promise; + + enablePlugin(pluginId: PluginIdentifiers.VersionedId): Promise; + disablePlugin(pluginId: PluginIdentifiers.VersionedId): Promise; + + getInstalledPlugins(): Promise; + getUninstalledPlugins(): Promise; + getDisabledPlugins(): Promise; setStorageValue(key: string, value: KeysToAnyValues, kind: PluginStorageKind): Promise; getStorageValue(key: string, kind: PluginStorageKind): Promise; @@ -1077,17 +1096,6 @@ export interface ServerPluginRunner { setClient(client: HostedPluginClient): void; setDefault(defaultRunner: ServerPluginRunner): void; clientClosed(): void; - - /** - * Provides additional deployed plugins. - */ - getExtraDeployedPlugins(): Promise; - - /** - * Provides additional plugin ids. - */ - getExtraDeployedPluginIds(): Promise; - } export const PluginHostEnvironmentVariable = Symbol('PluginHostEnvironmentVariable'); diff --git a/packages/plugin-ext/src/hosted/common/hosted-plugin.ts b/packages/plugin-ext/src/hosted/common/hosted-plugin.ts index ba9690bef5648..d09ae0388ffc5 100644 --- a/packages/plugin-ext/src/hosted/common/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/common/hosted-plugin.ts @@ -208,7 +208,10 @@ export abstract class AbstractHostedPluginSupport { - return []; - } - - /** - * Provides additional deployed plugins. - */ - public async getExtraDeployedPlugins(): Promise { - return []; - } - } diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin.ts index 8e60f20f184f2..966b6283770f9 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/node/hosted-plugin.ts @@ -16,7 +16,7 @@ import { injectable, inject, multiInject, postConstruct, optional } from '@theia/core/shared/inversify'; import { ILogger, ConnectionErrorHandler } from '@theia/core/lib/common'; -import { HostedPluginClient, PluginModel, ServerPluginRunner, DeployedPlugin, PluginIdentifiers } from '../../common/plugin-protocol'; +import { HostedPluginClient, PluginModel, ServerPluginRunner } from '../../common/plugin-protocol'; import { LogPart } from '../../common/types'; import { HostedPluginProcess } from './hosted-plugin-process'; @@ -92,20 +92,6 @@ export class HostedPluginSupport { } } - /** - * Provides additional plugin ids. - */ - async getExtraDeployedPluginIds(): Promise { - return [].concat.apply([], await Promise.all(this.pluginRunners.map(runner => runner.getExtraDeployedPluginIds()))); - } - - /** - * Provides additional deployed plugins. - */ - async getExtraDeployedPlugins(): Promise { - return [].concat.apply([], await Promise.all(this.pluginRunners.map(runner => runner.getExtraDeployedPlugins()))); - } - sendLog(logPart: LogPart): void { this.client.log(logPart); } diff --git a/packages/plugin-ext/src/hosted/node/metadata-scanner.ts b/packages/plugin-ext/src/hosted/node/metadata-scanner.ts index d03509ad4ad15..e25934c0fb3f5 100644 --- a/packages/plugin-ext/src/hosted/node/metadata-scanner.ts +++ b/packages/plugin-ext/src/hosted/node/metadata-scanner.ts @@ -29,13 +29,14 @@ export class MetadataScanner { }); } - getPluginMetadata(plugin: PluginPackage): PluginMetadata { + async getPluginMetadata(plugin: PluginPackage): Promise { const scanner = this.getScanner(plugin); + const id = PluginIdentifiers.componentsToVersionedId(plugin); return { host: PLUGIN_HOST_BACKEND, model: scanner.getModel(plugin), lifecycle: scanner.getLifecycle(plugin), - outOfSync: this.uninstallationManager.isUninstalled(PluginIdentifiers.componentsToVersionedId(plugin)), + outOfSync: this.uninstallationManager.isUninstalled(id) || await this.uninstallationManager.isDisabled(id), }; } diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts b/packages/plugin-ext/src/hosted/node/plugin-deployer-handler-impl.ts similarity index 94% rename from packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts rename to packages/plugin-ext/src/hosted/node/plugin-deployer-handler-impl.ts index 49cce6b693389..1f94ded447ac8 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin-deployer-handler.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-deployer-handler-impl.ts @@ -28,8 +28,7 @@ import { Stopwatch } from '@theia/core/lib/common'; import { PluginUninstallationManager } from '../../main/node/plugin-uninstallation-manager'; @injectable() -export class HostedPluginDeployerHandler implements PluginDeployerHandler { - +export class PluginDeployerHandlerImpl implements PluginDeployerHandler { @inject(ILogger) protected readonly logger: ILogger; @@ -83,6 +82,10 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler { return Array.from(this.deployedBackendPlugins.values()); } + async getDeployedPluginIds(): Promise { + return [... await this.getDeployedBackendPluginIds(), ... await this.getDeployedFrontendPluginIds()]; + } + async getDeployedPlugins(): Promise { await this.frontendPluginsMetadataDeferred.promise; await this.backendPluginsMetadataDeferred.promise; @@ -117,7 +120,7 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler { if (!manifest) { return undefined; } - const metadata = this.reader.readMetadata(manifest); + const metadata = await this.reader.readMetadata(manifest); const dependencies: PluginDependencies = { metadata }; // Do not resolve system (aka builtin) plugins because it should be done statically at build time. if (entry.type !== PluginType.System) { @@ -168,7 +171,7 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler { return success = false; } - const metadata = this.reader.readMetadata(manifest); + const metadata = await this.reader.readMetadata(manifest); metadata.isUnderDevelopment = entry.getValue('isUnderDevelopment') ?? false; id = PluginIdentifiers.componentsToVersionedId(metadata.model); @@ -271,4 +274,12 @@ export class HostedPluginDeployerHandler implements PluginDeployerHandler { storedLocations.forEach(location => knownLocations.add(location)); this.sourceLocations.set(id, knownLocations); } + + async enablePlugin(pluginId: PluginIdentifiers.VersionedId): Promise { + return this.uninstallationManager.markAsEnabled(pluginId); + } + + async disablePlugin(pluginId: PluginIdentifiers.VersionedId): Promise { + return this.uninstallationManager.markAsDisabled(pluginId); + } } diff --git a/packages/plugin-ext/src/hosted/node/plugin-ext-hosted-backend-module.ts b/packages/plugin-ext/src/hosted/node/plugin-ext-hosted-backend-module.ts index ae10070f58ea1..a10c49af05a3d 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-ext-hosted-backend-module.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-ext-hosted-backend-module.ts @@ -30,7 +30,7 @@ import { GrammarsReader } from './scanners/grammars-reader'; import { HostedPluginProcess, HostedPluginProcessConfiguration } from './hosted-plugin-process'; import { ExtPluginApiProvider } from '../../common/plugin-ext-api-contribution'; import { HostedPluginCliContribution } from './hosted-plugin-cli-contribution'; -import { HostedPluginDeployerHandler } from './hosted-plugin-deployer-handler'; +import { PluginDeployerHandlerImpl } from './plugin-deployer-handler-impl'; import { PluginUriFactory } from './scanners/plugin-uri-factory'; import { FilePluginUriFactory } from './scanners/file-plugin-uri-factory'; import { HostedPluginLocalizationService } from './hosted-plugin-localization-service'; @@ -67,8 +67,8 @@ export function bindCommonHostedBackend(bind: interfaces.Bind): void { bind(HostedPluginLocalizationService).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(HostedPluginLocalizationService); - bind(HostedPluginDeployerHandler).toSelf().inSingletonScope(); - bind(PluginDeployerHandler).toService(HostedPluginDeployerHandler); + bind(PluginDeployerHandlerImpl).toSelf().inSingletonScope(); + bind(PluginDeployerHandler).toService(PluginDeployerHandlerImpl); bind(PluginLanguagePackService).toSelf().inSingletonScope(); bind(LanguagePackService).toService(PluginLanguagePackService); diff --git a/packages/plugin-ext/src/hosted/node/plugin-reader.ts b/packages/plugin-ext/src/hosted/node/plugin-reader.ts index f90df35151083..4258668ad3cca 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-reader.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-reader.ts @@ -103,8 +103,8 @@ export class HostedPluginReader implements BackendApplicationContribution { return manifest; } - readMetadata(plugin: PluginPackage): PluginMetadata { - const pluginMetadata = this.scanner.getPluginMetadata(plugin); + async readMetadata(plugin: PluginPackage): Promise { + const pluginMetadata = await this.scanner.getPluginMetadata(plugin); if (pluginMetadata.model.entryPoint.backend) { pluginMetadata.model.entryPoint.backend = path.resolve(plugin.packagePath, pluginMetadata.model.entryPoint.backend); } diff --git a/packages/plugin-ext/src/hosted/node/plugin-service.ts b/packages/plugin-ext/src/hosted/node/plugin-service.ts index 9b1d09a899ae7..ec73be10fb4f3 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-service.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-service.ts @@ -14,14 +14,15 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { injectable, inject, named, optional, postConstruct } from '@theia/core/shared/inversify'; -import { HostedPluginServer, HostedPluginClient, PluginDeployer, GetDeployedPluginsParams, DeployedPlugin, PluginIdentifiers } from '../../common/plugin-protocol'; +import { HostedPluginServer, HostedPluginClient, PluginDeployer, DeployedPlugin, PluginIdentifiers } from '../../common/plugin-protocol'; import { HostedPluginSupport } from './hosted-plugin'; import { ILogger, Disposable, ContributionProvider, DisposableCollection } from '@theia/core'; import { ExtPluginApiProvider, ExtPluginApi } from '../../common/plugin-ext-api-contribution'; -import { HostedPluginDeployerHandler } from './hosted-plugin-deployer-handler'; +import { PluginDeployerHandlerImpl } from './plugin-deployer-handler-impl'; import { PluginDeployerImpl } from '../../main/node/plugin-deployer-impl'; import { HostedPluginLocalizationService } from './hosted-plugin-localization-service'; import { PluginUninstallationManager } from '../../main/node/plugin-uninstallation-manager'; +import { Deferred } from '@theia/core/lib/common/promise-util'; export const BackendPluginHostableFilter = Symbol('BackendPluginHostableFilter'); /** @@ -31,13 +32,16 @@ export const BackendPluginHostableFilter = Symbol('BackendPluginHostableFilter') */ export type BackendPluginHostableFilter = (plugin: DeployedPlugin) => boolean; +/** + * This class implements the per-front-end services for plugin management and communication + */ @injectable() export class HostedPluginServerImpl implements HostedPluginServer { @inject(ILogger) protected readonly logger: ILogger; - @inject(HostedPluginDeployerHandler) - protected readonly deployerHandler: HostedPluginDeployerHandler; + @inject(PluginDeployerHandlerImpl) + protected readonly deployerHandler: PluginDeployerHandlerImpl; @inject(PluginDeployer) protected readonly pluginDeployer: PluginDeployerImpl; @@ -58,18 +62,13 @@ export class HostedPluginServerImpl implements HostedPluginServer { protected client: HostedPluginClient | undefined; protected toDispose = new DisposableCollection(); - protected _ignoredPlugins?: Set; - - // We ignore any plugins that are marked as uninstalled the first time the frontend requests information about deployed plugins. - protected get ignoredPlugins(): Set { - if (!this._ignoredPlugins) { - this._ignoredPlugins = new Set(this.uninstallationManager.getUninstalledPluginIds()); - } - return this._ignoredPlugins; - } + protected uninstalledPlugins: Set; + protected disabledPlugins: Set; protected readonly pluginVersions = new Map(); + protected readonly initialized = new Deferred(); + constructor( @inject(HostedPluginSupport) private readonly hostedPlugin: HostedPluginSupport) { } @@ -80,21 +79,40 @@ export class HostedPluginServerImpl implements HostedPluginServer { this.backendPluginHostableFilter = () => true; } - this.toDispose.pushAll([ - this.pluginDeployer.onDidDeploy(() => this.client?.onDidDeploy()), - this.uninstallationManager.onDidChangeUninstalledPlugins(currentUninstalled => { - if (this._ignoredPlugins) { - const uninstalled = new Set(currentUninstalled); - for (const previouslyUninstalled of this._ignoredPlugins) { - if (!uninstalled.has(previouslyUninstalled)) { - this._ignoredPlugins.delete(previouslyUninstalled); + this.uninstalledPlugins = new Set(this.uninstallationManager.getUninstalledPluginIds()); + + const asyncInit = async () => { + this.disabledPlugins = new Set(await this.uninstallationManager.getDisabledPluginIds()); + + this.toDispose.pushAll([ + this.pluginDeployer.onDidDeploy(() => this.client?.onDidDeploy()), + this.uninstallationManager.onDidChangeUninstalledPlugins(currentUninstalled => { + if (this.uninstalledPlugins) { + const uninstalled = new Set(currentUninstalled); + for (const previouslyUninstalled of this.uninstalledPlugins) { + if (!uninstalled.has(previouslyUninstalled)) { + this.uninstalledPlugins.delete(previouslyUninstalled); + } } } - } - this.client?.onDidDeploy(); - }), - Disposable.create(() => this.hostedPlugin.clientClosed()), - ]); + this.client?.onDidDeploy(); + }), + this.uninstallationManager.onDidChangeDisabledPlugins(currentlyDisabled => { + if (this.disabledPlugins) { + const disabled = new Set(currentlyDisabled); + for (const previouslyUninstalled of this.disabledPlugins) { + if (!disabled.has(previouslyUninstalled)) { + this.disabledPlugins.delete(previouslyUninstalled); + } + } + } + this.client?.onDidDeploy(); + }), + Disposable.create(() => this.hostedPlugin.clientClosed()), + ]); + this.initialized.resolve(); + }; + asyncInit(); } protected getServerName(): string { @@ -111,6 +129,7 @@ export class HostedPluginServerImpl implements HostedPluginServer { } async getDeployedPluginIds(): Promise { + await this.initialized.promise; const backendPlugins = (await this.deployerHandler.getDeployedBackendPlugins()) .filter(this.backendPluginHostableFilter); if (backendPlugins.length > 0) { @@ -126,7 +145,6 @@ export class HostedPluginServerImpl implements HostedPluginServer { }; addIds(await this.deployerHandler.getDeployedFrontendPluginIds()); addIds(await this.deployerHandler.getDeployedBackendPluginIds()); - addIds(await this.hostedPlugin.getExtraDeployedPluginIds()); return Array.from(plugins); } @@ -146,7 +164,11 @@ export class HostedPluginServerImpl implements HostedPluginServer { if (knownVersion !== undefined && knownVersion !== versionAndId.version) { return false; } - if (this.ignoredPlugins.has(identifier)) { + if (this.uninstalledPlugins.has(identifier)) { + return false; + } + + if (this.disabledPlugins.has(identifier)) { return false; } if (knownVersion === undefined) { @@ -159,26 +181,18 @@ export class HostedPluginServerImpl implements HostedPluginServer { return Promise.resolve(this.uninstallationManager.getUninstalledPluginIds()); } - async getDeployedPlugins({ pluginIds }: GetDeployedPluginsParams): Promise { + getDisabledPluginIds(): Promise { + return Promise.resolve(this.uninstallationManager.getDisabledPluginIds()); + } + + async getDeployedPlugins(pluginIds: PluginIdentifiers.VersionedId[]): Promise { if (!pluginIds.length) { return []; } const plugins: DeployedPlugin[] = []; - let extraDeployedPlugins: Map | undefined; for (const versionedId of pluginIds) { - if (!this.isRelevantPlugin(versionedId)) { - continue; - } - let plugin = this.deployerHandler.getDeployedPlugin(versionedId); - if (!plugin) { - if (!extraDeployedPlugins) { - extraDeployedPlugins = new Map(); - for (const extraDeployedPlugin of await this.hostedPlugin.getExtraDeployedPlugins()) { - extraDeployedPlugins.set(PluginIdentifiers.componentsToVersionedId(extraDeployedPlugin.metadata.model), extraDeployedPlugin); - } - } - plugin = extraDeployedPlugins.get(versionedId); - } + const plugin = this.deployerHandler.getDeployedPlugin(versionedId); + if (plugin) { plugins.push(plugin); } diff --git a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts index ae3823e2a7c01..5ee29c9cb39ad 100644 --- a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts @@ -121,7 +121,8 @@ export class MenusContributionPointHandler { } } } catch (error) { - console.warn(`Failed to register a menu item for plugin ${plugin.metadata.model.id} contributed to ${contributionPoint}`, item, error); + console.warn(`Failed to register a menu item for plugin ${plugin.metadata.model.id} contributed to ${contributionPoint}`, item); + console.debug(error); } } } diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts index 4263a835cb4f9..275cdb56f4006 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-impl.ts @@ -141,6 +141,14 @@ export class PluginDeployerImpl implements PluginDeployer { await this.pluginDeployerHandler.uninstallPlugin(pluginId); } + enablePlugin(pluginId: PluginIdentifiers.VersionedId): Promise { + return this.pluginDeployerHandler.enablePlugin(pluginId); + } + + disablePlugin(pluginId: PluginIdentifiers.VersionedId): Promise { + return this.pluginDeployerHandler.disablePlugin(pluginId); + } + async undeploy(pluginId: PluginIdentifiers.VersionedId): Promise { if (await this.pluginDeployerHandler.undeployPlugin(pluginId)) { this.onDidDeployEmitter.fire(); diff --git a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts index fdb6825dc519a..6cc422f775a5c 100644 --- a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts +++ b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts @@ -33,7 +33,7 @@ import { HttpPluginDeployerResolver } from './plugin-http-resolver'; import { ConnectionHandler, RpcConnectionHandler, bindContributionProvider } from '@theia/core'; import { PluginPathsService, pluginPathsServicePath } from '../common/plugin-paths-protocol'; import { PluginPathsServiceImpl } from './paths/plugin-paths-service'; -import { PluginServerHandler } from './plugin-server-handler'; +import { PluginServerImpl } from './plugin-server-impl'; import { PluginCliContribution } from './plugin-cli-contribution'; import { PluginTheiaEnvironment } from '../common/plugin-theia-environment'; import { PluginTheiaDeployerParticipant } from './plugin-theia-deployer-participant'; @@ -70,7 +70,7 @@ export function bindMainBackend(bind: interfaces.Bind, unbind: interfaces.Unbind bind(PluginDeployerFileHandler).to(PluginTheiaFileHandler).inSingletonScope(); bind(PluginDeployerDirectoryHandler).to(PluginTheiaDirectoryHandler).inSingletonScope(); - bind(PluginServer).to(PluginServerHandler).inSingletonScope(); + bind(PluginServer).to(PluginServerImpl).inSingletonScope(); bind(PluginsKeyValueStorage).toSelf().inSingletonScope(); diff --git a/packages/plugin-ext/src/main/node/plugin-mgmt-cli-contribution.ts b/packages/plugin-ext/src/main/node/plugin-mgmt-cli-contribution.ts index 098da7600b9eb..41a582bff0434 100644 --- a/packages/plugin-ext/src/main/node/plugin-mgmt-cli-contribution.ts +++ b/packages/plugin-ext/src/main/node/plugin-mgmt-cli-contribution.ts @@ -17,7 +17,7 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { Argv, Arguments } from '@theia/core/shared/yargs'; import { CliContribution } from '@theia/core/lib/node/cli'; -import { HostedPluginDeployerHandler } from '../../hosted/node/hosted-plugin-deployer-handler'; +import { PluginDeployerHandlerImpl } from '../../hosted/node/plugin-deployer-handler-impl'; import { PluginType } from '../../common'; @injectable() @@ -27,8 +27,8 @@ export class PluginMgmtCliContribution implements CliContribution { static SHOW_VERSIONS = '--show-versions'; static SHOW_BUILTINS = '--show-builtins'; - @inject(HostedPluginDeployerHandler) - protected deployerHandler: HostedPluginDeployerHandler; + @inject(PluginDeployerHandlerImpl) + protected deployerHandler: PluginDeployerHandlerImpl; configure(conf: Argv): void { conf.command([PluginMgmtCliContribution.LIST_PLUGINS, 'list-extensions'], diff --git a/packages/plugin-ext/src/main/node/plugin-server-handler.ts b/packages/plugin-ext/src/main/node/plugin-server-impl.ts similarity index 60% rename from packages/plugin-ext/src/main/node/plugin-server-handler.ts rename to packages/plugin-ext/src/main/node/plugin-server-impl.ts index a40df614b9b7c..2ae2c79699c5e 100644 --- a/packages/plugin-ext/src/main/node/plugin-server-handler.ts +++ b/packages/plugin-ext/src/main/node/plugin-server-impl.ts @@ -18,21 +18,31 @@ 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, UnresolvedPluginEntry, PluginIdentifiers, PluginDeployOptions } from '../../common/plugin-protocol'; +import { + PluginServer, PluginDeployer, PluginStorageKind, PluginType, UnresolvedPluginEntry, PluginIdentifiers, + PluginDeployOptions, PluginDeployerHandler +} from '../../common/plugin-protocol'; import { KeysToAnyValues, KeysToKeysToAnyValue } from '../../common/types'; +import { PluginUninstallationManager } from './plugin-uninstallation-manager'; @injectable() -export class PluginServerHandler implements PluginServer { +export class PluginServerImpl implements PluginServer { @inject(PluginDeployer) protected readonly pluginDeployer: PluginDeployerImpl; + @inject(PluginDeployerHandler) + protected readonly pluginDeployerHandler: PluginDeployerHandler; + @inject(PluginsKeyValueStorage) protected readonly pluginsKeyValueStorage: PluginsKeyValueStorage; - async deploy(pluginEntry: string, arg2?: PluginType | CancellationToken, options?: PluginDeployOptions): Promise { + @inject(PluginUninstallationManager) + protected readonly uninstallationManager: PluginUninstallationManager; + + async install(pluginEntry: string, arg2?: PluginType | CancellationToken, options?: PluginDeployOptions): Promise { const type = typeof arg2 === 'number' ? arg2 as PluginType : undefined; - const successfulDeployments = await this.doDeploy({ + const successfulDeployments = await this.doInstall({ id: pluginEntry, type: type ?? PluginType.User }, options); @@ -42,16 +52,32 @@ export class PluginServerHandler implements PluginServer { } } - protected doDeploy(pluginEntry: UnresolvedPluginEntry, options?: PluginDeployOptions): Promise { + protected doInstall(pluginEntry: UnresolvedPluginEntry, options?: PluginDeployOptions): Promise { return this.pluginDeployer.deploy(pluginEntry, options); } + getInstalledPlugins(): Promise { + return Promise.resolve(this.pluginDeployerHandler.getDeployedPluginIds()); + } + + getUninstalledPlugins(): Promise { + return Promise.resolve(this.uninstallationManager.getUninstalledPluginIds()); + } + + getDisabledPlugins(): Promise { + return Promise.resolve(this.uninstallationManager.getDisabledPluginIds()); + } + uninstall(pluginId: PluginIdentifiers.VersionedId): Promise { return this.pluginDeployer.uninstall(pluginId); } - undeploy(pluginId: PluginIdentifiers.VersionedId): Promise { - return this.pluginDeployer.undeploy(pluginId); + enablePlugin(pluginId: PluginIdentifiers.VersionedId): Promise { + return this.pluginDeployer.enablePlugin(pluginId); + } + + disablePlugin(pluginId: PluginIdentifiers.VersionedId): Promise { + return this.pluginDeployer.disablePlugin(pluginId); } setStorageValue(key: string, value: KeysToAnyValues, kind: PluginStorageKind): Promise { diff --git a/packages/plugin-ext/src/main/node/plugin-uninstallation-manager.ts b/packages/plugin-ext/src/main/node/plugin-uninstallation-manager.ts index dfba80f9d5133..f6f3c7fedfdeb 100644 --- a/packages/plugin-ext/src/main/node/plugin-uninstallation-manager.ts +++ b/packages/plugin-ext/src/main/node/plugin-uninstallation-manager.ts @@ -15,60 +15,118 @@ // ***************************************************************************** import { Emitter, Event } from '@theia/core'; -import { injectable } from '@theia/core/shared/inversify'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { PluginIdentifiers } from '../../common'; +import { SettingService } from '@theia/core/lib/node'; +import { Deferred } from '@theia/core/lib/common/promise-util'; @injectable() export class PluginUninstallationManager { + static DISABLED_PLUGINS = 'installedPlugins.disabledPlugins'; + + @inject(SettingService) + protected readonly settingService: SettingService; + protected readonly onDidChangeUninstalledPluginsEmitter = new Emitter(); + onDidChangeUninstalledPlugins: Event = this.onDidChangeUninstalledPluginsEmitter.event; + + protected readonly onDidChangeDisabledPluginsEmitter = new Emitter(); + onDidChangeDisabledPlugins: Event = this.onDidChangeDisabledPluginsEmitter.event; - get onDidChangeUninstalledPlugins(): Event { - return this.onDidChangeUninstalledPluginsEmitter.event; + protected uninstalledPlugins: Set = new Set(); + protected disabledPlugins: Set = new Set(); + + protected readonly initialized = new Deferred(); + + @postConstruct() + init(): void { + this.load().then(() => this.initialized.resolve()); } - protected uninstalledPlugins: PluginIdentifiers.VersionedId[] = []; + protected async load(): Promise { + try { + const disabled: PluginIdentifiers.VersionedId[] = JSON.parse(await this.settingService.get(PluginUninstallationManager.DISABLED_PLUGINS) || '[]'); + disabled.forEach(id => this.disabledPlugins.add(id)); + } catch (e) { + // settings may be corrupt; just carry on + console.warn(e); + } + } - protected fireDidChange(): void { - this.onDidChangeUninstalledPluginsEmitter.fire(Object.freeze(this.uninstalledPlugins.slice())); + protected async save(): Promise { + await this.settingService.set(PluginUninstallationManager.DISABLED_PLUGINS, JSON.stringify(await this.getDisabledPluginIds())); } - markAsUninstalled(...pluginIds: PluginIdentifiers.VersionedId[]): boolean { + async markAsUninstalled(...pluginIds: PluginIdentifiers.VersionedId[]): Promise { let didChange = false; - for (const id of pluginIds) { didChange = this.markOneAsUninstalled(id) || didChange; } - if (didChange) { this.fireDidChange(); } + for (const id of pluginIds) { + if (!this.uninstalledPlugins.has(id)) { + didChange = true; + this.uninstalledPlugins.add(id); + } + } + if (didChange) { + this.onDidChangeUninstalledPluginsEmitter.fire(this.getUninstalledPluginIds()); + } + this.markAsEnabled(...pluginIds); return didChange; } - protected markOneAsUninstalled(pluginId: PluginIdentifiers.VersionedId): boolean { - if (!this.uninstalledPlugins.includes(pluginId)) { - this.uninstalledPlugins.push(pluginId); - return true; + async markAsInstalled(...pluginIds: PluginIdentifiers.VersionedId[]): Promise { + let didChange = false; + for (const id of pluginIds) { + didChange = this.uninstalledPlugins.delete(id) || didChange; + } + if (didChange) { + this.onDidChangeUninstalledPluginsEmitter.fire(this.getUninstalledPluginIds()); } - return false; + return didChange; } - markAsInstalled(...pluginIds: PluginIdentifiers.VersionedId[]): boolean { + isUninstalled(pluginId: PluginIdentifiers.VersionedId): boolean { + return this.uninstalledPlugins.has(pluginId); + } + + getUninstalledPluginIds(): readonly PluginIdentifiers.VersionedId[] { + return [...this.uninstalledPlugins]; + } + + async markAsDisabled(...pluginIds: PluginIdentifiers.VersionedId[]): Promise { + await this.initialized.promise; let didChange = false; - for (const id of pluginIds) { didChange = this.markOneAsInstalled(id) || didChange; } - if (didChange) { this.fireDidChange(); } + for (const id of pluginIds) { + if (!this.disabledPlugins.has(id)) { + this.disabledPlugins.add(id); + didChange = true; + } + } + if (didChange) { + await this.save(); + this.onDidChangeDisabledPluginsEmitter.fire([...this.disabledPlugins]); + } return didChange; } - protected markOneAsInstalled(pluginId: PluginIdentifiers.VersionedId): boolean { - let index: number; + async markAsEnabled(...pluginIds: PluginIdentifiers.VersionedId[]): Promise { + await this.initialized.promise; let didChange = false; - while ((index = this.uninstalledPlugins.indexOf(pluginId)) !== -1) { - this.uninstalledPlugins.splice(index, 1); - didChange = true; + for (const id of pluginIds) { + didChange = this.disabledPlugins.delete(id) || didChange; + } + if (didChange) { + await this.save(); + this.onDidChangeDisabledPluginsEmitter.fire([...this.disabledPlugins]); } return didChange; } - isUninstalled(pluginId: PluginIdentifiers.VersionedId): boolean { - return this.uninstalledPlugins.includes(pluginId); + async isDisabled(pluginId: PluginIdentifiers.VersionedId): Promise { + await this.initialized.promise; + return this.disabledPlugins.has(pluginId); } - getUninstalledPluginIds(): readonly PluginIdentifiers.VersionedId[] { - return Object.freeze(this.uninstalledPlugins.slice()); + async getDisabledPluginIds(): Promise { + await this.initialized.promise; + return [...this.disabledPlugins]; } } diff --git a/packages/vsx-registry/src/browser/style/index.css b/packages/vsx-registry/src/browser/style/index.css index ef05e93adaec3..8d7dbce45eaa4 100644 --- a/packages/vsx-registry/src/browser/style/index.css +++ b/packages/vsx-registry/src/browser/style/index.css @@ -16,9 +16,7 @@ :root { --theia-vsx-extension-icon-size: calc(var(--theia-ui-icon-font-size) * 3); - --theia-vsx-extension-editor-icon-size: calc( - var(--theia-vsx-extension-icon-size) * 3 - ); + --theia-vsx-extension-editor-icon-size: calc(var(--theia-vsx-extension-icon-size) * 3); } .vsx-search-container { @@ -76,14 +74,14 @@ } .theia-vsx-extension, -.theia-vsx-extensions-view-container .part > .body { +.theia-vsx-extensions-view-container .part>.body { min-height: calc(var(--theia-content-line-height) * 3); } .theia-vsx-extensions-search-bar { - padding: var(--theia-ui-padding) var(--theia-scrollbar-width) - var(--theia-ui-padding) 18px - /* expansion toggle padding of tree elements in result list */; + padding: var(--theia-ui-padding) var(--theia-scrollbar-width) var(--theia-ui-padding) 18px + /* expansion toggle padding of tree elements in result list */ + ; display: flex; align-content: center; } @@ -100,6 +98,7 @@ border: none; outline: none; } + .theia-vsx-extension { display: flex; flex-direction: row; @@ -125,9 +124,7 @@ .theia-vsx-extension-content { display: flex; flex-direction: column; - width: calc( - 100% - var(--theia-vsx-extension-icon-size) - var(--theia-ui-padding) * 2.5 - ); + width: calc(100% - var(--theia-vsx-extension-icon-size) - var(--theia-ui-padding) * 2.5); } .theia-vsx-extension-content .title { @@ -142,6 +139,10 @@ font-weight: bold; } +.theia-vsx-extension-content .disabled { + font-weight: bold; +} + .theia-vsx-extension-content .title .version, .theia-vsx-extension-content .title .stat { opacity: 0.85; @@ -158,13 +159,13 @@ align-items: center; } -.theia-vsx-extension-content .title .stat .average-rating > i { +.theia-vsx-extension-content .title .stat .average-rating>i { color: #ff8e00; } -.theia-vsx-extension-editor .download-count > i, -.theia-vsx-extension-content .title .stat .average-rating > i, -.theia-vsx-extension-content .title .stat .download-count > i { +.theia-vsx-extension-editor .download-count>i, +.theia-vsx-extension-content .title .stat .average-rating>i, +.theia-vsx-extension-content .title .stat .download-count>i { padding-right: calc(var(--theia-ui-padding) / 2); } @@ -220,8 +221,7 @@ .theia-vsx-extension-editor .header { display: flex; - padding: calc(var(--theia-ui-padding) * 3) calc(var(--theia-ui-padding) * 3) - calc(var(--theia-ui-padding) * 3); + padding: calc(var(--theia-ui-padding) * 3) calc(var(--theia-ui-padding) * 3) calc(var(--theia-ui-padding) * 3); flex-shrink: 0; border-bottom: 1px solid hsla(0, 0%, 50%, 0.5); width: 100%; @@ -262,19 +262,19 @@ border-collapse: collapse; } -.theia-vsx-extension-editor .body table > thead > tr > th { +.theia-vsx-extension-editor .body table>thead>tr>th { text-align: left; border-bottom: 1px solid var(--theia-extensionEditor-tableHeadBorder); } -.theia-vsx-extension-editor .body table > thead > tr > th, -.theia-vsx-extension-editor .body table > thead > tr > td, -.theia-vsx-extension-editor .body table > tbody > tr > th, -.theia-vsx-extension-editor .body table > tbody > tr > td { +.theia-vsx-extension-editor .body table>thead>tr>th, +.theia-vsx-extension-editor .body table>thead>tr>td, +.theia-vsx-extension-editor .body table>tbody>tr>th, +.theia-vsx-extension-editor .body table>tbody>tr>td { padding: 5px 10px; } -.theia-vsx-extension-editor .body table > tbody > tr + tr > td { +.theia-vsx-extension-editor .body table>tbody>tr+tr>td { border-top: 1px solid var(--theia-extensionEditor-tableCellBorder); } @@ -364,7 +364,7 @@ white-space: nowrap; } -.theia-vsx-extension-editor .header .details .subtitle > span { +.theia-vsx-extension-editor .header .details .subtitle>span { display: flex; align-items: center; cursor: pointer; @@ -372,11 +372,7 @@ height: var(--theia-content-line-height); } -.theia-vsx-extension-editor - .header - .details - .subtitle - > span:not(:first-child):not(:empty) { +.theia-vsx-extension-editor .header .details .subtitle>span:not(:first-child):not(:empty) { border-left: 1px solid hsla(0, 0%, 50%, 0.7); padding-left: calc(var(--theia-ui-padding) * 2); margin-left: calc(var(--theia-ui-padding) * 2); @@ -387,26 +383,16 @@ font-size: var(--theia-ui-font-size3); } -.theia-vsx-extension-editor - .header - .details - .subtitle - .publisher - .namespace-access, +.theia-vsx-extension-editor .header .details .subtitle .publisher .namespace-access, .theia-vsx-extension-editor .header .details .subtitle .download-count::before { padding-right: var(--theia-ui-padding); } -.theia-vsx-extension-editor .header .details .subtitle .average-rating > i { +.theia-vsx-extension-editor .header .details .subtitle .average-rating>i { color: #ff8e00; } -.theia-vsx-extension-editor - .header - .details - .subtitle - .average-rating - > i:not(:first-child) { +.theia-vsx-extension-editor .header .details .subtitle .average-rating>i:not(:first-child) { padding-left: calc(var(--theia-ui-padding) / 2); } diff --git a/packages/vsx-registry/src/browser/vsx-extension-commands.ts b/packages/vsx-registry/src/browser/vsx-extension-commands.ts index 091563201bca9..d18a0a93170d8 100644 --- a/packages/vsx-registry/src/browser/vsx-extension-commands.ts +++ b/packages/vsx-registry/src/browser/vsx-extension-commands.ts @@ -44,6 +44,14 @@ export namespace VSXExtensionsCommands { export const INSTALL_ANOTHER_VERSION: Command = { id: 'vsxExtensions.installAnotherVersion' }; + + export const DISABLE: Command = { + id: 'vsxExtensions.disable' + }; + + export const ENABLE: Command = { + id: 'vsxExtensions.enable' + }; export const COPY: Command = { id: 'vsxExtensions.copy' }; diff --git a/packages/vsx-registry/src/browser/vsx-extension-editor-manager.ts b/packages/vsx-registry/src/browser/vsx-extension-editor-manager.ts index 9bc33c16fafe0..a4a14eff67dc8 100644 --- a/packages/vsx-registry/src/browser/vsx-extension-editor-manager.ts +++ b/packages/vsx-registry/src/browser/vsx-extension-editor-manager.ts @@ -17,7 +17,6 @@ import { injectable } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import { WidgetOpenHandler } from '@theia/core/lib/browser'; -import { VSXExtensionOptions } from './vsx-extension'; import { VSCodeExtensionUri } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-uri'; import { VSXExtensionEditor } from './vsx-extension-editor'; @@ -31,7 +30,7 @@ export class VSXExtensionEditorManager extends WidgetOpenHandler): void { @@ -298,9 +323,23 @@ export class VSXExtension implements VSXExtensionData, TreeElement { return md; } - protected _busy = 0; - get busy(): boolean { - return !!this._busy; + protected _currentTaskName: string | undefined; + get currentTask(): string | undefined { + return this._currentTaskName; + } + protected _currentTask: Promise | undefined; + + protected runTask(name: string, task: () => Promise): Promise { + if (this._currentTask) { + return Promise.reject('busy'); + } + this._currentTaskName = name; + this._currentTask = task(); + this._currentTask.finally(() => { + this._currentTask = undefined; + this._currentTaskName = undefined; + }); + return this._currentTask; } async install(options?: PluginDeployOptions): Promise { @@ -310,39 +349,56 @@ export class VSXExtension implements VSXExtensionData, TreeElement { msg: nls.localize('theia/vsx-registry/confirmDialogMessage', 'The extension "{0}" is unverified and might pose a security risk.', this.displayName) }).open(); if (choice) { - this.doInstall(options); + await this.doInstall(options); } } else { - this.doInstall(options); + await this.doInstall(options); } } async uninstall(): Promise { - this._busy++; - try { - const { plugin } = this; - if (plugin) { - await this.progressService.withProgress( + const { id, installedVersion } = this; + if (id && installedVersion) { + await this.runTask(nls.localizeByDefault('Uninstalling'), + async () => await this.progressService.withProgress( nls.localizeByDefault('Uninstalling {0}...', this.id), 'extensions', - () => this.pluginServer.uninstall(PluginIdentifiers.componentsToVersionedId(plugin.metadata.model)) + () => this.pluginServer.uninstall(PluginIdentifiers.idAndVersionToVersionedId({ id: (id as PluginIdentifiers.UnversionedId), version: installedVersion })) + ) + ); + } + } + + async disable(): Promise { + const { id, installedVersion } = this; + if (id && installedVersion) { + await this.runTask(nls.localize('vsx.disabling', 'Disabling'), async () => { + await this.progressService.withProgress( + nls.localize('vsx.disabling.extensions', 'Disabling {0}...', this.id), 'extensions', + () => this.pluginServer.disablePlugin(PluginIdentifiers.idAndVersionToVersionedId({ id: (id as PluginIdentifiers.UnversionedId), version: installedVersion })) ); - } - } finally { - this._busy--; + }); } } - protected async doInstall(options?: PluginDeployOptions): Promise { - this._busy++; - try { - await this.progressService.withProgress(nls.localizeByDefault("Installing extension '{0}' v{1}...", this.id, this.version ?? 0), 'extensions', () => - this.pluginServer.deploy(this.uri.toString(), undefined, options) - ); - } finally { - this._busy--; + async enable(): Promise { + const { id, installedVersion } = this; + if (id && installedVersion) { + await this.runTask(nls.localize('vsx.enabling', 'Enabling'), async () => { + await this.progressService.withProgress( + nls.localize('vsx.enabling.extension', 'Enabling {0}...', this.id), 'extensions', + () => this.pluginServer.enablePlugin(PluginIdentifiers.idAndVersionToVersionedId({ id: (id as PluginIdentifiers.UnversionedId), version: installedVersion })) + ); + }); } } + protected async doInstall(options?: PluginDeployOptions): Promise { + await this.runTask(nls.localizeByDefault('Installing'), + () => this.progressService.withProgress(nls.localizeByDefault("Installing extension '{0}' v{1}...", this.id, this.version ?? 0), 'extensions', () => + this.pluginServer.install(this.uri.toString(), undefined, options) + )); + } + handleContextMenu(e: React.MouseEvent): void { e.preventDefault(); this.contextMenuRenderer.render({ @@ -435,32 +491,24 @@ export abstract class AbstractVSXExtensionComponent; + const outOfSync = installed && (deployed ? (disabled || uninstalled) : !(disabled || uninstalled)); + if (currentTask) { + return ; } - if (busy) { - if (installed) { - return ; + return
+ { + outOfSync && } - return ; - } - if (installed) { - return
- { - outOfSynch - ? - : - } - -
-
; - } - return ; + { + !builtin && ((installed && !uninstalled) ? + : + ) + } +
+
; } } @@ -486,7 +534,7 @@ export namespace VSXExtensionComponent { export class VSXExtensionComponent extends AbstractVSXExtensionComponent { override render(): React.ReactNode { - const { iconUrl, publisher, displayName, description, version, downloadCount, averageRating, tooltip, verified } = this.props.extension; + const { iconUrl, publisher, displayName, description, version, downloadCount, averageRating, tooltip, verified, disabled } = this.props.extension; return
- {displayName} {VSXExtension.formatVersion(version)} + {displayName}  + {VSXExtension.formatVersion(version)}  + {disabled && ({nls.localizeByDefault('disabled')})}
{!!downloadCount && {downloadCompactFormatter.format(downloadCount)}} @@ -517,6 +567,7 @@ export class VSXExtensionComponent
{description}
+
{verified === true ? ( diff --git a/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts b/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts index f7ebc5f95a616..08782d516a2d5 100644 --- a/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts +++ b/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts @@ -114,6 +114,18 @@ export class VSXExtensionsContribution extends AbstractViewContribution this.installAnotherVersion(extension), }); + commands.registerCommand(VSXExtensionsCommands.DISABLE, { + isVisible: (extension: VSXExtension) => extension.installed && !extension.disabled, + isEnabled: (extension: VSXExtension) => extension.installed && !extension.disabled, + execute: async (extension: VSXExtension) => extension.disable(), + }); + + commands.registerCommand(VSXExtensionsCommands.ENABLE, { + isVisible: (extension: VSXExtension) => extension.installed && extension.disabled, + isEnabled: (extension: VSXExtension) => extension.installed && extension.disabled, + execute: async (extension: VSXExtension) => extension.enable(), + }); + commands.registerCommand(VSXExtensionsCommands.COPY, { execute: (extension: VSXExtension) => this.copy(extension) }); @@ -152,6 +164,15 @@ export class VSXExtensionsContribution extends AbstractViewContribution; /** * Single source for all extensions */ protected readonly extensions = new Map(); protected readonly onDidChangeEmitter = new Emitter(); - protected _installed = new Set(); + protected disabled = new Set(); + protected uninstalled = new Set(); + protected deployed = new Set(); + protected _installed = new Set(); protected _recommended = new Set(); protected _searchResult = new Set(); + protected builtins = new Set(); protected _searchError?: string; protected searchCancellationTokenSource = new CancellationTokenSource(); @@ -61,6 +66,12 @@ export class VSXExtensionsModel { @inject(HostedPluginSupport) protected readonly pluginSupport: HostedPluginSupport; + @inject(HostedPluginWatcher) + protected pluginWatcher: HostedPluginWatcher; + + @inject(HostedPluginServer) + protected readonly pluginServer: HostedPluginServer; + @inject(VSXExtensionFactory) protected readonly extensionFactory: VSXExtensionFactory; @@ -128,8 +139,24 @@ export class VSXExtensionsModel { this.updateSearchResult(); } + isBuiltIn(id: string): boolean { + return this.builtins.has(id as PluginIdentifiers.UnversionedId); + } + isInstalled(id: string): boolean { - return this._installed.has(id); + return this._installed.has(id as PluginIdentifiers.UnversionedId); + } + + isUninstalled(id: string): boolean { + return this.uninstalled.has(id as PluginIdentifiers.UnversionedId); + } + + isDeployed(id: string): boolean { + return this.deployed.has(id as PluginIdentifiers.UnversionedId); + } + + isDisabled(id: string): boolean { + return this.disabled.has(id as PluginIdentifiers.UnversionedId); } getExtension(id: string): VSXExtension | undefined { @@ -187,12 +214,15 @@ export class VSXExtensionsModel { protected async initInstalled(): Promise { await this.pluginSupport.willStart; - this.pluginSupport.onDidChangePlugins(() => this.updateInstalled()); try { await this.updateInstalled(); } catch (e) { console.error(e); } + + this.pluginWatcher.onDidDeploy(() => { + this.updateInstalled(); + }); } protected async initSearchResult(): Promise { @@ -223,10 +253,10 @@ export class VSXExtensionsModel { return this.searchCancellationTokenSource = new CancellationTokenSource(); } - protected setExtension(id: string): VSXExtension { + protected setExtension(id: string, version?: string): VSXExtension { let extension = this.extensions.get(id); if (!extension) { - extension = this.extensionFactory({ id }); + extension = this.extensionFactory({ id, version, model: this }); this.extensions.set(id, extension); } return extension; @@ -328,19 +358,41 @@ export class VSXExtensionsModel { } protected async updateInstalled(): Promise { + const [deployed, uninstalled, disabled] = await Promise.all( + [this.pluginServer.getDeployedPluginIds(), this.pluginServer.getUninstalledPluginIds(), this.pluginServer.getDisabledPluginIds()]); + + this.uninstalled = new Set(); + uninstalled.forEach(id => this.uninstalled.add(PluginIdentifiers.unversionedFromVersioned(id))); + this.disabled = new Set(); + disabled.forEach(id => this.disabled.add(PluginIdentifiers.unversionedFromVersioned(id))); + this.deployed = new Set(); + deployed.forEach(id => this.deployed.add(PluginIdentifiers.unversionedFromVersioned(id))); + const prevInstalled = this._installed; + const installedVersioned = new Set(); return this.doChange(async () => { - const plugins = this.pluginSupport.plugins; - const currInstalled = new Set(); + const currInstalled = new Set(); const refreshing = []; - for (const plugin of plugins) { - if (plugin.model.engine.type === 'vscode') { - const version = plugin.model.version; - const id = plugin.model.id; - this._installed.delete(id); - const extension = this.setExtension(id); - currInstalled.add(extension.id); - refreshing.push(this.refresh(id, version)); + for (const versionedId of deployed) { + installedVersioned.add(versionedId); + const idAndVersion = PluginIdentifiers.idAndVersionFromVersionedId(versionedId); + if (idAndVersion) { + this._installed.delete(idAndVersion.id); + this.setExtension(idAndVersion.id, idAndVersion.version); + currInstalled.add(idAndVersion.id); + refreshing.push(this.refresh(idAndVersion.id, idAndVersion.version)); + } + } + for (const versionedId of disabled) { + const idAndVersion = PluginIdentifiers.idAndVersionFromVersionedId(versionedId); + installedVersioned.add(versionedId); + if (idAndVersion && !this.isUninstalled(idAndVersion.id)) { + if (!currInstalled.has(idAndVersion.id)) { + this._installed.delete(idAndVersion.id); + this.setExtension(idAndVersion.id, idAndVersion.version); + currInstalled.add(idAndVersion.id); + refreshing.push(this.refresh(idAndVersion.id, idAndVersion.version)); + } } } for (const id of this._installed) { @@ -348,10 +400,33 @@ export class VSXExtensionsModel { if (!extension) { continue; } refreshing.push(this.refresh(id, extension.version)); } + await Promise.all(refreshing); const installed = new Set([...prevInstalled, ...currInstalled]); const installedSorted = Array.from(installed).sort((a, b) => this.compareExtensions(a, b)); this._installed = new Set(installedSorted.values()); - await Promise.all(refreshing); + + const missingIds = new Set(); + for (const id of installedVersioned) { + const unversionedId = PluginIdentifiers.unversionedFromVersioned(id); + const plugin = this.pluginSupport.getPlugin(unversionedId); + if (plugin) { + if (plugin.type === PluginType.System) { + this.builtins.add(unversionedId); + } else { + this.builtins.delete(unversionedId); + } + } else { + missingIds.add(id); + } + } + const missing = await this.pluginServer.getDeployedPlugins([...missingIds.values()]); + for (const plugin of missing) { + if (plugin.type === PluginType.System) { + this.builtins.add(PluginIdentifiers.componentsToUnversionedId(plugin.metadata.model)); + } else { + this.builtins.delete(PluginIdentifiers.componentsToUnversionedId(plugin.metadata.model)); + } + } }); } @@ -362,7 +437,7 @@ export class VSXExtensionsModel { const updateRecommendationsForScope = (scope: PreferenceInspectionScope, root?: URI) => { const { recommendations, unwantedRecommendations } = this.getRecommendationsForScope(scope, root); - recommendations.forEach(recommendation => allRecommendations.add(recommendation)); + recommendations.forEach(recommendation => allRecommendations.add(recommendation.toLowerCase())); unwantedRecommendations.forEach(unwantedRecommendation => allUnwantedRecommendations.add(unwantedRecommendation)); }; @@ -449,15 +524,12 @@ export class VSXExtensionsModel { * @param extension the extension to refresh. */ protected shouldRefresh(extension?: VSXExtension): boolean { - if (extension === undefined) { - return true; - } - return !extension.builtin; + return extension === undefined || extension.plugin === undefined; } protected onDidFailRefresh(id: string, error: unknown): VSXExtension | undefined { const cached = this.getExtension(id); - if (cached && cached.installed) { + if (cached && cached.deployed) { return cached; } console.error(`[${id}]: failed to refresh, reason:`, error); diff --git a/packages/vsx-registry/src/browser/vsx-language-quick-pick-service.ts b/packages/vsx-registry/src/browser/vsx-language-quick-pick-service.ts index 717a35d1fe048..9c0b90311acde 100644 --- a/packages/vsx-registry/src/browser/vsx-language-quick-pick-service.ts +++ b/packages/vsx-registry/src/browser/vsx-language-quick-pick-service.ts @@ -71,7 +71,7 @@ export class VSXLanguageQuickPickService extends LanguageQuickPickService { }); try { const extensionUri = VSCodeExtensionUri.fromId(`${extension.extension.namespace}.${extension.extension.name}`).toString(); - await this.pluginServer.deploy(extensionUri); + await this.pluginServer.install(extensionUri); } finally { progress.cancel(); }