From 0f9d0038082847c27e911a3e72017c2f7c252c3f Mon Sep 17 00:00:00 2001 From: seantan22 Date: Wed, 31 Mar 2021 11:39:41 -0400 Subject: [PATCH] VSX: Add Extension Context-Menu & Copy Commands What it does - Adds a context-menu to each extension to which extension-specific commands can be registered; accessible through the gear icon. - Adds support for `Copy` and `Copy Extension Id` commands. How to test 1. Open the _extensions-view_. 2. Select an extension and click the gear icon. 3. Click either the `Copy` command or the `Copy Extension Id` command. 4. Paste results in a text-editor to confirm that the correct information was copied. Signed-off-by: seantan22 --- .../vsx-registry/src/browser/style/index.css | 5 +- .../src/browser/vsx-extension.tsx | 71 +++++++++++++++++-- .../browser/vsx-extensions-contribution.ts | 48 ++++++++++++- 3 files changed, 113 insertions(+), 11 deletions(-) diff --git a/packages/vsx-registry/src/browser/style/index.css b/packages/vsx-registry/src/browser/style/index.css index 9bce666912400..172ce01508e4b 100644 --- a/packages/vsx-registry/src/browser/style/index.css +++ b/packages/vsx-registry/src/browser/style/index.css @@ -121,13 +121,15 @@ flex-direction: row; justify-content: space-between; align-items: center; + white-space: nowrap; } .theia-vsx-extension-action-bar .action { - font-size: 80%; + font-size: 90%; min-width: auto !important; padding: 2px var(--theia-ui-padding) !important; margin-top: 2px; + vertical-align: middle; } /* Editor Section */ @@ -303,6 +305,7 @@ margin-top: calc(var(--theia-ui-padding)*5/3); margin-left: 0px; padding: 1px var(--theia-ui-padding); + vertical-align: middle; } /** Theming */ diff --git a/packages/vsx-registry/src/browser/vsx-extension.tsx b/packages/vsx-registry/src/browser/vsx-extension.tsx index f750bd93087d5..1ba11e015f3dd 100644 --- a/packages/vsx-registry/src/browser/vsx-extension.tsx +++ b/packages/vsx-registry/src/browser/vsx-extension.tsx @@ -27,6 +27,14 @@ import { Endpoint } from '@theia/core/lib/browser/endpoint'; import { VSXEnvironment } from '../common/vsx-environment'; import { VSXExtensionsSearchModel } from './vsx-extensions-search-model'; import { VSXExtensionNamespaceAccess, VSXUser } from '../common/vsx-registry-types'; +import { MenuPath } from '@theia/core/lib/common'; +import { ContextMenuRenderer } from '@theia/core/lib/browser'; + +export const EXTENSIONS_CONTEXT_MENU: MenuPath = ['extensions_context_menu']; + +export namespace VSXExtensionsContextMenu { + export const COPY = [...EXTENSIONS_CONTEXT_MENU, '1_copy']; +} @injectable() export class VSXExtensionData { @@ -38,6 +46,7 @@ export class VSXExtensionData { readonly description?: string; readonly averageRating?: number; readonly downloadCount?: number; + readonly downloadUrl?: string; readonly readmeUrl?: string; readonly licenseUrl?: string; readonly repository?: string; @@ -55,6 +64,7 @@ export class VSXExtensionData { 'description', 'averageRating', 'downloadCount', + 'downloadUrl', 'readmeUrl', 'licenseUrl', 'repository', @@ -92,6 +102,9 @@ export class VSXExtension implements VSXExtensionData, TreeElement { @inject(ProgressService) protected readonly progressService: ProgressService; + @inject(ContextMenuRenderer) + protected readonly contextMenuRenderer: ContextMenuRenderer; + @inject(VSXEnvironment) readonly environment: VSXEnvironment; @@ -180,6 +193,10 @@ export class VSXExtension implements VSXExtensionData, TreeElement { return this.getData('downloadCount'); } + get downloadUrl(): string | undefined { + return this.getData('downloadUrl'); + } + get readmeUrl(): string | undefined { const plugin = this.plugin; const readmeUrl = plugin && plugin.metadata.model.readmeUrl; @@ -253,6 +270,42 @@ export class VSXExtension implements VSXExtensionData, TreeElement { } } + handleContextMenu(e: React.MouseEvent): void { + e.preventDefault(); + this.contextMenuRenderer.render({ + menuPath: EXTENSIONS_CONTEXT_MENU, + anchor: { + x: e.clientX, + y: e.clientY, + }, + args: [this] + }); + } + + /** + * Get the registry link for the given extension. + * @param path the url path. + * @returns the registry link for the given extension at the path. + */ + async getRegistryLink(path = ''): Promise { + const uri = await this.environment.getRegistryUri(); + return uri.resolve('extension/' + this.id.replace('.', '/')).resolve(path); + } + + async serialize(): Promise { + const serializedExtension: string[] = []; + serializedExtension.push(`Name: ${this.displayName}`); + serializedExtension.push(`Id: ${this.id}`); + serializedExtension.push(`Description: ${this.description}`); + serializedExtension.push(`Version: ${this.version}`); + serializedExtension.push(`Publisher: ${this.publisher}`); + if (this.downloadUrl !== undefined) { + const registryLink = await this.getRegistryLink(); + serializedExtension.push(`Open VSX Link: ${registryLink.toString()}`); + }; + return serializedExtension.join('\n'); + } + async open(options: OpenerOptions = { mode: 'reveal' }): Promise { await this.doOpen(this.uri, options); } @@ -289,11 +342,15 @@ export abstract class AbstractVSXExtensionComponent extends React.Component) => { + this.props.extension.handleContextMenu(e); + }; + protected renderAction(): React.ReactNode { const extension = this.props.extension; const { builtin, busy, installed } = extension; if (builtin) { - return undefined; + return
; } if (busy) { if (installed) { @@ -302,7 +359,8 @@ export abstract class AbstractVSXExtensionComponent extends React.ComponentInstalling; } if (installed) { - return ; + return
+
; } return ; } @@ -475,8 +533,8 @@ export class VSXExtensionEditorComponent extends AbstractVSXExtensionComponent { e.preventDefault(); const extension = this.props.extension; - const uri = await extension.environment.getRegistryUri(); - extension.doOpen(uri.resolve('extension/' + extension.id.replace('.', '/'))); + const uri = await extension.getRegistryLink(); + extension.doOpen(uri); }; readonly searchPublisher = (e: React.MouseEvent) => { e.stopPropagation(); @@ -502,8 +560,8 @@ export class VSXExtensionEditorComponent extends AbstractVSXExtensionComponent { e.preventDefault(); const extension = this.props.extension; - const uri = await extension.environment.getRegistryUri(); - extension.doOpen(uri.resolve('extension/' + extension.id.replace('.', '/') + '/reviews')); + const uri = await extension.getRegistryLink('reviews'); + extension.doOpen(uri); }; readonly openRepository = (e: React.MouseEvent) => { e.stopPropagation(); @@ -524,5 +582,4 @@ export class VSXExtensionEditorComponent extends AbstractVSXExtensionComponent { extension.doOpen(new URI(licenseUrl)); } }; - } diff --git a/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts b/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts index ca7de7d914306..2bc71746dca8b 100644 --- a/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts +++ b/packages/vsx-registry/src/browser/vsx-extensions-contribution.ts @@ -24,24 +24,35 @@ import { ColorContribution } from '@theia/core/lib/browser/color-application-con import { ColorRegistry, Color } from '@theia/core/lib/browser/color-registry'; 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 { MenuModelRegistry, MessageService, Mutable } from '@theia/core/lib/common'; import { FileDialogService, OpenFileDialogProps } from '@theia/filesystem/lib/browser'; import { LabelProvider } from '@theia/core/lib/browser'; import { VscodeCommands } from '@theia/plugin-ext-vscode/lib/browser/plugin-vscode-commands-contribution'; +import { VSXExtensionsContextMenu, VSXExtension } from './vsx-extension'; +import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; export namespace VSXExtensionsCommands { + + const EXTENSIONS_CATEGORY = 'Extensions'; + export const CLEAR_ALL: Command = { id: 'vsxExtensions.clearAll', - category: 'Extensions', + category: EXTENSIONS_CATEGORY, label: 'Clear Search Results', iconClass: 'clear-all' }; export const INSTALL_FROM_VSIX: Command & { dialogLabel: string } = { id: 'vsxExtensions.installFromVSIX', - category: 'Extensions', + category: EXTENSIONS_CATEGORY, label: 'Install from VSIX...', dialogLabel: 'Install from VSIX' }; + export const COPY: Command = { + id: 'vsxExtensions.copy' + }; + export const COPY_EXTENSION_ID: Command = { + id: 'vsxExtensions.copyExtensionId' + }; } @injectable() @@ -54,6 +65,7 @@ export class VSXExtensionsContribution extends AbstractViewContribution this.installFromVSIX() }); + + commands.registerCommand(VSXExtensionsCommands.COPY, { + execute: (extension: VSXExtension) => this.copy(extension) + }); + + commands.registerCommand(VSXExtensionsCommands.COPY_EXTENSION_ID, { + execute: (extension: VSXExtension) => this.copyExtensionId(extension) + }); } registerToolbarItems(registry: TabBarToolbarRegistry): void { @@ -123,6 +143,20 @@ export class VSXExtensionsContribution extends AbstractViewContribution { + this.clipboardService.writeText(await extension.serialize()); + } + + protected copyExtensionId(extension: VSXExtension): void { + this.clipboardService.writeText(extension.id); + } }