prevent submenu cycles

This commit is contained in:
João Moreno 2020-07-22 15:22:31 +02:00
parent 5b2e4229ff
commit c2e4add116
2 changed files with 21 additions and 7 deletions

View file

@ -42,6 +42,7 @@ export interface IMenuOptions {
anchorAlignment?: AnchorAlignment;
expandDirection?: Direction;
useEventAsContext?: boolean;
submenuIds?: Set<string>;
}
export interface IMenuStyles {
@ -199,6 +200,15 @@ export class Menu extends ActionBar {
menuElement.style.maxHeight = `${Math.max(10, window.innerHeight - container.getBoundingClientRect().top - 30)}px`;
actions = actions.filter(a => {
if (options.submenuIds?.has(a.id)) {
console.warn(`Found submenu cycle: ${a.id}`);
return false;
}
return true;
});
this.push(actions, { icon: true, label: true, isMenu: true });
container.appendChild(this.scrollableElement.getDomNode());
@ -294,7 +304,7 @@ export class Menu extends ActionBar {
return new MenuSeparatorActionViewItem(options.context, action, { icon: true });
} else if (action instanceof SubmenuAction) {
const actions = Array.isArray(action.actions) ? action.actions : action.actions();
const menuActionViewItem = new SubmenuMenuActionViewItem(action, actions, parentData, options);
const menuActionViewItem = new SubmenuMenuActionViewItem(action, actions, parentData, { ...options, submenuIds: new Set([...(options.submenuIds || []), action.id]) });
if (options.enableMnemonics) {
const mnemonic = menuActionViewItem.getMnemonic();

View file

@ -25,6 +25,7 @@ import { ContextMenuService as HTMLContextMenuService } from 'vs/platform/contex
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { stripCodicons } from 'vs/base/common/codicons';
import { coalesce } from 'vs/base/common/arrays';
export class ContextMenuService extends Disposable implements IContextMenuService {
@ -123,14 +124,12 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
}
}
private createMenu(delegate: IContextMenuDelegate, entries: IAction[], onHide: () => void): IContextMenuItem[] {
private createMenu(delegate: IContextMenuDelegate, entries: IAction[], onHide: () => void, submenuIds = new Set<string>()): IContextMenuItem[] {
const actionRunner = delegate.actionRunner || new ActionRunner();
return entries.map(entry => this.createMenuItem(delegate, entry, actionRunner, onHide));
return coalesce(entries.map(entry => this.createMenuItem(delegate, entry, actionRunner, onHide, submenuIds)));
}
private createMenuItem(delegate: IContextMenuDelegate, entry: IAction, actionRunner: IActionRunner, onHide: () => void): IContextMenuItem {
private createMenuItem(delegate: IContextMenuDelegate, entry: IAction, actionRunner: IActionRunner, onHide: () => void, submenuIds: Set<string>): IContextMenuItem | undefined {
// Separator
if (entry instanceof Separator) {
return { type: 'separator' };
@ -138,10 +137,15 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
// Submenu
if (entry instanceof SubmenuAction) {
if (submenuIds.has(entry.id)) {
console.warn(`Found submenu cycle: ${entry.id}`);
return undefined;
}
const actions = Array.isArray(entry.actions) ? entry.actions : entry.actions();
return {
label: unmnemonicLabel(stripCodicons(entry.label)).trim(),
submenu: this.createMenu(delegate, actions, onHide)
submenu: this.createMenu(delegate, actions, onHide, new Set([...submenuIds, entry.id]))
};
}