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

vscode built-in menus #4173

Merged
merged 9 commits into from
Jan 31, 2019
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
# Change Log

## v0.3.20
## v0.4.0
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- [plugin] added `tasks.onDidEndTask` Plug-in API
- [cpp] fixed `CPP_CLANGD_COMMAND` and `CPP_CLANGD_ARGS` environment variables

Breaking changes:
- menus aligned with built-in VS Code menus [#4173](https://github.com/theia-ide/theia/pull/4173)
- navigator context menu group changes:
- `1_open` and `4_new` replaced by `navigation` group
- `6_workspace` renamed to `2_workspace` group
- `5_diff` renamed to `3_compare` group
- `6_find` renamed to `4_search` group
- `2_clipboard` renamed to `5_cutcopypaste` group
- `3_move` and `7_actions` replaced by `navigation` group
- editor context menu group changes:
- `2_cut_copy_paste` renamed to `9_cutcopypaste` group

## v0.3.19
- [core] added `hostname` alias
Expand Down
21 changes: 12 additions & 9 deletions packages/core/src/browser/menu/browser-menu-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class BrowserMainMenuFactory {

for (const menu of menuModel.children) {
if (menu instanceof CompositeMenuNode) {
const menuWidget = new DynamicMenuWidget(menu, { commands: phosphorCommands });
const menuWidget = new DynamicMenuWidget(menu, { commands: phosphorCommands }, this.contextKeyService);
menuBar.addMenu(menuWidget);
}
}
Expand All @@ -61,7 +61,7 @@ export class BrowserMainMenuFactory {
const menuModel = this.menuProvider.getMenu(path);
const phosphorCommands = this.createPhosphorCommands(menuModel);

const contextMenu = new DynamicMenuWidget(menuModel, { commands: phosphorCommands });
const contextMenu = new DynamicMenuWidget(menuModel, { commands: phosphorCommands }, this.contextKeyService);
return contextMenu;
}

Expand All @@ -87,16 +87,15 @@ export class BrowserMainMenuFactory {
return;
}
if (commands.hasCommand(command.id)) {
this.logger.warn(`Command with ID ${command.id} is already registered`);
// several menu items can be registered for the same command in different contexts
return;
}
const { when } = menu.action;
commands.addCommand(command.id, {
execute: () => this.commandRegistry.executeCommand(command.id),
label: menu.label,
icon: menu.icon,
isEnabled: () => this.commandRegistry.isEnabled(command.id),
isVisible: () => this.commandRegistry.isVisible(command.id) && (!when || this.contextKeyService.match(when)),
isVisible: () => this.commandRegistry.isVisible(command.id),
isToggled: () => this.commandRegistry.isToggled(command.id)
});

Expand Down Expand Up @@ -134,7 +133,11 @@ class DynamicMenuBarWidget extends MenuBarWidget {
*/
class DynamicMenuWidget extends MenuWidget {

constructor(protected menu: CompositeMenuNode, protected options: MenuWidget.IOptions) {
constructor(
protected menu: CompositeMenuNode,
protected options: MenuWidget.IOptions,
protected contextKeyService: ContextKeyService
) {
super(options);
if (menu.label) {
this.title.label = menu.label;
Expand Down Expand Up @@ -181,7 +184,7 @@ class DynamicMenuWidget extends MenuWidget {

if (item.isSubmenu) { // submenu node

const submenu = new DynamicMenuWidget(item, this.options);
const submenu = new DynamicMenuWidget(item, this.options, this.contextKeyService);
if (submenu.items.length === 0) {
continue;
}
Expand Down Expand Up @@ -212,8 +215,8 @@ class DynamicMenuWidget extends MenuWidget {
}

} else if (item instanceof ActionMenuNode) {

if (!commands.isVisible(item.action.commandId)) {
const { when } = item.action;
if (!(commands.isVisible(item.action.commandId) && (!when || this.contextKeyService.match(when)))) {
continue;
}

Expand Down
76 changes: 64 additions & 12 deletions packages/core/src/browser/shell/tab-bar-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ContributionProvider } from '../../common/contribution-provider';
import { FrontendApplicationContribution } from '../frontend-application';
import { CommandRegistry, CommandService } from '../../common/command';
import { Disposable } from '../../common/disposable';
import { ContextKeyService } from '../context-key-service';

/**
* Factory for instantiating tab-bar toolbars.
Expand All @@ -39,7 +40,7 @@ export class TabBarToolbar extends ReactWidget {
protected current: Widget | undefined;
protected items = new Map<string, TabBarToolbarItem>();

constructor(protected readonly commandService: CommandService, protected readonly labelParser: LabelParser) {
constructor(protected readonly commands: CommandRegistry, protected readonly labelParser: LabelParser) {
super();
this.addClass(TabBarToolbar.Styles.TAB_BAR_TOOLBAR);
this.hide();
Expand Down Expand Up @@ -68,14 +69,21 @@ export class TabBarToolbar extends ReactWidget {
protected renderItem(item: TabBarToolbarItem): React.ReactNode {
let innerText = '';
const classNames = [];
for (const labelPart of this.labelParser.parse(item.text)) {
if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) {
const className = `fa fa-${labelPart.name}${labelPart.animation ? ' fa-' + labelPart.animation : ''}`;
classNames.push(...className.split(' '));
} else {
innerText = labelPart;
if (item.text) {
for (const labelPart of this.labelParser.parse(item.text)) {
if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) {
const className = `fa fa-${labelPart.name}${labelPart.animation ? ' fa-' + labelPart.animation : ''}`;
classNames.push(...className.split(' '));
} else {
innerText = labelPart;
}
}
}
const command = this.commands.getCommand(item.command);
const iconClass = command && command.iconClass;
if (iconClass) {
classNames.push(iconClass);
}
return <div key={item.id} className={TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} >
<div id={item.id} className={classNames.join(' ')} onClick={this.executeCommand} title={item.tooltip}>{innerText}</div>
</div>;
Expand All @@ -84,7 +92,7 @@ export class TabBarToolbar extends ReactWidget {
protected executeCommand = (e: React.MouseEvent<HTMLElement>) => {
const item = this.items.get(e.currentTarget.id);
if (item) {
this.commandService.executeCommand(item.command, this.current);
this.commands.executeCommand(item.command, this.current);
}
}

Expand Down Expand Up @@ -127,7 +135,7 @@ export interface TabBarToolbarItem {
readonly command: string;

/**
* Text of the item.
* Optional text of the item.
*
* Shamelessly copied and reused from `status-bar`:
*
Expand All @@ -144,26 +152,60 @@ export interface TabBarToolbarItem {
* The type of animation can be either `spin` or `pulse`.
* Look [here](http://fontawesome.io/examples/#animated) for more information to animated icons.
*/
readonly text: string;
readonly text?: string;

/**
* Priority among the items. Can be negative. The smaller the number the left-most the item will be placed in the toolbar. It is `0` by default.
*/
readonly priority?: number;

/**
* Optional group for the item.
*/
readonly group?: string;

/**
* Optional tooltip for the item.
*/
readonly tooltip?: string;

/**
* https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts
*/
readonly when?: string;

}

export namespace TabBarToolbarItem {

/**
* Compares the items by `priority` in ascending. Undefined priorities will be treated as `0`.
*/
export const PRIORITY_COMPARATOR = (left: TabBarToolbarItem, right: TabBarToolbarItem) => (left.priority || 0) - (right.priority || 0);
export const PRIORITY_COMPARATOR = (left: TabBarToolbarItem, right: TabBarToolbarItem) => {
// The navigation group is special as it will always be sorted to the top/beginning of a menu.
if (left.group === 'navigation') {
return -1;
}
if (right.group === 'navigation') {
return 1;
}
if (left.group && right.group) {
if (left.group < right.group) {
return -1;
} else if (left.group > right.group) {
return 1;
} else {
return 0;
}
}
if (left.group) {
return -1;
}
if (right.group) {
return 1;
}
return (left.priority || 0) - (right.priority || 0);
};

}

Expand All @@ -178,6 +220,9 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;

@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;

@inject(ContributionProvider)
@named(TabBarToolbarContribution)
protected readonly contributionProvider: ContributionProvider<TabBarToolbarContribution>;
Expand Down Expand Up @@ -208,7 +253,14 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution {
* By default returns with all items where the command is enabled and `item.isVisible` is `true`.
*/
visibleItems(widget: Widget): TabBarToolbarItem[] {
return [...this.items.values()].filter(item => this.commandRegistry.isVisible(item.command, widget));
const result = [];
for (const item of this.items.values()) {
if (this.commandRegistry.isVisible(item.command, widget) &&
(!item.when || this.contextKeyService.match(item.when, widget.node))) {
result.push(item);
}
}
return result;
}

}
11 changes: 11 additions & 0 deletions packages/core/src/common/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ export class CompositeMenuNode implements MenuNode {
public addNode(node: MenuNode): Disposable {
this._children.push(node);
this._children.sort((m1, m2) => {
// The navigation group is special as it will always be sorted to the top/beginning of a menu.
if (CompositeMenuNode.isNavigationGroup(m1)) {
return -1;
}
if (CompositeMenuNode.isNavigationGroup(m2)) {
return 1;
}
if (m1.sortString < m2.sortString) {
return -1;
} else if (m1.sortString > m2.sortString) {
Expand Down Expand Up @@ -213,6 +220,10 @@ export class CompositeMenuNode implements MenuNode {
get isSubmenu(): boolean {
return this.label !== undefined;
}

static isNavigationGroup(node: MenuNode): node is CompositeMenuNode {
return node instanceof CompositeMenuNode && node.id === 'navigation';
}
}

export class ActionMenuNode implements MenuNode {
Expand Down
15 changes: 14 additions & 1 deletion packages/debug/src/browser/view/debug-stack-frames-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
********************************************************************************/

import { injectable, inject, postConstruct, interfaces, Container } from 'inversify';
import { MenuPath } from '@theia/core';
import { MenuPath, SelectionService } from '@theia/core';
import { TreeNode, NodeProps, SelectableTreeNode } from '@theia/core/lib/browser';
import { SourceTreeWidget, TreeElementNode } from '@theia/core/lib/browser/source-tree';
import { DebugStackFramesSource, LoadMoreStackFrames } from './debug-stack-frames-source';
Expand Down Expand Up @@ -48,6 +48,9 @@ export class DebugStackFramesWidget extends SourceTreeWidget {
@inject(DebugViewModel)
protected readonly viewModel: DebugViewModel;

@inject(SelectionService)
protected readonly selectionService: SelectionService;

@inject(DebugCallStackItemTypeKey)
protected readonly debugCallStackItemTypeKey: DebugCallStackItemTypeKey;

Expand Down Expand Up @@ -88,13 +91,23 @@ export class DebugStackFramesWidget extends SourceTreeWidget {
}
this.updatingSelection = true;
try {
let selection: string | number | undefined;
const node = this.model.selectedNodes[0];
if (TreeElementNode.is(node)) {
if (node.element instanceof DebugStackFrame) {
node.element.thread.currentFrame = node.element;
this.debugCallStackItemTypeKey.set('stackFrame');
const source = node.element.source;
if (source) {
if (source.inMemory) {
selection = source.raw.path || source.raw.sourceReference;
} else {
selection = source.uri.toString();
}
}
}
}
this.selectionService.selection = selection;
} finally {
this.updatingSelection = false;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/debug/src/browser/view/debug-threads-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { injectable, inject, postConstruct, interfaces, Container } from 'inversify';
import { MenuPath } from '@theia/core';
import { TreeNode, NodeProps, SelectableTreeNode } from '@theia/core/lib/browser';
import { SelectionService } from '@theia/core/lib/common/selection-service';
import { SourceTreeWidget, TreeElementNode } from '@theia/core/lib/browser/source-tree';
import { DebugThreadsSource } from './debug-threads-source';
import { DebugSession } from '../debug-session';
Expand Down Expand Up @@ -52,6 +53,9 @@ export class DebugThreadsWidget extends SourceTreeWidget {
@inject(DebugViewModel)
protected readonly viewModel: DebugViewModel;

@inject(SelectionService)
protected readonly selectionService: SelectionService;

@inject(DebugCallStackItemTypeKey)
protected readonly debugCallStackItemTypeKey: DebugCallStackItemTypeKey;

Expand Down Expand Up @@ -91,6 +95,7 @@ export class DebugThreadsWidget extends SourceTreeWidget {
}
this.updatingSelection = true;
try {
let selection: number | undefined;
const node = this.model.selectedNodes[0];
if (TreeElementNode.is(node)) {
if (node.element instanceof DebugSession) {
Expand All @@ -99,8 +104,10 @@ export class DebugThreadsWidget extends SourceTreeWidget {
} else if (node.element instanceof DebugThread) {
node.element.session.currentThread = node.element;
this.debugCallStackItemTypeKey.set('thread');
selection = node.element.raw.id;
}
}
this.selectionService.selection = selection;
} finally {
this.updatingSelection = false;
}
Expand Down
10 changes: 8 additions & 2 deletions packages/editor/src/browser/editor-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,16 @@ import { EditorCommands } from './editor-command';

export const EDITOR_CONTEXT_MENU: MenuPath = ['editor_context_menu'];

/**
* Editor context menu default groups should be aligned
* with VS Code default groups: https://code.visualstudio.com/api/references/contribution-points#contributes.menus
*/
export namespace EditorContextMenu {
export const UNDO_REDO = [...EDITOR_CONTEXT_MENU, '1_undo'];
export const CUT_COPY_PASTE = [...EDITOR_CONTEXT_MENU, '2_cut_copy_paste'];
export const NAVIGATION = [...EDITOR_CONTEXT_MENU, 'navigation'];
export const MODIFICATION = [...EDITOR_CONTEXT_MENU, '1_modification'];
export const CUT_COPY_PASTE = [...EDITOR_CONTEXT_MENU, '9_cutcopypaste'];
export const COMMANDS = [...EDITOR_CONTEXT_MENU, 'z_commands'];
export const UNDO_REDO = [...EDITOR_CONTEXT_MENU, '1_undo'];
}

export namespace EditorMainMenu {
Expand Down
4 changes: 2 additions & 2 deletions packages/git/src/browser/diff/git-diff-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { WidgetManager } from '@theia/core/lib/browser/widget-manager';
import { injectable, inject } from 'inversify';
import { GitDiffWidget, GIT_DIFF } from './git-diff-widget';
import { open, OpenerService } from '@theia/core/lib/browser';
import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution';
import { NavigatorContextMenu } from '@theia/navigator/lib/browser/navigator-contribution';
import { UriCommandHandler, UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler';
import { GitQuickOpenService } from '../git-quick-open-service';
import { FileSystem } from '@theia/filesystem/lib/common';
Expand Down Expand Up @@ -62,7 +62,7 @@ export class GitDiffContribution extends AbstractViewContribution<GitDiffWidget>
}

registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction([...NAVIGATOR_CONTEXT_MENU, '5_diff'], {
menus.registerMenuAction(NavigatorContextMenu.COMPARE, {
commandId: GitDiffCommands.OPEN_FILE_DIFF.id
});
}
Expand Down
Loading