Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VSX: Add Extension Context-Menu & Copy Commands #9292

Merged
merged 1 commit into from
Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/vsx-registry/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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 */
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);
}
}