From 14df86f3e4a032ef83ccb428895562b58cfdd828 Mon Sep 17 00:00:00 2001 From: seantan22 Date: Tue, 2 Mar 2021 10:57:06 -0500 Subject: [PATCH] VSX: Add Toolbar Menu and Support 'Install from VSIX...' Command What it does - Adds an `Install from VSIX...` command that supports installation of extensions from a locally available `.vsix` file. - Adds a '_more_' toolbar menu to the `extensions view` to which additional commands may be registered How to test 1. Visit open-vsx.org and download an extension as a `.vsix` file. 2. Open the `extensions` view. 3. Select the `_more_` menu item in the toolbar and execute the `Install from VSIX...` command. 4. Select a local `.vsix` file in the file dialog and click `Install`. 5. Confirm that the selected extension has been installed by checking the `extensions` view. Signed-off-by: seantan22 --- .../plugin-vscode-commands-contribution.ts | 18 ++-- .../browser/vsx-extensions-contribution.ts | 84 ++++++++++++++++++- 2 files changed, 94 insertions(+), 8 deletions(-) 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 4c38cfaf828e3..f6b4873f86247 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 @@ -45,7 +45,7 @@ import { } from '@theia/plugin-ext/lib/common/plugin-api-rpc-model'; import { DocumentsMainImpl } from '@theia/plugin-ext/lib/main/browser/documents-main'; import { createUntitledURI } from '@theia/plugin-ext/lib/main/browser/editor/untitled-resource'; -import { toDocumentSymbol } from '@theia/plugin-ext/lib/plugin/type-converters'; +import { isUriComponents, toDocumentSymbol } from '@theia/plugin-ext/lib/plugin/type-converters'; import { ViewColumn } from '@theia/plugin-ext/lib/plugin/types-impl'; import { WorkspaceCommands } from '@theia/workspace/lib/browser'; import { WorkspaceService, WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service'; @@ -65,6 +65,7 @@ import { import { FILE_NAVIGATOR_ID, FileNavigatorWidget } from '@theia/navigator/lib/browser'; import { SelectableTreeNode } from '@theia/core/lib/browser/tree/tree-selection'; import { UriComponents } from '@theia/plugin-ext/lib/common/uri-components'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; export namespace VscodeCommands { export const OPEN: Command = { @@ -78,6 +79,10 @@ export namespace VscodeCommands { export const DIFF: Command = { id: 'vscode.diff' }; + + export const INSTALL_FROM_VSIX: Command = { + id: 'workbench.extensions.installExtension' + }; } @injectable() @@ -110,6 +115,8 @@ export class PluginVscodeCommandsContribution implements CommandContribution { protected readonly codeEditorWidgetUtil: CodeEditorWidgetUtil; @inject(PluginServer) protected readonly pluginServer: PluginServer; + @inject(FileService) + protected readonly fileService: FileService; registerCommands(commands: CommandRegistry): void { commands.registerCommand(VscodeCommands.OPEN, { @@ -212,12 +219,13 @@ export class PluginVscodeCommandsContribution implements CommandContribution { commands.registerCommand({ id: 'workbench.action.openSettings' }, { execute: () => commands.executeCommand(CommonCommands.OPEN_PREFERENCES.id) }); - commands.registerCommand({ id: 'workbench.extensions.installExtension' }, { - execute: async (vsixUriOrExtensionId: UriComponents | string) => { + commands.registerCommand({ id: VscodeCommands.INSTALL_FROM_VSIX.id }, { + execute: async (vsixUriOrExtensionId: TheiaURI | UriComponents | string) => { if (typeof vsixUriOrExtensionId === 'string') { - this.pluginServer.deploy(`vscode:extension/${vsixUriOrExtensionId}`); + await this.pluginServer.deploy(`vscode:extension/${vsixUriOrExtensionId}`); } else { - this.pluginServer.deploy(`local-file:${URI.revive(vsixUriOrExtensionId).fsPath}`); + const uriPath = isUriComponents(vsixUriOrExtensionId) ? URI.revive(vsixUriOrExtensionId).fsPath : await this.fileService.fsPath(vsixUriOrExtensionId); + await this.pluginServer.deploy(`local-file:${uriPath}`); } } }); diff --git a/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts b/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts index de5c7d9198072..170bd282ec0d9 100644 --- a/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts +++ b/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts @@ -22,8 +22,14 @@ import { Widget } from '@theia/core/lib/browser/widgets/widget'; import { VSXExtensionsModel } from './vsx-extensions-model'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorRegistry, Color } from '@theia/core/lib/browser/color-registry'; -import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { TabBarToolbarContribution, TabBarToolbarItem, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser/frontend-application'; +import { MessageService, Mutable } from '@theia/core/lib/common'; +import { FileDialogService, OpenFileDialogProps } from '@theia/filesystem/lib/browser'; +import { PluginServer } from '@theia/plugin-ext/lib/common'; +import URI from '@theia/core/lib/common/uri'; +import { LabelProvider } from '@theia/core/lib/browser'; +import { VscodeCommands } from '@theia/plugin-ext-vscode/lib/browser/plugin-vscode-commands-contribution'; export namespace VSXExtensionsCommands { export const CLEAR_ALL: Command = { @@ -32,14 +38,25 @@ export namespace VSXExtensionsCommands { label: 'Clear Search Results', iconClass: 'clear-all' }; + export const INSTALL_FROM_VSIX: Command & { dialogLabel: string } = { + id: 'vsxExtensions.installFromVSIX', + category: 'Extensions', + label: 'Install from VSIX...', + dialogLabel: 'Install from VSIX' + }; } @injectable() export class VSXExtensionsContribution extends AbstractViewContribution implements ColorContribution, FrontendApplicationContribution, TabBarToolbarContribution { - @inject(VSXExtensionsModel) - protected readonly model: VSXExtensionsModel; + @inject(VSXExtensionsModel) protected readonly model: VSXExtensionsModel; + @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; + @inject(PluginServer) protected readonly pluginServer: PluginServer; + @inject(TabBarToolbarRegistry) protected readonly tabbarToolbarRegistry: TabBarToolbarRegistry; + @inject(FileDialogService) protected readonly fileDialogService: FileDialogService; + @inject(MessageService) protected readonly messageService: MessageService; + @inject(LabelProvider) protected readonly labelProvider: LabelProvider; constructor() { super({ @@ -65,6 +82,10 @@ export class VSXExtensionsContribution extends AbstractViewContribution this.withWidget(w, () => !!this.model.search.query), isVisible: w => this.withWidget(w, () => true) }); + + commands.registerCommand(VSXExtensionsCommands.INSTALL_FROM_VSIX, { + execute: () => this.installFromVSIX() + }); } registerToolbarItems(registry: TabBarToolbarRegistry): void { @@ -75,8 +96,36 @@ export class VSXExtensionsContribution extends AbstractViewContribution) => { + const commandId = item.command; + const id = 'vsxExtensions.tabbar.toolbar.' + commandId; + const command = this.commandRegistry.getCommand(commandId); + this.commandRegistry.registerCommand({ id, iconClass: command && command.iconClass }, { + execute: (w, ...args) => w instanceof VSXExtensionsViewContainer + && this.commandRegistry.executeCommand(commandId, ...args), + isEnabled: (w, ...args) => w instanceof VSXExtensionsViewContainer + && this.commandRegistry.isEnabled(commandId, ...args), + isVisible: (w, ...args) => w instanceof VSXExtensionsViewContainer + && this.commandRegistry.isVisible(commandId, ...args), + isToggled: (w, ...args) => w instanceof VSXExtensionsViewContainer + && this.commandRegistry.isToggled(commandId, ...args), + }); + item.command = id; + this.tabbarToolbarRegistry.registerItem(item); + }; + registerColors(colors: ColorRegistry): void { // VS Code colors should be aligned with https://code.visualstudio.com/api/references/theme-color#extensions colors.register( @@ -107,4 +156,33 @@ export class VSXExtensionsContribution extends AbstractViewContribution { + const props: OpenFileDialogProps = { + title: VSXExtensionsCommands.INSTALL_FROM_VSIX.dialogLabel, + openLabel: 'Install', + filters: { 'VSIX Extensions (*.vsix)': ['vsix'] }, + canSelectMany: false + }; + const extensionUri = await this.fileDialogService.showOpenDialog(props); + if (extensionUri) { + if (extensionUri.path.ext === '.vsix') { + const extensionName = this.labelProvider.getName(extensionUri); + try { + await this.commandRegistry.executeCommand(VscodeCommands.INSTALL_FROM_VSIX.id, extensionUri); + this.messageService.info(`Completed installing ${extensionName} from VSIX.`); + } catch (e) { + this.messageService.error(`Failed to install ${extensionName} from VSIX.`); + console.warn(e); + } + } else { + this.messageService.error('The selected file is not a valid "*.vsix" plugin.'); + } + return extensionUri; + } + return undefined; + } }