index.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. JupyterLab, JupyterLabPlugin
  5. } from '@jupyterlab/application';
  6. import {
  7. each
  8. } from '@phosphor/algorithm';
  9. import {
  10. Menu, Widget
  11. } from '@phosphor/widgets';
  12. import {
  13. IMainMenu, IMenuExtender, EditMenu, FileMenu, KernelMenu,
  14. MainMenu, RunMenu, SettingsMenu, ViewMenu, TabsMenu
  15. } from '@jupyterlab/mainmenu';
  16. /**
  17. * A namespace for command IDs of semantic extension points.
  18. */
  19. export
  20. namespace CommandIDs {
  21. export
  22. const undo = 'editmenu:undo';
  23. export
  24. const redo = 'editmenu:redo';
  25. export
  26. const clearCurrent = 'editmenu:clear-current';
  27. export
  28. const clearAll = 'editmenu:clear-all';
  29. export
  30. const find = 'editmenu:find';
  31. export
  32. const findAndReplace = 'editmenu:find-and-replace';
  33. export
  34. const closeAndCleanup = 'filemenu:close-and-cleanup';
  35. export
  36. const createConsole = 'filemenu:create-console';
  37. export
  38. const interruptKernel = 'kernelmenu:interrupt';
  39. export
  40. const restartKernel = 'kernelmenu:restart';
  41. export
  42. const restartKernelAndClear = 'kernelmenu:restart-and-clear';
  43. export
  44. const changeKernel = 'kernelmenu:change';
  45. export
  46. const shutdownKernel = 'kernelmenu:shutdown';
  47. export
  48. const wordWrap = 'viewmenu:word-wrap';
  49. export
  50. const lineNumbering = 'viewmenu:line-numbering';
  51. export
  52. const matchBrackets = 'viewmenu:match-brackets';
  53. export
  54. const run = 'runmenu:run';
  55. export
  56. const runAll = 'runmenu:run-all';
  57. export
  58. const restartAndRunAll = 'runmenu:restart-and-run-all';
  59. export
  60. const runAbove = 'runmenu:run-above';
  61. export
  62. const runBelow = 'runmenu:run-below';
  63. }
  64. /**
  65. * A service providing an interface to the main menu.
  66. */
  67. const menuPlugin: JupyterLabPlugin<IMainMenu> = {
  68. id: '@jupyterlab/mainmenu-extension:plugin',
  69. provides: IMainMenu,
  70. activate: (app: JupyterLab): IMainMenu => {
  71. let menu = new MainMenu(app.commands);
  72. menu.id = 'jp-MainMenu';
  73. let logo = new Widget();
  74. logo.addClass('jp-MainAreaPortraitIcon');
  75. logo.addClass('jp-JupyterIcon');
  76. logo.id = 'jp-MainLogo';
  77. // Create the application menus.
  78. createEditMenu(app, menu.editMenu);
  79. createFileMenu(app, menu.fileMenu);
  80. createKernelMenu(app, menu.kernelMenu);
  81. createRunMenu(app, menu.runMenu);
  82. createSettingsMenu(app, menu.settingsMenu);
  83. createViewMenu(app, menu.viewMenu);
  84. createTabsMenu(app, menu.tabsMenu);
  85. app.shell.addToTopArea(logo);
  86. app.shell.addToTopArea(menu);
  87. return menu;
  88. }
  89. };
  90. /**
  91. * Create the basic `Edit` menu.
  92. */
  93. function createEditMenu(app: JupyterLab, menu: EditMenu): void {
  94. const commands = menu.menu.commands;
  95. // Add the undo/redo commands the the Edit menu.
  96. commands.addCommand(CommandIDs.undo, {
  97. label: 'Undo',
  98. isEnabled:
  99. Private.delegateEnabled(app, menu.undoers, 'undo'),
  100. execute:
  101. Private.delegateExecute(app, menu.undoers, 'undo')
  102. });
  103. commands.addCommand(CommandIDs.redo, {
  104. label: 'Redo',
  105. isEnabled:
  106. Private.delegateEnabled(app, menu.undoers, 'redo'),
  107. execute:
  108. Private.delegateExecute(app, menu.undoers, 'redo')
  109. });
  110. menu.addGroup([
  111. { command: CommandIDs.undo },
  112. { command: CommandIDs.redo }
  113. ], 0);
  114. // Add the clear commands to the Edit menu.
  115. commands.addCommand(CommandIDs.clearCurrent, {
  116. label: () => {
  117. const noun =
  118. Private.delegateLabel(app, menu.clearers, 'noun');
  119. const enabled =
  120. Private.delegateEnabled(app, menu.clearers, 'clearCurrent')();
  121. return `Clear${enabled ? ` ${noun}` : ''}`;
  122. },
  123. isEnabled:
  124. Private.delegateEnabled(app, menu.clearers, 'clearCurrent'),
  125. execute:
  126. Private.delegateExecute(app, menu.clearers, 'clearCurrent')
  127. });
  128. commands.addCommand(CommandIDs.clearAll, {
  129. label: () => {
  130. const noun = Private.delegateLabel(app, menu.clearers, 'noun');
  131. const enabled = Private.delegateEnabled(app, menu.clearers, 'clearAll')();
  132. return `Clear All${enabled ? ` ${noun}` : ''}`;
  133. },
  134. isEnabled:
  135. Private.delegateEnabled(app, menu.clearers, 'clearAll'),
  136. execute:
  137. Private.delegateExecute(app, menu.clearers, 'clearAll')
  138. });
  139. menu.addGroup([
  140. { command: CommandIDs.clearCurrent },
  141. { command: CommandIDs.clearAll },
  142. ], 10);
  143. // Add the find/replace commands the the Edit menu.
  144. commands.addCommand(CommandIDs.find, {
  145. label: 'Find…',
  146. isEnabled:
  147. Private.delegateEnabled(app, menu.findReplacers, 'find'),
  148. execute:
  149. Private.delegateExecute(app, menu.findReplacers, 'find')
  150. });
  151. commands.addCommand(CommandIDs.findAndReplace, {
  152. label: 'Find and Replace…',
  153. isEnabled:
  154. Private.delegateEnabled(app, menu.findReplacers, 'findAndReplace'),
  155. execute:
  156. Private.delegateExecute(app, menu.findReplacers, 'findAndReplace')
  157. });
  158. menu.addGroup([
  159. { command: CommandIDs.find },
  160. { command: CommandIDs.findAndReplace }
  161. ], 200);
  162. }
  163. /**
  164. * Create the basic `File` menu.
  165. */
  166. function createFileMenu(app: JupyterLab, menu: FileMenu): void {
  167. const commands = menu.menu.commands;
  168. // Add a delegator command for closing and cleaning up an activity.
  169. commands.addCommand(CommandIDs.closeAndCleanup, {
  170. label: () => {
  171. const action =
  172. Private.delegateLabel(app, menu.closeAndCleaners, 'action');
  173. const name =
  174. Private.delegateLabel(app, menu.closeAndCleaners, 'name');
  175. return `Close and ${action ? ` ${action} ${name}` : 'Shutdown'}`;
  176. },
  177. isEnabled:
  178. Private.delegateEnabled(app, menu.closeAndCleaners, 'closeAndCleanup'),
  179. execute:
  180. Private.delegateExecute(app, menu.closeAndCleaners, 'closeAndCleanup')
  181. });
  182. // Add a delegator command for creating a console for an activity.
  183. commands.addCommand(CommandIDs.createConsole, {
  184. label: () => {
  185. const name = Private.delegateLabel(app, menu.consoleCreators, 'name');
  186. const label = `New Console for ${name ? name : 'Activity' }`;
  187. return label;
  188. },
  189. isEnabled: Private.delegateEnabled(app, menu.consoleCreators, 'createConsole'),
  190. execute: Private.delegateExecute(app, menu.consoleCreators, 'createConsole')
  191. });
  192. // Add the new group
  193. const newGroup = [
  194. { type: 'submenu' as Menu.ItemType, submenu: menu.newMenu.menu },
  195. { command: 'filebrowser:create-main-launcher' },
  196. ];
  197. const newViewGroup = [
  198. { command: 'docmanager:clone' },
  199. { command: CommandIDs.createConsole }
  200. ];
  201. // Add the close group
  202. const closeGroup = [
  203. 'docmanager:close',
  204. 'filemenu:close-and-cleanup',
  205. 'docmanager:close-all-files'
  206. ].map(command => { return { command }; });
  207. // Add save group.
  208. const saveGroup = [
  209. 'docmanager:save',
  210. 'docmanager:save-as',
  211. 'docmanager:save-all'
  212. ].map(command => { return { command }; });
  213. // Add the re group.
  214. const reGroup = [
  215. 'docmanager:restore-checkpoint',
  216. 'docmanager:rename'
  217. ].map(command => { return { command }; });
  218. menu.addGroup(newGroup, 0);
  219. menu.addGroup(newViewGroup, 1);
  220. menu.addGroup(closeGroup, 2);
  221. menu.addGroup(saveGroup, 3);
  222. menu.addGroup(reGroup, 4);
  223. }
  224. /**
  225. * Create the basic `Kernel` menu.
  226. */
  227. function createKernelMenu(app: JupyterLab, menu: KernelMenu): void {
  228. const commands = menu.menu.commands;
  229. commands.addCommand(CommandIDs.interruptKernel, {
  230. label: 'Interrupt Kernel',
  231. isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'interruptKernel'),
  232. execute: Private.delegateExecute(app, menu.kernelUsers, 'interruptKernel')
  233. });
  234. commands.addCommand(CommandIDs.restartKernel, {
  235. label: 'Restart Kernel…',
  236. isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'restartKernel'),
  237. execute: Private.delegateExecute(app, menu.kernelUsers, 'restartKernel')
  238. });
  239. commands.addCommand(CommandIDs.restartKernelAndClear, {
  240. label: () => {
  241. const noun = Private.delegateLabel(app, menu.kernelUsers, 'noun');
  242. const enabled =
  243. Private.delegateEnabled(app, menu.kernelUsers, 'restartKernelAndClear')();
  244. return `Restart Kernel and Clear${enabled ? ` ${noun}` : ''}…`;
  245. },
  246. isEnabled:
  247. Private.delegateEnabled(app, menu.kernelUsers, 'restartKernelAndClear'),
  248. execute:
  249. Private.delegateExecute(app, menu.kernelUsers, 'restartKernelAndClear')
  250. });
  251. commands.addCommand(CommandIDs.changeKernel, {
  252. label: 'Change Kernel…',
  253. isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'changeKernel'),
  254. execute: Private.delegateExecute(app, menu.kernelUsers, 'changeKernel')
  255. });
  256. commands.addCommand(CommandIDs.shutdownKernel, {
  257. label: 'Shutdown Kernel',
  258. isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'shutdownKernel'),
  259. execute: Private.delegateExecute(app, menu.kernelUsers, 'shutdownKernel')
  260. });
  261. const restartGroup = [
  262. CommandIDs.restartKernel,
  263. CommandIDs.restartKernelAndClear,
  264. CommandIDs.restartAndRunAll,
  265. ].map(command => { return { command }; });
  266. menu.addGroup([{ command: CommandIDs.interruptKernel }], 0);
  267. menu.addGroup(restartGroup, 1);
  268. menu.addGroup([{ command: CommandIDs.shutdownKernel }], 2);
  269. menu.addGroup([{ command: CommandIDs.changeKernel }], 3);
  270. }
  271. /**
  272. * Create the basic `View` menu.
  273. */
  274. function createViewMenu(app: JupyterLab, menu: ViewMenu): void {
  275. const commands = menu.menu.commands;
  276. commands.addCommand(CommandIDs.lineNumbering, {
  277. label: 'Show Line Numbers',
  278. isEnabled: Private.delegateEnabled(app, menu.editorViewers, 'toggleLineNumbers'),
  279. isToggled: Private.delegateToggled(app, menu.editorViewers, 'lineNumbersToggled'),
  280. execute: Private.delegateExecute(app, menu.editorViewers, 'toggleLineNumbers')
  281. });
  282. commands.addCommand(CommandIDs.matchBrackets, {
  283. label: 'Match Brackets',
  284. isEnabled: Private.delegateEnabled(app, menu.editorViewers, 'toggleMatchBrackets'),
  285. isToggled: Private.delegateToggled(app, menu.editorViewers, 'matchBracketsToggled'),
  286. execute: Private.delegateExecute(app, menu.editorViewers, 'toggleMatchBrackets')
  287. });
  288. commands.addCommand(CommandIDs.wordWrap, {
  289. label: 'Wrap Words',
  290. isEnabled: Private.delegateEnabled(app, menu.editorViewers, 'toggleWordWrap'),
  291. isToggled: Private.delegateToggled(app, menu.editorViewers, 'wordWrapToggled'),
  292. execute: Private.delegateExecute(app, menu.editorViewers, 'toggleWordWrap')
  293. });
  294. menu.addGroup([
  295. { command: 'application:toggle-left-area' },
  296. { command: 'application:toggle-right-area' }
  297. ], 0);
  298. const editorViewerGroup = [
  299. CommandIDs.lineNumbering,
  300. CommandIDs.matchBrackets,
  301. CommandIDs.wordWrap
  302. ].map( command => { return { command }; });
  303. menu.addGroup(editorViewerGroup, 10);
  304. // Add the command for toggling single-document mode.
  305. menu.addGroup([{ command: 'application:toggle-mode' }], 1000);
  306. }
  307. function createRunMenu(app: JupyterLab, menu: RunMenu): void {
  308. const commands = menu.menu.commands;
  309. commands.addCommand(CommandIDs.run, {
  310. label: () => {
  311. const noun = Private.delegateLabel(app, menu.codeRunners, 'noun');
  312. const enabled =
  313. Private.delegateEnabled(app, menu.codeRunners, 'run')();
  314. return `Run Selected${enabled ? ` ${noun}` : ''}`;
  315. },
  316. isEnabled: Private.delegateEnabled(app, menu.codeRunners, 'run'),
  317. execute: Private.delegateExecute(app, menu.codeRunners, 'run')
  318. });
  319. commands.addCommand(CommandIDs.runAll, {
  320. label: () => {
  321. const noun = Private.delegateLabel(app, menu.codeRunners, 'noun');
  322. const enabled =
  323. Private.delegateEnabled(app, menu.codeRunners, 'runAll')();
  324. return `Run All${enabled ? ` ${noun}` : ''}`;
  325. },
  326. isEnabled: Private.delegateEnabled(app, menu.codeRunners, 'runAll'),
  327. execute: Private.delegateExecute(app, menu.codeRunners, 'runAll')
  328. });
  329. commands.addCommand(CommandIDs.restartAndRunAll, {
  330. label: () => {
  331. const noun = Private.delegateLabel(app, menu.codeRunners, 'noun');
  332. const enabled =
  333. Private.delegateEnabled(app, menu.codeRunners, 'restartAndRunAll')();
  334. return `Restart Kernel and Run All${enabled ? ` ${noun}` : ''}…`;
  335. },
  336. isEnabled: Private.delegateEnabled(app, menu.codeRunners, 'restartAndRunAll'),
  337. execute: Private.delegateExecute(app, menu.codeRunners, 'restartAndRunAll')
  338. });
  339. const runAllGroup = [
  340. CommandIDs.runAll,
  341. CommandIDs.restartAndRunAll
  342. ].map( command => { return { command }; });
  343. menu.addGroup([{ command: CommandIDs.run }], 0);
  344. menu.addGroup(runAllGroup, 999);
  345. }
  346. function createSettingsMenu(app: JupyterLab, menu: SettingsMenu): void {
  347. menu.addGroup([{ command: 'settingeditor:open' }], 1000);
  348. }
  349. function createTabsMenu(app: JupyterLab, menu: TabsMenu): void {
  350. const commands = app.commands;
  351. // Add commands for cycling the active tabs.
  352. menu.addGroup([
  353. { command: 'application:activate-next-tab' },
  354. { command: 'application:activate-previous-tab' }
  355. ], 0);
  356. let tabGroup: Menu.IItemOptions[] = [];
  357. // Utility function to create a command to activate
  358. // a given tab, or get it if it already exists.
  359. const createMenuItem = (widget: Widget): Menu.IItemOptions => {
  360. const commandID = `tabmenu:activate-${widget.id}`;
  361. if (!commands.hasCommand(commandID)) {
  362. commands.addCommand(commandID, {
  363. label: () => widget.title.label,
  364. isVisible: () => !widget.isDisposed,
  365. isEnabled: () => !widget.isDisposed,
  366. isToggled: () => app.shell.currentWidget === widget,
  367. execute: () => app.shell.activateById(widget.id)
  368. });
  369. }
  370. return { command: commandID };
  371. };
  372. app.restored.then(() => {
  373. // Iterate over the current widgets in the
  374. // main area, and add them to the tab group
  375. // of the menu.
  376. const populateTabs = () => {
  377. menu.removeGroup(tabGroup);
  378. tabGroup.length = 0;
  379. each(app.shell.widgets('main'), widget => {
  380. tabGroup.push(createMenuItem(widget));
  381. });
  382. menu.addGroup(tabGroup, 1);
  383. };
  384. populateTabs();
  385. app.shell.layoutModified.connect(() => { populateTabs(); });
  386. });
  387. }
  388. export default menuPlugin;
  389. /**
  390. * A namespace for Private data.
  391. */
  392. namespace Private {
  393. /**
  394. * Given a widget and a set containing IMenuExtenders,
  395. * check the tracker and return the extender, if any,
  396. * that holds the widget.
  397. */
  398. function findExtender<E extends IMenuExtender<Widget>>(widget: Widget, s: Set<E>): E {
  399. let extender: E;
  400. s.forEach(value => {
  401. if (value.tracker.has(widget)) {
  402. extender = value;
  403. }
  404. });
  405. return extender;
  406. }
  407. /**
  408. * A utility function that delegates a portion of a label to an IMenuExtender.
  409. */
  410. export
  411. function delegateLabel<E extends IMenuExtender<Widget>>(app: JupyterLab, s: Set<E>, label: keyof E): string {
  412. let widget = app.shell.currentWidget;
  413. const extender = findExtender(widget, s);
  414. if (!extender) {
  415. return '';
  416. }
  417. return extender[label];
  418. }
  419. /**
  420. * A utility function that delegates command execution
  421. * to an IMenuExtender.
  422. */
  423. export
  424. function delegateExecute<E extends IMenuExtender<Widget>>(app: JupyterLab, s: Set<E>, executor: keyof E): () => Promise<any> {
  425. return () => {
  426. let widget = app.shell.currentWidget;
  427. const extender = findExtender(widget, s);
  428. if (!extender) {
  429. return Promise.resolve(void 0);
  430. }
  431. return extender[executor](widget);
  432. };
  433. }
  434. /**
  435. * A utility function that delegates whether a command is enabled
  436. * to an IMenuExtender.
  437. */
  438. export
  439. function delegateEnabled<E extends IMenuExtender<Widget>>(app: JupyterLab, s: Set<E>, executor: keyof E): () => boolean {
  440. return () => {
  441. let widget = app.shell.currentWidget;
  442. const extender = findExtender(widget, s);
  443. return !!extender && !!extender[executor] &&
  444. (extender.isEnabled ? extender.isEnabled(widget) : true);
  445. };
  446. }
  447. /**
  448. * A utility function that delegates whether a command is toggled
  449. * for an IMenuExtender.
  450. */
  451. export
  452. function delegateToggled<E extends IMenuExtender<Widget>>(app: JupyterLab, s: Set<E>, toggled: keyof E): () => boolean {
  453. return () => {
  454. let widget = app.shell.currentWidget;
  455. const extender = findExtender(widget, s);
  456. return !!extender && !!extender[toggled] && !!extender[toggled](widget);
  457. };
  458. }
  459. }