menuService.ts ×29

Frontier kind: Code frontier

unlabeled · c_2518c16bed4e

4 tests · 16439 LOC · 70 files · introduces 0 tests · 223 LOC · 3 files

Introduces — evidence that enters the hierarchy at this concept

Code
34 ranges223 lines · 3 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
2113 ranges16439 lines · 70 files · Browse complete extent
All tests (intent)
4 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

3 files ranked by introduced lines: 223 introduced LOC across 34 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/actions/common/menuService.ts 175 introduced LOC · 29 ranges

Open complete file

33
34 createMenu(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuCreateOptions): IMenu {
35 > return new MenuImpl(id, this._hiddenStates, { emitEventsForSubmenuChanges: false, eventDebounceDelay: 50, ...options }, this._commandService, this._keybindingService, contextKeyService); menuService.ts
36 > }
37
38 getMenuActions(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuActionOptions): [string, Array<MenuItemAction | SubmenuItemAction>][] {
93
94 private _isHiddenByDefault(menu: MenuId, commandId: string) {
95 > return this._hiddenByDefaultCache.get(`${menu.id}/${commandId}`) ?? false; menuService.ts
96 > }
97
98 setDefaultState(menu: MenuId, commandId: string, hidden: boolean): void {
99 > this._hiddenByDefaultCache.set(`${menu.id}/${commandId}`, hidden); menuService.ts
100 > }
101
102 isHidden(menu: MenuId, commandId: string): boolean {
103 > const hiddenByDefault = this._isHiddenByDefault(menu, commandId); menuService.ts
104 > const state = this._data[menu.id]?.includes(commandId) ?? false;
105 > return hiddenByDefault ? !state : state;
106 > }
107
108 updateHidden(menu: MenuId, commandId: string, hidden: boolean): void {
174
175 constructor(
176 > protected readonly _id: MenuId, menuService.ts
177 > protected readonly _collectContextKeysForSubmenus: boolean,
178 > ) {
179 > this.refresh();
180 > }
181
182 get allMenuIds(): ReadonlySet<MenuId> {
197
198 refresh(): void {
200 > // reset
201 > this._menuGroups.length = 0;
202 > this._allMenuIds.clear();
203 > this._structureContextKeys.clear();
204 > this._preconditionContextKeys.clear();
205 > this._toggledContextKeys.clear();
206 >
207 > const menuItems = this._sort(MenuRegistry.getMenuItems(this._id));
208 > let group: MenuItemGroup | undefined;
209 >
210 > for (const item of menuItems) {
211 > // group by groupId
212 > const groupName = item.group || '';
213 > if (!group || group[0] !== groupName) {
214 > group = [groupName, []];
215 > this._menuGroups.push(group);
216 > }
217 > group[1].push(item);
218 >
219 > // keep keys and submenu ids for eventing
220 > this._collectContextKeysAndSubmenuIds(item);
221 > }
222 > this._allMenuIds.add(this._id);
223 > }
224
225 protected _sort(menuItems: (IMenuItem | ISubmenuItem)[]) {
229
230 private _collectContextKeysAndSubmenuIds(item: IMenuItem | ISubmenuItem): void {
232 > MenuInfoSnapshot._fillInKbExprKeys(item.when, this._structureContextKeys);
233 >
234 > if (isIMenuItem(item)) {
235 > // keep precondition keys for event if applicable
236 > if (item.command.precondition) {
237 MenuInfoSnapshot._fillInKbExprKeys(item.command.precondition, this._preconditionContextKeys);
238 }
239 > // keep toggled keys for event if applicable menuService.ts
240 > if (item.command.toggled) {
241 const toggledExpression: ContextKeyExpression = (item.command.toggled as { condition: ContextKeyExpression }).condition || item.command.toggled;
242 MenuInfoSnapshot._fillInKbExprKeys(toggledExpression, this._toggledContextKeys);
243 }
245 > } else if (this._collectContextKeysForSubmenus) {
246 // recursively collect context keys from submenus so that this
247 // menu fires events when context key changes affect submenus
250 this._allMenuIds.add(item.submenu);
251 }
252 > } menuService.ts
253
254 private static _fillInKbExprKeys(exp: ContextKeyExpression | undefined, set: Set<string>): void {
255 > if (exp) { menuService.ts
256 for (const key of exp.keys()) {
257 set.add(key);
258 }
259 }
260 > } menuService.ts
261
262 }
265
266 constructor(
267 > _id: MenuId, menuService.ts
268 > private readonly _hiddenStates: PersistedMenuHideState,
269 > _collectContextKeysForSubmenus: boolean,
270 > @ICommandService private readonly _commandService: ICommandService,
271 > @IKeybindingService private readonly _keybindingService: IKeybindingService,
272 > @IContextKeyService private readonly _contextKeyService: IContextKeyService
273 > ) {
274 > super(_id, _collectContextKeysForSubmenus);
275 > this.refresh();
276 > }
277
278 createActionGroups(options: IMenuActionOptions | undefined): [string, Array<MenuItemAction | SubmenuItemAction>][] {
279 > const result: [string, Array<MenuItemAction | SubmenuItemAction>][] = []; menuService.ts
280 >
281 > for (const group of this._menuGroups) {
282 > const [id, items] = group;
283 >
284 > let activeActions: Array<MenuItemAction | SubmenuItemAction> | undefined;
285 > for (const item of items) {
286 > if (this._contextKeyService.contextMatchesRules(item.when)) {
287 > const isMenuItem = isIMenuItem(item);
288 > if (isMenuItem) {
289 > this._hiddenStates.setDefaultState(this._id, item.command.id, !!item.isHiddenByDefault);
290 > }
291 >
292 > const menuHide = createMenuHide(this._id, isMenuItem ? item.command : item, this._hiddenStates);
293 > if (isMenuItem) {
294 > // MenuItemAction
295 > const menuKeybinding = createConfigureKeybindingAction(this._commandService, this._keybindingService, item.command.id, item.when);
296 > (activeActions ??= []).push(new MenuItemAction(item.command, item.alt, options, menuHide, menuKeybinding, this._contextKeyService, this._commandService));
297 > } else {
298 // SubmenuItemAction
299 const groups = new MenuInfo(item.submenu, this._hiddenStates, this._collectContextKeysForSubmenus, this._commandService, this._keybindingService, this._contextKeyService).createActionGroups(options);
303 }
304 }
305 > } menuService.ts
306 > }
307 > if (activeActions && activeActions.length > 0) {
308 > result.push([id, activeActions]);
309 > }
310 > }
311 > return result;
312 > }
313
314 protected override _sort(menuItems: (IMenuItem | ISubmenuItem)[]): (IMenuItem | ISubmenuItem)[] {
315 > return menuItems.sort(MenuInfo._compareMenuItems); menuService.ts
316 > }
317
318 private static _compareMenuItems(a: IMenuItem | ISubmenuItem, b: IMenuItem | ISubmenuItem): number {
320 > const aGroup = a.group;
321 > const bGroup = b.group;
322 >
323 > if (aGroup !== bGroup) {
324
325 // Falsy groups come last
346 // sort on priority - default is 0
347 const aPrio = a.order || 0;
348 > const bPrio = b.order || 0; menuService.ts
349 > if (aPrio < bPrio) {
350 return -1;
351 } else if (aPrio > bPrio) {
355 // sort on titles
356 return MenuInfo._compareTitles(
357 > isIMenuItem(a) ? a.command.title : a.title, menuService.ts
358 > isIMenuItem(b) ? b.command.title : b.title
359 > );
360 > }
361
362 private static _compareTitles(a: string | ILocalizedString, b: string | ILocalizedString) {
376
377 constructor(
378 > id: MenuId, menuService.ts
379 > hiddenStates: PersistedMenuHideState,
380 > options: Required<IMenuCreateOptions>,
381 > @ICommandService commandService: ICommandService,
382 > @IKeybindingService keybindingService: IKeybindingService,
383 > @IContextKeyService contextKeyService: IContextKeyService
384 > ) {
385 > this._menuInfo = new MenuInfo(id, hiddenStates, options.emitEventsForSubmenuChanges, commandService, keybindingService, contextKeyService);
386 >
387 > // Rebuild this menu whenever the menu registry reports an event for this MenuId.
388 > // This usually happen while code and extensions are loaded and affects the over
389 > // structure of the menu
390 > const rebuildMenuSoon = new RunOnceScheduler(() => {
391 this._menuInfo.refresh();
392 this._onDidChange.fire({ menu: this, isStructuralChange: true, isEnablementChange: true, isToggleChange: true });
393 > }, options.eventDebounceDelay); menuService.ts
394 > this._disposables.add(rebuildMenuSoon);
395 > this._disposables.add(MenuRegistry.onDidChangeMenu(e => {
396 for (const id of this._menuInfo.allMenuIds) {
397 if (e.has(id)) {
400 }
401 }
402 > })); menuService.ts
403 >
404 > // When context keys or storage state changes we need to check if the menu also has changed. However,
405 > // we only do that when someone listens on this menu because (1) these events are
406 > // firing often and (2) menu are often leaked
407 > const lazyListener = this._disposables.add(new DisposableStore());
408 >
409 > const merge = (events: IMenuChangeEvent[]): IMenuChangeEvent => {
410
411 let isStructuralChange = false;
425 return { menu: this, isStructuralChange, isEnablementChange, isToggleChange };
426 };
428 > const startLazyListener = () => {
429
430 lazyListener.add(contextKeyService.onDidChangeContext(e => {
440 }));
441 };
443 > this._onDidChange = new DebounceEmitter({
444 > // start/stop context key listener
445 > onWillAddFirstListener: startLazyListener,
446 > onDidRemoveLastListener: lazyListener.clear.bind(lazyListener),
447 > delay: options.eventDebounceDelay,
448 > merge
449 > });
450 > this.onDidChange = this._onDidChange.event;
451 > }
452
453 getActions(options?: IMenuActionOptions | undefined): [string, (MenuItemAction | SubmenuItemAction)[]][] {
454 > return this._menuInfo.createActionGroups(options); menuService.ts
455 > }
456
457 dispose(): void {
458 > this._disposables.dispose(); menuService.ts
459 > this._onDidChange.dispose();
460 > }
461 }
462
463 > function createMenuHide(menu: MenuId, command: ICommandAction | ISubmenuItem, states: PersistedMenuHideState): IMenuItemHide { menuService.ts
464 >
465 > const id = isISubmenuItem(command) ? command.submenu.id : command.id;
466 > const title = typeof command.title === 'string' ? command.title : command.title.value;
467 >
468 > const hide = toAction({
469 > id: `hide/${menu.id}/${id}`,
470 > label: localize('hide.label', 'Hide \'{0}\'', title),
471 > run() { states.updateHidden(menu, id, true); }
472 > });
473 >
474 > const toggle = toAction({
475 > id: `toggle/${menu.id}/${id}`,
476 > label: title,
477 > get checked() { return !states.isHidden(menu, id); },
478 > run() { states.updateHidden(menu, id, !!this.checked); }
479 > });
480 >
481 > return {
482 > hide,
483 > toggle,
484 > get isHidden() { return !toggle.checked; },
485 > };
486 > }
487
488 export function createConfigureKeybindingAction(commandService: ICommandService, keybindingService: IKeybindingService, commandId: string, when: ContextKeyExpression | undefined = undefined, enabled = true): IAction {
489 > return toAction({ menuService.ts
490 > id: `configureKeybinding/${commandId}`,
491 > label: localize('configure keybinding', "Configure Keybinding"),
492 > enabled,
493 > run() {
494 // Only set the when clause when there is no keybinding
495 // It is possible that the action and the keybinding have different when clauses
498 commandService.executeCommand('workbench.action.openGlobalKeybindings', `@command:${commandId}` + (whenValue ? ` +when:${whenValue}` : ''));
499 }
500 > }); menuService.ts
501 > }
src/vs/platform/actions/common/actions.ts 38 introduced LOC · 4 ranges

Open complete file

60
61 export function isISubmenuItem(item: unknown): item is ISubmenuItem {
62 > return (item as ISubmenuItem).submenu !== undefined; actions.ts
63 > }
64
65 export class MenuId {
437
438 static merge(events: IMenuRegistryChangeEvent[]): IMenuRegistryChangeEvent {
439 > const ids = new Set<MenuId>(); actions.ts
440 > for (const item of events) {
441 > if (item instanceof MenuRegistryChangeEvent) {
442 > ids.add(item.id);
443 > }
444 > }
445 > return ids;
446 > }
447
448 readonly has: (id: MenuId) => boolean;
595
596 constructor(
597 > item: ICommandAction, actions.ts
598 > alt: ICommandAction | undefined,
599 > options: IMenuActionOptions | undefined,
600 > readonly hideActions: IMenuItemHide | undefined,
601 > readonly menuKeybinding: IAction | undefined,
602 > @IContextKeyService contextKeyService: IContextKeyService,
603 > @ICommandService private _commandService: ICommandService
604 > ) {
605 > this.id = item.id;
606 > this.label = MenuItemAction.label(item, options);
607 > this.tooltip = (typeof item.tooltip === 'string' ? item.tooltip : item.tooltip?.value) ?? '';
608 > this.enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition);
609 > this.checked = undefined;
610 >
611 > let icon: ThemeIcon | undefined;
612 >
613 > if (item.toggled) {
614 const toggled = ((item.toggled as { condition: ContextKeyExpression }).condition ? item.toggled : { condition: item.toggled }) as {
615 condition: ContextKeyExpression; icon?: Icon; tooltip?: string | ILocalizedString; title?: string | ILocalizedString;
628 }
629 }
630 > actions.ts
631 > if (!icon) {
632 > icon = ThemeIcon.isThemeIcon(item.icon) ? item.icon : undefined;
633 > }
634 >
635 > this.item = item;
636 > this.alt = alt ? new MenuItemAction(alt, undefined, options, hideActions, undefined, contextKeyService, _commandService) : undefined;
637 > this._options = options;
638 > this.class = icon && ThemeIcon.asClassName(icon);
639 >
640 > }
641
642 run(...args: unknown[]): Promise<void> {
src/vs/base/common/actions.ts 10 introduced LOC · 1 range

Open complete file

283
284 export function toAction(props: { id: string; label: string; tooltip?: string; enabled?: boolean; checked?: boolean; class?: string; run: Function }): IAction {
285 > return { actions.ts
286 > id: props.id,
287 > label: props.label,
288 > tooltip: props.tooltip ?? props.label,
289 > class: props.class,
290 > enabled: props.enabled ?? true,
291 > checked: props.checked,
292 > run: async (...args: unknown[]) => props.run(...args),
293 > };
294 > }