Skip to content

Commit

Permalink
VSX: Add Extension Context-Menu & Copy Commands
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
seantan22 committed Apr 7, 2021
1 parent 22dcd7e commit f66f98d
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 11 deletions.
4 changes: 3 additions & 1 deletion packages/vsx-registry/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,11 @@
}

.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 */
Expand Down Expand Up @@ -303,6 +304,7 @@
margin-top: calc(var(--theia-ui-padding)*5/3);
margin-left: 0px;
padding: 1px var(--theia-ui-padding);
vertical-align: middle;
}

/** Theming */
Expand Down
71 changes: 64 additions & 7 deletions packages/vsx-registry/src/browser/vsx-extension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -55,6 +64,7 @@ export class VSXExtensionData {
'description',
'averageRating',
'downloadCount',
'downloadUrl',
'readmeUrl',
'licenseUrl',
'repository',
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -253,6 +270,42 @@ export class VSXExtension implements VSXExtensionData, TreeElement {
}
}

handleContextMenu(e: React.MouseEvent<HTMLElement, 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<URI> {
const uri = await this.environment.getRegistryUri();
return uri.resolve('extension/' + this.id.replace('.', '/')).resolve(path);
}

async serialize(): Promise<string> {
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<void> {
await this.doOpen(this.uri, options);
}
Expand Down Expand Up @@ -289,11 +342,15 @@ export abstract class AbstractVSXExtensionComponent extends React.Component<Abst
}
};

protected readonly manage = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
this.props.extension.handleContextMenu(e);
};

protected renderAction(): React.ReactNode {
const extension = this.props.extension;
const { builtin, busy, installed } = extension;
if (builtin) {
return undefined;
return <div className="codicon codicon-settings-gear action" onClick={this.manage}></div>;
}
if (busy) {
if (installed) {
Expand All @@ -302,7 +359,8 @@ export abstract class AbstractVSXExtensionComponent extends React.Component<Abst
return <button className="theia-button action prominent theia-mod-disabled">Installing</button>;
}
if (installed) {
return <button className="theia-button action" onClick={this.uninstall}>Uninstall</button>;
return <div><button className="theia-button action" onClick={this.uninstall}>Uninstall</button>
<div className="codicon codicon-settings-gear action" onClick={this.manage}></div></div>;
}
return <button className="theia-button prominent action" onClick={this.install}>Install</button>;
}
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -524,5 +582,4 @@ export class VSXExtensionEditorComponent extends AbstractVSXExtensionComponent {
extension.doOpen(new URI(licenseUrl));
}
};

}
48 changes: 45 additions & 3 deletions packages/vsx-registry/src/browser/vsx-extensions-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -54,6 +65,7 @@ export class VSXExtensionsContribution extends AbstractViewContribution<VSXExten
@inject(FileDialogService) protected readonly fileDialogService: FileDialogService;
@inject(MessageService) protected readonly messageService: MessageService;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
@inject(ClipboardService) protected readonly clipboardService: ClipboardService;

constructor() {
super({
Expand Down Expand Up @@ -83,6 +95,14 @@ export class VSXExtensionsContribution extends AbstractViewContribution<VSXExten
commands.registerCommand(VSXExtensionsCommands.INSTALL_FROM_VSIX, {
execute: () => 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 {
Expand Down Expand Up @@ -123,6 +143,20 @@ export class VSXExtensionsContribution extends AbstractViewContribution<VSXExten
this.tabbarToolbarRegistry.registerItem(item);
};

registerMenus(menus: MenuModelRegistry): void {
super.registerMenus(menus);
menus.registerMenuAction(VSXExtensionsContextMenu.COPY, {
commandId: VSXExtensionsCommands.COPY.id,
label: 'Copy',
order: '0'
});
menus.registerMenuAction(VSXExtensionsContextMenu.COPY, {
commandId: VSXExtensionsCommands.COPY_EXTENSION_ID.id,
label: 'Copy Extension Id',
order: '1'
});
}

registerColors(colors: ColorRegistry): void {
// VS Code colors should be aligned with https://code.visualstudio.com/api/references/theme-color#extensions
colors.register(
Expand Down Expand Up @@ -180,4 +214,12 @@ export class VSXExtensionsContribution extends AbstractViewContribution<VSXExten
}
}
}

protected async copy(extension: VSXExtension): Promise<void> {
this.clipboardService.writeText(await extension.serialize());
}

protected copyExtensionId(extension: VSXExtension): void {
this.clipboardService.writeText(extension.id);
}
}

0 comments on commit f66f98d

Please sign in to comment.