9cdbf9a5a57c8b05b201d92c266c4c64c5e9dc884e299219e8e835c37f868551e5dc8816255e176948d006a72af011b694280cf98161d4ffd1809fdeafb479 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  6. var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  7. if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  8. else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  9. return c > 3 && r && Object.defineProperty(target, key, r), r;
  10. };
  11. var __param = (this && this.__param) || function (paramIndex, decorator) {
  12. return function (target, key) { decorator(target, key, paramIndex); }
  13. };
  14. import { RunOnceScheduler } from '../../../base/common/async.js';
  15. import { Emitter } from '../../../base/common/event.js';
  16. import { DisposableStore } from '../../../base/common/lifecycle.js';
  17. import { IMenuService, isIMenuItem, MenuItemAction, MenuRegistry, SubmenuItemAction } from './actions.js';
  18. import { ICommandService } from '../../commands/common/commands.js';
  19. import { IContextKeyService } from '../../contextkey/common/contextkey.js';
  20. import { toAction } from '../../../base/common/actions.js';
  21. import { IStorageService } from '../../storage/common/storage.js';
  22. import { removeFastWithoutKeepingOrder } from '../../../base/common/arrays.js';
  23. import { localize } from '../../../nls.js';
  24. let MenuService = class MenuService {
  25. constructor(_commandService, storageService) {
  26. this._commandService = _commandService;
  27. this._hiddenStates = new PersistedMenuHideState(storageService);
  28. }
  29. createMenu(id, contextKeyService, options) {
  30. return new Menu(id, this._hiddenStates, Object.assign({ emitEventsForSubmenuChanges: false, eventDebounceDelay: 50 }, options), this._commandService, contextKeyService, this);
  31. }
  32. };
  33. MenuService = __decorate([
  34. __param(0, ICommandService),
  35. __param(1, IStorageService)
  36. ], MenuService);
  37. export { MenuService };
  38. let PersistedMenuHideState = class PersistedMenuHideState {
  39. constructor(_storageService) {
  40. this._storageService = _storageService;
  41. this._disposables = new DisposableStore();
  42. this._onDidChange = new Emitter();
  43. this.onDidChange = this._onDidChange.event;
  44. this._ignoreChangeEvent = false;
  45. try {
  46. const raw = _storageService.get(PersistedMenuHideState._key, 0 /* StorageScope.PROFILE */, '{}');
  47. this._data = JSON.parse(raw);
  48. }
  49. catch (err) {
  50. this._data = Object.create(null);
  51. }
  52. this._disposables.add(_storageService.onDidChangeValue(e => {
  53. if (e.key !== PersistedMenuHideState._key) {
  54. return;
  55. }
  56. if (!this._ignoreChangeEvent) {
  57. try {
  58. const raw = _storageService.get(PersistedMenuHideState._key, 0 /* StorageScope.PROFILE */, '{}');
  59. this._data = JSON.parse(raw);
  60. }
  61. catch (err) {
  62. console.log('FAILED to read storage after UPDATE', err);
  63. }
  64. }
  65. this._onDidChange.fire();
  66. }));
  67. }
  68. dispose() {
  69. this._onDidChange.dispose();
  70. this._disposables.dispose();
  71. }
  72. isHidden(menu, commandId) {
  73. var _a, _b;
  74. return (_b = (_a = this._data[menu.id]) === null || _a === void 0 ? void 0 : _a.includes(commandId)) !== null && _b !== void 0 ? _b : false;
  75. }
  76. updateHidden(menu, commandId, hidden) {
  77. const entries = this._data[menu.id];
  78. if (!hidden) {
  79. // remove and cleanup
  80. if (entries) {
  81. const idx = entries.indexOf(commandId);
  82. if (idx >= 0) {
  83. removeFastWithoutKeepingOrder(entries, idx);
  84. }
  85. if (entries.length === 0) {
  86. delete this._data[menu.id];
  87. }
  88. }
  89. }
  90. else {
  91. // add unless already added
  92. if (!entries) {
  93. this._data[menu.id] = [commandId];
  94. }
  95. else {
  96. const idx = entries.indexOf(commandId);
  97. if (idx < 0) {
  98. entries.push(commandId);
  99. }
  100. }
  101. }
  102. this._persist();
  103. }
  104. _persist() {
  105. try {
  106. this._ignoreChangeEvent = true;
  107. const raw = JSON.stringify(this._data);
  108. this._storageService.store(PersistedMenuHideState._key, raw, 0 /* StorageScope.PROFILE */, 0 /* StorageTarget.USER */);
  109. }
  110. finally {
  111. this._ignoreChangeEvent = false;
  112. }
  113. }
  114. };
  115. PersistedMenuHideState._key = 'menu.hiddenCommands';
  116. PersistedMenuHideState = __decorate([
  117. __param(0, IStorageService)
  118. ], PersistedMenuHideState);
  119. let Menu = class Menu {
  120. constructor(_id, _hiddenStates, _options, _commandService, _contextKeyService, _menuService) {
  121. this._id = _id;
  122. this._hiddenStates = _hiddenStates;
  123. this._options = _options;
  124. this._commandService = _commandService;
  125. this._contextKeyService = _contextKeyService;
  126. this._menuService = _menuService;
  127. this._disposables = new DisposableStore();
  128. this._menuGroups = [];
  129. this._contextKeys = new Set();
  130. this._build();
  131. // Rebuild this menu whenever the menu registry reports an event for this MenuId.
  132. // This usually happen while code and extensions are loaded and affects the over
  133. // structure of the menu
  134. const rebuildMenuSoon = new RunOnceScheduler(() => {
  135. this._build();
  136. this._onDidChange.fire(this);
  137. }, _options.eventDebounceDelay);
  138. this._disposables.add(rebuildMenuSoon);
  139. this._disposables.add(MenuRegistry.onDidChangeMenu(e => {
  140. if (e.has(_id)) {
  141. rebuildMenuSoon.schedule();
  142. }
  143. }));
  144. // When context keys or storage state changes we need to check if the menu also has changed. However,
  145. // we only do that when someone listens on this menu because (1) these events are
  146. // firing often and (2) menu are often leaked
  147. const lazyListener = this._disposables.add(new DisposableStore());
  148. const startLazyListener = () => {
  149. const fireChangeSoon = new RunOnceScheduler(() => this._onDidChange.fire(this), _options.eventDebounceDelay);
  150. lazyListener.add(fireChangeSoon);
  151. lazyListener.add(_contextKeyService.onDidChangeContext(e => {
  152. if (e.affectsSome(this._contextKeys)) {
  153. fireChangeSoon.schedule();
  154. }
  155. }));
  156. lazyListener.add(_hiddenStates.onDidChange(() => {
  157. fireChangeSoon.schedule();
  158. }));
  159. };
  160. this._onDidChange = new Emitter({
  161. // start/stop context key listener
  162. onFirstListenerAdd: startLazyListener,
  163. onLastListenerRemove: lazyListener.clear.bind(lazyListener)
  164. });
  165. this.onDidChange = this._onDidChange.event;
  166. }
  167. dispose() {
  168. this._disposables.dispose();
  169. this._onDidChange.dispose();
  170. }
  171. _build() {
  172. // reset
  173. this._menuGroups.length = 0;
  174. this._contextKeys.clear();
  175. const menuItems = MenuRegistry.getMenuItems(this._id);
  176. let group;
  177. menuItems.sort(Menu._compareMenuItems);
  178. for (const item of menuItems) {
  179. // group by groupId
  180. const groupName = item.group || '';
  181. if (!group || group[0] !== groupName) {
  182. group = [groupName, []];
  183. this._menuGroups.push(group);
  184. }
  185. group[1].push(item);
  186. // keep keys for eventing
  187. this._collectContextKeys(item);
  188. }
  189. }
  190. _collectContextKeys(item) {
  191. Menu._fillInKbExprKeys(item.when, this._contextKeys);
  192. if (isIMenuItem(item)) {
  193. // keep precondition keys for event if applicable
  194. if (item.command.precondition) {
  195. Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys);
  196. }
  197. // keep toggled keys for event if applicable
  198. if (item.command.toggled) {
  199. const toggledExpression = item.command.toggled.condition || item.command.toggled;
  200. Menu._fillInKbExprKeys(toggledExpression, this._contextKeys);
  201. }
  202. }
  203. else if (this._options.emitEventsForSubmenuChanges) {
  204. // recursively collect context keys from submenus so that this
  205. // menu fires events when context key changes affect submenus
  206. MenuRegistry.getMenuItems(item.submenu).forEach(this._collectContextKeys, this);
  207. }
  208. }
  209. getActions(options) {
  210. const result = [];
  211. const allToggleActions = [];
  212. for (const group of this._menuGroups) {
  213. const [id, items] = group;
  214. const toggleActions = [];
  215. const activeActions = [];
  216. for (const item of items) {
  217. if (this._contextKeyService.contextMatchesRules(item.when)) {
  218. let action;
  219. const isMenuItem = isIMenuItem(item);
  220. if (isMenuItem) {
  221. const menuHide = createMenuHide(this._id, item.command, this._hiddenStates);
  222. action = new MenuItemAction(item.command, item.alt, options, menuHide, this._contextKeyService, this._commandService);
  223. }
  224. else {
  225. action = new SubmenuItemAction(item, this._menuService, this._contextKeyService, options);
  226. if (action.actions.length === 0) {
  227. action.dispose();
  228. action = undefined;
  229. }
  230. }
  231. if (action) {
  232. activeActions.push(action);
  233. }
  234. }
  235. }
  236. if (activeActions.length > 0) {
  237. result.push([id, activeActions]);
  238. }
  239. if (toggleActions.length > 0) {
  240. allToggleActions.push(toggleActions);
  241. }
  242. }
  243. return result;
  244. }
  245. static _fillInKbExprKeys(exp, set) {
  246. if (exp) {
  247. for (const key of exp.keys()) {
  248. set.add(key);
  249. }
  250. }
  251. }
  252. static _compareMenuItems(a, b) {
  253. const aGroup = a.group;
  254. const bGroup = b.group;
  255. if (aGroup !== bGroup) {
  256. // Falsy groups come last
  257. if (!aGroup) {
  258. return 1;
  259. }
  260. else if (!bGroup) {
  261. return -1;
  262. }
  263. // 'navigation' group comes first
  264. if (aGroup === 'navigation') {
  265. return -1;
  266. }
  267. else if (bGroup === 'navigation') {
  268. return 1;
  269. }
  270. // lexical sort for groups
  271. const value = aGroup.localeCompare(bGroup);
  272. if (value !== 0) {
  273. return value;
  274. }
  275. }
  276. // sort on priority - default is 0
  277. const aPrio = a.order || 0;
  278. const bPrio = b.order || 0;
  279. if (aPrio < bPrio) {
  280. return -1;
  281. }
  282. else if (aPrio > bPrio) {
  283. return 1;
  284. }
  285. // sort on titles
  286. return Menu._compareTitles(isIMenuItem(a) ? a.command.title : a.title, isIMenuItem(b) ? b.command.title : b.title);
  287. }
  288. static _compareTitles(a, b) {
  289. const aStr = typeof a === 'string' ? a : a.original;
  290. const bStr = typeof b === 'string' ? b : b.original;
  291. return aStr.localeCompare(bStr);
  292. }
  293. };
  294. Menu = __decorate([
  295. __param(3, ICommandService),
  296. __param(4, IContextKeyService),
  297. __param(5, IMenuService)
  298. ], Menu);
  299. function createMenuHide(menu, command, states) {
  300. const id = `${menu.id}/${command.id}`;
  301. const title = typeof command.title === 'string' ? command.title : command.title.value;
  302. const hide = toAction({
  303. id,
  304. label: localize('hide.label', 'Hide \'{0}\'', title),
  305. run() { states.updateHidden(menu, command.id, true); }
  306. });
  307. const toggle = toAction({
  308. id,
  309. label: title,
  310. get checked() { return !states.isHidden(menu, command.id); },
  311. run() {
  312. const newValue = !states.isHidden(menu, command.id);
  313. states.updateHidden(menu, command.id, newValue);
  314. }
  315. });
  316. return {
  317. hide,
  318. toggle,
  319. get isHidden() { return !toggle.checked; },
  320. };
  321. }