index.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { each, find } from '@lumino/algorithm';
  4. import { IDisposable } from '@lumino/disposable';
  5. import { Menu, Widget } from '@lumino/widgets';
  6. import {
  7. ILabShell,
  8. JupyterFrontEnd,
  9. JupyterFrontEndPlugin,
  10. IRouter
  11. } from '@jupyterlab/application';
  12. import { ICommandPalette, showDialog, Dialog } from '@jupyterlab/apputils';
  13. import { PageConfig, URLExt } from '@jupyterlab/coreutils';
  14. import {
  15. IMainMenu,
  16. IMenuExtender,
  17. EditMenu,
  18. FileMenu,
  19. HelpMenu,
  20. KernelMenu,
  21. MainMenu,
  22. RunMenu,
  23. SettingsMenu,
  24. ViewMenu,
  25. TabsMenu
  26. } from '@jupyterlab/mainmenu';
  27. import { ServerConnection } from '@jupyterlab/services';
  28. import { ITranslator, TranslationBundle } from '@jupyterlab/translation';
  29. import { jupyterIcon } from '@jupyterlab/ui-components';
  30. /**
  31. * A namespace for command IDs of semantic extension points.
  32. */
  33. export namespace CommandIDs {
  34. export const openEdit = 'editmenu:open';
  35. export const undo = 'editmenu:undo';
  36. export const redo = 'editmenu:redo';
  37. export const clearCurrent = 'editmenu:clear-current';
  38. export const clearAll = 'editmenu:clear-all';
  39. export const find = 'editmenu:find';
  40. export const goToLine = 'editmenu:go-to-line';
  41. export const openFile = 'filemenu:open';
  42. export const closeAndCleanup = 'filemenu:close-and-cleanup';
  43. export const createConsole = 'filemenu:create-console';
  44. export const shutdown = 'filemenu:shutdown';
  45. export const logout = 'filemenu:logout';
  46. export const openKernel = 'kernelmenu:open';
  47. export const interruptKernel = 'kernelmenu:interrupt';
  48. export const restartKernel = 'kernelmenu:restart';
  49. export const restartKernelAndClear = 'kernelmenu:restart-and-clear';
  50. export const restartAndRunToSelected = 'notebook:restart-and-run-to-selected';
  51. export const changeKernel = 'kernelmenu:change';
  52. export const shutdownKernel = 'kernelmenu:shutdown';
  53. export const shutdownAllKernels = 'kernelmenu:shutdownAll';
  54. export const openView = 'viewmenu:open';
  55. export const wordWrap = 'viewmenu:word-wrap';
  56. export const lineNumbering = 'viewmenu:line-numbering';
  57. export const matchBrackets = 'viewmenu:match-brackets';
  58. export const openRun = 'runmenu:open';
  59. export const run = 'runmenu:run';
  60. export const runAll = 'runmenu:run-all';
  61. export const restartAndRunAll = 'runmenu:restart-and-run-all';
  62. export const runAbove = 'runmenu:run-above';
  63. export const runBelow = 'runmenu:run-below';
  64. export const openTabs = 'tabsmenu:open';
  65. export const activateById = 'tabsmenu:activate-by-id';
  66. export const activatePreviouslyUsedTab =
  67. 'tabsmenu:activate-previously-used-tab';
  68. export const openSettings = 'settingsmenu:open';
  69. export const openHelp = 'helpmenu:open';
  70. export const openFirst = 'mainmenu:open-first';
  71. }
  72. /**
  73. * A service providing an interface to the main menu.
  74. */
  75. const plugin: JupyterFrontEndPlugin<IMainMenu> = {
  76. id: '@jupyterlab/mainmenu-extension:plugin',
  77. requires: [IRouter, ITranslator],
  78. optional: [ICommandPalette, ILabShell],
  79. provides: IMainMenu,
  80. activate: (
  81. app: JupyterFrontEnd,
  82. router: IRouter,
  83. translator: ITranslator,
  84. palette: ICommandPalette | null,
  85. labShell: ILabShell | null
  86. ): IMainMenu => {
  87. const { commands } = app;
  88. const trans = translator.load('jupyterlab');
  89. const menu = new MainMenu(commands);
  90. menu.id = 'jp-MainMenu';
  91. const logo = new Widget();
  92. jupyterIcon.element({
  93. container: logo.node,
  94. elementPosition: 'center',
  95. margin: '2px 2px 2px 8px',
  96. height: 'auto',
  97. width: '16px'
  98. });
  99. logo.id = 'jp-MainLogo';
  100. // Only add quit button if the back-end supports it by checking page config.
  101. const quitButton = PageConfig.getOption('quitButton').toLowerCase();
  102. menu.fileMenu.quitEntry = quitButton === 'true';
  103. // Create the application menus.
  104. createEditMenu(app, menu.editMenu, trans);
  105. createFileMenu(app, menu.fileMenu, router, trans);
  106. createKernelMenu(app, menu.kernelMenu, trans);
  107. createRunMenu(app, menu.runMenu, trans);
  108. createSettingsMenu(app, menu.settingsMenu, trans);
  109. createViewMenu(app, menu.viewMenu, trans);
  110. createHelpMenu(app, menu.helpMenu, trans);
  111. // The tabs menu relies on lab shell functionality.
  112. if (labShell) {
  113. createTabsMenu(app, menu.tabsMenu, labShell, trans);
  114. }
  115. // Create commands to open the main application menus.
  116. const activateMenu = (item: Menu) => {
  117. menu.activeMenu = item;
  118. menu.openActiveMenu();
  119. };
  120. commands.addCommand(CommandIDs.openEdit, {
  121. label: trans.__('Open Edit Menu'),
  122. execute: () => activateMenu(menu.editMenu.menu)
  123. });
  124. commands.addCommand(CommandIDs.openFile, {
  125. label: trans.__('Open File Menu'),
  126. execute: () => activateMenu(menu.fileMenu.menu)
  127. });
  128. commands.addCommand(CommandIDs.openKernel, {
  129. label: trans.__('Open Kernel Menu'),
  130. execute: () => activateMenu(menu.kernelMenu.menu)
  131. });
  132. commands.addCommand(CommandIDs.openRun, {
  133. label: trans.__('Open Run Menu'),
  134. execute: () => activateMenu(menu.runMenu.menu)
  135. });
  136. commands.addCommand(CommandIDs.openView, {
  137. label: trans.__('Open View Menu'),
  138. execute: () => activateMenu(menu.viewMenu.menu)
  139. });
  140. commands.addCommand(CommandIDs.openSettings, {
  141. label: trans.__('Open Settings Menu'),
  142. execute: () => activateMenu(menu.settingsMenu.menu)
  143. });
  144. commands.addCommand(CommandIDs.openTabs, {
  145. label: trans.__('Open Tabs Menu'),
  146. execute: () => activateMenu(menu.tabsMenu.menu)
  147. });
  148. commands.addCommand(CommandIDs.openHelp, {
  149. label: trans.__('Open Help Menu'),
  150. execute: () => activateMenu(menu.helpMenu.menu)
  151. });
  152. commands.addCommand(CommandIDs.openFirst, {
  153. label: trans.__('Open First Menu'),
  154. execute: () => {
  155. menu.activeIndex = 0;
  156. menu.openActiveMenu();
  157. }
  158. });
  159. if (palette) {
  160. // Add some of the commands defined here to the command palette.
  161. if (menu.fileMenu.quitEntry) {
  162. palette.addItem({
  163. command: CommandIDs.shutdown,
  164. category: trans.__('Main Area')
  165. });
  166. palette.addItem({
  167. command: CommandIDs.logout,
  168. category: trans.__('Main Area')
  169. });
  170. }
  171. palette.addItem({
  172. command: CommandIDs.shutdownAllKernels,
  173. category: trans.__('Kernel Operations')
  174. });
  175. palette.addItem({
  176. command: CommandIDs.activatePreviouslyUsedTab,
  177. category: trans.__('Main Area')
  178. });
  179. }
  180. app.shell.add(logo, 'top');
  181. app.shell.add(menu, 'top');
  182. return menu;
  183. }
  184. };
  185. /**
  186. * Create the basic `Edit` menu.
  187. */
  188. export function createEditMenu(
  189. app: JupyterFrontEnd,
  190. menu: EditMenu,
  191. trans: TranslationBundle
  192. ): void {
  193. const commands = menu.menu.commands;
  194. menu.menu.title.label = trans.__('Edit');
  195. // Add the undo/redo commands the the Edit menu.
  196. commands.addCommand(CommandIDs.undo, {
  197. label: trans.__('Undo'),
  198. isEnabled: Private.delegateEnabled(app, menu.undoers, 'undo'),
  199. execute: Private.delegateExecute(app, menu.undoers, 'undo')
  200. });
  201. commands.addCommand(CommandIDs.redo, {
  202. label: trans.__('Redo'),
  203. isEnabled: Private.delegateEnabled(app, menu.undoers, 'redo'),
  204. execute: Private.delegateExecute(app, menu.undoers, 'redo')
  205. });
  206. menu.addGroup(
  207. [{ command: CommandIDs.undo }, { command: CommandIDs.redo }],
  208. 0
  209. );
  210. // Add the clear commands to the Edit menu.
  211. commands.addCommand(CommandIDs.clearCurrent, {
  212. label: () => {
  213. const enabled = Private.delegateEnabled(
  214. app,
  215. menu.clearers,
  216. 'clearCurrent'
  217. )();
  218. let localizedLabel = trans.__('Clear');
  219. if (enabled) {
  220. localizedLabel = Private.delegateLabel(
  221. app,
  222. menu.clearers,
  223. 'clearCurrentLabel'
  224. );
  225. }
  226. return localizedLabel;
  227. },
  228. isEnabled: Private.delegateEnabled(app, menu.clearers, 'clearCurrent'),
  229. execute: Private.delegateExecute(app, menu.clearers, 'clearCurrent')
  230. });
  231. commands.addCommand(CommandIDs.clearAll, {
  232. label: () => {
  233. const enabled = Private.delegateEnabled(app, menu.clearers, 'clearAll')();
  234. let localizedLabel = trans.__('Clear All');
  235. if (enabled) {
  236. localizedLabel = Private.delegateLabel(
  237. app,
  238. menu.clearers,
  239. 'clearAllLabel'
  240. );
  241. }
  242. return localizedLabel;
  243. },
  244. isEnabled: Private.delegateEnabled(app, menu.clearers, 'clearAll'),
  245. execute: Private.delegateExecute(app, menu.clearers, 'clearAll')
  246. });
  247. menu.addGroup(
  248. [{ command: CommandIDs.clearCurrent }, { command: CommandIDs.clearAll }],
  249. 10
  250. );
  251. commands.addCommand(CommandIDs.goToLine, {
  252. label: trans.__('Go to Line…'),
  253. isEnabled: Private.delegateEnabled(app, menu.goToLiners, 'goToLine'),
  254. execute: Private.delegateExecute(app, menu.goToLiners, 'goToLine')
  255. });
  256. menu.addGroup([{ command: CommandIDs.goToLine }], 200);
  257. }
  258. /**
  259. * Create the basic `File` menu.
  260. */
  261. export function createFileMenu(
  262. app: JupyterFrontEnd,
  263. menu: FileMenu,
  264. router: IRouter,
  265. trans: TranslationBundle
  266. ): void {
  267. const commands = menu.menu.commands;
  268. menu.menu.title.label = trans.__('File');
  269. menu.newMenu.menu.title.label = trans.__('New');
  270. // Add a delegator command for closing and cleaning up an activity.
  271. // This one is a bit different, in that we consider it enabled
  272. // even if it cannot find a delegate for the activity.
  273. // In that case, we instead call the application `close` command.
  274. commands.addCommand(CommandIDs.closeAndCleanup, {
  275. label: () => {
  276. const localizedLabel = Private.delegateLabel(
  277. app,
  278. menu.closeAndCleaners,
  279. 'closeAndCleanupLabel'
  280. );
  281. return localizedLabel ? localizedLabel : trans.__('Close and Shutdown');
  282. },
  283. isEnabled: () =>
  284. !!app.shell.currentWidget && !!app.shell.currentWidget.title.closable,
  285. execute: () => {
  286. // Check if we have a registered delegate. If so, call that.
  287. if (
  288. Private.delegateEnabled(app, menu.closeAndCleaners, 'closeAndCleanup')()
  289. ) {
  290. return Private.delegateExecute(
  291. app,
  292. menu.closeAndCleaners,
  293. 'closeAndCleanup'
  294. )();
  295. }
  296. // If we have no delegate, call the top-level application close.
  297. return app.commands.execute('application:close');
  298. }
  299. });
  300. // Add a delegator command for creating a console for an activity.
  301. commands.addCommand(CommandIDs.createConsole, {
  302. label: () => {
  303. const localizedLabel = Private.delegateLabel(
  304. app,
  305. menu.consoleCreators,
  306. 'createConsoleLabel'
  307. );
  308. return localizedLabel
  309. ? localizedLabel
  310. : trans.__('New Console for Activity');
  311. },
  312. isEnabled: Private.delegateEnabled(
  313. app,
  314. menu.consoleCreators,
  315. 'createConsole'
  316. ),
  317. execute: Private.delegateExecute(app, menu.consoleCreators, 'createConsole')
  318. });
  319. commands.addCommand(CommandIDs.shutdown, {
  320. label: trans.__('Shut Down'),
  321. caption: trans.__('Shut down JupyterLab'),
  322. execute: () => {
  323. return showDialog({
  324. title: trans.__('Shutdown confirmation'),
  325. body: trans.__('Please confirm you want to shut down JupyterLab.'),
  326. buttons: [
  327. Dialog.cancelButton(),
  328. Dialog.warnButton({ label: trans.__('Shut Down') })
  329. ]
  330. }).then(result => {
  331. if (result.button.accept) {
  332. const setting = ServerConnection.makeSettings();
  333. const apiURL = URLExt.join(setting.baseUrl, 'api/shutdown');
  334. return ServerConnection.makeRequest(
  335. apiURL,
  336. { method: 'POST' },
  337. setting
  338. )
  339. .then(result => {
  340. if (result.ok) {
  341. // Close this window if the shutdown request has been successful
  342. const body = document.createElement('div');
  343. const p1 = document.createElement('p');
  344. p1.textContent = trans.__(
  345. 'You have shut down the Jupyter server. You can now close this tab.'
  346. );
  347. const p2 = document.createElement('p');
  348. p2.textContent = trans.__(
  349. 'To use JupyterLab again, you will need to relaunch it.'
  350. );
  351. body.appendChild(p1);
  352. body.appendChild(p2);
  353. void showDialog({
  354. title: trans.__('Server stopped'),
  355. body: new Widget({ node: body }),
  356. buttons: []
  357. });
  358. window.close();
  359. } else {
  360. throw new ServerConnection.ResponseError(result);
  361. }
  362. })
  363. .catch(data => {
  364. throw new ServerConnection.NetworkError(data);
  365. });
  366. }
  367. });
  368. }
  369. });
  370. commands.addCommand(CommandIDs.logout, {
  371. label: trans.__('Log Out'),
  372. caption: trans.__('Log out of JupyterLab'),
  373. execute: () => {
  374. router.navigate('/logout', { hard: true });
  375. }
  376. });
  377. // Add the new group
  378. const newGroup = [
  379. { type: 'submenu' as Menu.ItemType, submenu: menu.newMenu.menu },
  380. { command: 'filebrowser:create-main-launcher' }
  381. ];
  382. const openGroup = [{ command: 'filebrowser:open-path' }];
  383. const newViewGroup = [
  384. { command: 'docmanager:clone' },
  385. { command: CommandIDs.createConsole }
  386. ].filter(item => !!item);
  387. // Add the close group
  388. const closeGroup = [
  389. 'application:close',
  390. 'filemenu:close-and-cleanup',
  391. 'application:close-all'
  392. ].map(command => {
  393. return { command };
  394. });
  395. // Add save group.
  396. const saveGroup = [
  397. 'docmanager:save',
  398. 'docmanager:save-as',
  399. 'docmanager:save-all'
  400. ].map(command => {
  401. return { command };
  402. });
  403. // Add the re group.
  404. const reGroup = [
  405. 'docmanager:reload',
  406. 'docmanager:restore-checkpoint',
  407. 'docmanager:rename'
  408. ].map(command => {
  409. return { command };
  410. });
  411. // Add the quit group.
  412. const quitGroup = [
  413. { command: 'filemenu:logout' },
  414. { command: 'filemenu:shutdown' }
  415. ];
  416. const printGroup = [{ command: 'apputils:print' }];
  417. menu.addGroup(newGroup, 0);
  418. menu.addGroup(openGroup, 1);
  419. menu.addGroup(newViewGroup, 2);
  420. menu.addGroup(closeGroup, 3);
  421. menu.addGroup(saveGroup, 4);
  422. menu.addGroup(reGroup, 5);
  423. menu.addGroup(printGroup, 98);
  424. if (menu.quitEntry) {
  425. menu.addGroup(quitGroup, 99);
  426. }
  427. }
  428. /**
  429. * Create the basic `Kernel` menu.
  430. */
  431. export function createKernelMenu(
  432. app: JupyterFrontEnd,
  433. menu: KernelMenu,
  434. trans: TranslationBundle
  435. ): void {
  436. const commands = menu.menu.commands;
  437. menu.menu.title.label = trans.__('Kernel');
  438. commands.addCommand(CommandIDs.interruptKernel, {
  439. label: trans.__('Interrupt Kernel'),
  440. isEnabled: Private.delegateEnabled(
  441. app,
  442. menu.kernelUsers,
  443. 'interruptKernel'
  444. ),
  445. execute: Private.delegateExecute(app, menu.kernelUsers, 'interruptKernel')
  446. });
  447. commands.addCommand(CommandIDs.restartKernel, {
  448. label: trans.__('Restart Kernel…'),
  449. isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'restartKernel'),
  450. execute: Private.delegateExecute(app, menu.kernelUsers, 'restartKernel')
  451. });
  452. commands.addCommand(CommandIDs.restartKernelAndClear, {
  453. label: () => {
  454. const enabled = Private.delegateEnabled(
  455. app,
  456. menu.kernelUsers,
  457. 'restartKernelAndClear'
  458. )();
  459. let localizedLabel = trans.__('Restart Kernel and Clear…');
  460. if (enabled) {
  461. localizedLabel = Private.delegateLabel(
  462. app,
  463. menu.kernelUsers,
  464. 'restartKernelAndClearLabel'
  465. );
  466. }
  467. return localizedLabel;
  468. },
  469. isEnabled: Private.delegateEnabled(
  470. app,
  471. menu.kernelUsers,
  472. 'restartKernelAndClear'
  473. ),
  474. execute: Private.delegateExecute(
  475. app,
  476. menu.kernelUsers,
  477. 'restartKernelAndClear'
  478. )
  479. });
  480. commands.addCommand(CommandIDs.changeKernel, {
  481. label: trans.__('Change Kernel…'),
  482. isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'changeKernel'),
  483. execute: Private.delegateExecute(app, menu.kernelUsers, 'changeKernel')
  484. });
  485. commands.addCommand(CommandIDs.shutdownKernel, {
  486. label: trans.__('Shut Down Kernel'),
  487. isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'shutdownKernel'),
  488. execute: Private.delegateExecute(app, menu.kernelUsers, 'shutdownKernel')
  489. });
  490. commands.addCommand(CommandIDs.shutdownAllKernels, {
  491. label: trans.__('Shut Down All Kernels…'),
  492. isEnabled: () => {
  493. return app.serviceManager.sessions.running().next() !== undefined;
  494. },
  495. execute: () => {
  496. return showDialog({
  497. title: trans.__('Shut Down All?'),
  498. body: trans.__('Shut down all kernels?'),
  499. buttons: [
  500. Dialog.cancelButton({ label: trans.__('Dismiss') }),
  501. Dialog.warnButton({ label: trans.__('Shut Down All') })
  502. ]
  503. }).then(result => {
  504. if (result.button.accept) {
  505. return app.serviceManager.sessions.shutdownAll();
  506. }
  507. });
  508. }
  509. });
  510. const restartGroup = [
  511. CommandIDs.restartKernel,
  512. CommandIDs.restartKernelAndClear,
  513. CommandIDs.restartAndRunToSelected,
  514. CommandIDs.restartAndRunAll
  515. ].map(command => {
  516. return { command };
  517. });
  518. menu.addGroup([{ command: CommandIDs.interruptKernel }], 0);
  519. menu.addGroup(restartGroup, 1);
  520. menu.addGroup(
  521. [
  522. { command: CommandIDs.shutdownKernel },
  523. { command: CommandIDs.shutdownAllKernels }
  524. ],
  525. 2
  526. );
  527. menu.addGroup([{ command: CommandIDs.changeKernel }], 3);
  528. }
  529. /**
  530. * Create the basic `View` menu.
  531. */
  532. export function createViewMenu(
  533. app: JupyterFrontEnd,
  534. menu: ViewMenu,
  535. trans: TranslationBundle
  536. ): void {
  537. const commands = menu.menu.commands;
  538. menu.menu.title.label = trans.__('View');
  539. commands.addCommand(CommandIDs.lineNumbering, {
  540. label: trans.__('Show Line Numbers'),
  541. isEnabled: Private.delegateEnabled(
  542. app,
  543. menu.editorViewers,
  544. 'toggleLineNumbers'
  545. ),
  546. isToggled: Private.delegateToggled(
  547. app,
  548. menu.editorViewers,
  549. 'lineNumbersToggled'
  550. ),
  551. execute: Private.delegateExecute(
  552. app,
  553. menu.editorViewers,
  554. 'toggleLineNumbers'
  555. )
  556. });
  557. commands.addCommand(CommandIDs.matchBrackets, {
  558. label: trans.__('Match Brackets'),
  559. isEnabled: Private.delegateEnabled(
  560. app,
  561. menu.editorViewers,
  562. 'toggleMatchBrackets'
  563. ),
  564. isToggled: Private.delegateToggled(
  565. app,
  566. menu.editorViewers,
  567. 'matchBracketsToggled'
  568. ),
  569. execute: Private.delegateExecute(
  570. app,
  571. menu.editorViewers,
  572. 'toggleMatchBrackets'
  573. )
  574. });
  575. commands.addCommand(CommandIDs.wordWrap, {
  576. label: trans.__('Wrap Words'),
  577. isEnabled: Private.delegateEnabled(
  578. app,
  579. menu.editorViewers,
  580. 'toggleWordWrap'
  581. ),
  582. isToggled: Private.delegateToggled(
  583. app,
  584. menu.editorViewers,
  585. 'wordWrapToggled'
  586. ),
  587. execute: Private.delegateExecute(app, menu.editorViewers, 'toggleWordWrap')
  588. });
  589. menu.addGroup([{ command: 'apputils:activate-command-palette' }], 0);
  590. menu.addGroup(
  591. [
  592. { command: 'application:toggle-mode' },
  593. { command: 'application:toggle-presentation-mode' }
  594. ],
  595. 1
  596. );
  597. menu.addGroup(
  598. [
  599. { command: 'application:toggle-left-area' },
  600. { command: 'application:toggle-right-area' }
  601. ],
  602. 2
  603. );
  604. const editorViewerGroup = [
  605. CommandIDs.lineNumbering,
  606. CommandIDs.matchBrackets,
  607. CommandIDs.wordWrap
  608. ].map(command => {
  609. return { command };
  610. });
  611. menu.addGroup(editorViewerGroup, 10);
  612. }
  613. /**
  614. * Create the basic `Run` menu.
  615. */
  616. export function createRunMenu(
  617. app: JupyterFrontEnd,
  618. menu: RunMenu,
  619. trans: TranslationBundle
  620. ): void {
  621. const commands = menu.menu.commands;
  622. menu.menu.title.label = trans.__('Run');
  623. commands.addCommand(CommandIDs.run, {
  624. label: () => {
  625. const localizedLabel = Private.delegateLabel(
  626. app,
  627. menu.codeRunners,
  628. 'runLabel'
  629. );
  630. const enabled = Private.delegateEnabled(app, menu.codeRunners, 'run')();
  631. return enabled ? localizedLabel : trans.__('Run Selected');
  632. },
  633. isEnabled: Private.delegateEnabled(app, menu.codeRunners, 'run'),
  634. execute: Private.delegateExecute(app, menu.codeRunners, 'run')
  635. });
  636. commands.addCommand(CommandIDs.runAll, {
  637. label: () => {
  638. let localizedLabel = trans.__('Run All');
  639. const enabled = Private.delegateEnabled(
  640. app,
  641. menu.codeRunners,
  642. 'runAll'
  643. )();
  644. if (enabled) {
  645. localizedLabel = Private.delegateLabel(
  646. app,
  647. menu.codeRunners,
  648. 'runAllLabel'
  649. );
  650. }
  651. return localizedLabel;
  652. },
  653. isEnabled: Private.delegateEnabled(app, menu.codeRunners, 'runAll'),
  654. execute: Private.delegateExecute(app, menu.codeRunners, 'runAll')
  655. });
  656. commands.addCommand(CommandIDs.restartAndRunAll, {
  657. label: () => {
  658. let localizedLabel = trans.__('Restart Kernel and Run All');
  659. const enabled = Private.delegateEnabled(
  660. app,
  661. menu.codeRunners,
  662. 'restartAndRunAll'
  663. )();
  664. if (enabled) {
  665. localizedLabel = Private.delegateLabel(
  666. app,
  667. menu.codeRunners,
  668. 'restartAndRunAllLabel'
  669. );
  670. }
  671. return localizedLabel;
  672. },
  673. isEnabled: Private.delegateEnabled(
  674. app,
  675. menu.codeRunners,
  676. 'restartAndRunAll'
  677. ),
  678. execute: Private.delegateExecute(app, menu.codeRunners, 'restartAndRunAll')
  679. });
  680. const runAllGroup = [CommandIDs.runAll, CommandIDs.restartAndRunAll].map(
  681. command => {
  682. return { command };
  683. }
  684. );
  685. menu.addGroup([{ command: CommandIDs.run }], 0);
  686. menu.addGroup(runAllGroup, 999);
  687. }
  688. /**
  689. * Create the basic `Settings` menu.
  690. */
  691. export function createSettingsMenu(
  692. _: JupyterFrontEnd,
  693. menu: SettingsMenu,
  694. trans: TranslationBundle
  695. ): void {
  696. menu.menu.title.label = trans.__('Settings');
  697. menu.addGroup([{ command: 'settingeditor:open' }], 1000);
  698. }
  699. /**
  700. * Create the basic `Tabs` menu.
  701. */
  702. export function createTabsMenu(
  703. app: JupyterFrontEnd,
  704. menu: TabsMenu,
  705. labShell: ILabShell | null,
  706. trans: TranslationBundle
  707. ): void {
  708. const commands = app.commands;
  709. menu.menu.title.label = trans.__('Tabs');
  710. // Add commands for cycling the active tabs.
  711. menu.addGroup(
  712. [
  713. { command: 'application:activate-next-tab' },
  714. { command: 'application:activate-previous-tab' },
  715. { command: 'application:activate-next-tab-bar' },
  716. { command: 'application:activate-previous-tab-bar' },
  717. { command: CommandIDs.activatePreviouslyUsedTab }
  718. ],
  719. 0
  720. );
  721. // A list of the active tabs in the main area.
  722. const tabGroup: Menu.IItemOptions[] = [];
  723. // A disposable for getting rid of the out-of-date tabs list.
  724. let disposable: IDisposable;
  725. // Command to activate a widget by id.
  726. commands.addCommand(CommandIDs.activateById, {
  727. label: args => {
  728. const id = args['id'] || '';
  729. const widget = find(app.shell.widgets('main'), w => w.id === id);
  730. return (widget && widget.title.label) || '';
  731. },
  732. isToggled: args => {
  733. const id = args['id'] || '';
  734. return !!app.shell.currentWidget && app.shell.currentWidget.id === id;
  735. },
  736. execute: args => app.shell.activateById((args['id'] as string) || '')
  737. });
  738. let previousId = '';
  739. // Command to toggle between the current
  740. // tab and the last modified tab.
  741. commands.addCommand(CommandIDs.activatePreviouslyUsedTab, {
  742. label: trans.__('Activate Previously Used Tab'),
  743. isEnabled: () => !!previousId,
  744. execute: () => commands.execute(CommandIDs.activateById, { id: previousId })
  745. });
  746. if (labShell) {
  747. void app.restored.then(() => {
  748. // Iterate over the current widgets in the
  749. // main area, and add them to the tab group
  750. // of the menu.
  751. const populateTabs = () => {
  752. // remove the previous tab list
  753. if (disposable && !disposable.isDisposed) {
  754. disposable.dispose();
  755. }
  756. tabGroup.length = 0;
  757. let isPreviouslyUsedTabAttached = false;
  758. each(app.shell.widgets('main'), widget => {
  759. if (widget.id === previousId) {
  760. isPreviouslyUsedTabAttached = true;
  761. }
  762. tabGroup.push({
  763. command: CommandIDs.activateById,
  764. args: { id: widget.id }
  765. });
  766. });
  767. disposable = menu.addGroup(tabGroup, 1);
  768. previousId = isPreviouslyUsedTabAttached ? previousId : '';
  769. };
  770. populateTabs();
  771. labShell.layoutModified.connect(() => {
  772. populateTabs();
  773. });
  774. // Update the ID of the previous active tab if a new tab is selected.
  775. labShell.currentChanged.connect((_, args) => {
  776. const widget = args.oldValue;
  777. if (!widget) {
  778. return;
  779. }
  780. previousId = widget.id;
  781. });
  782. });
  783. }
  784. }
  785. /**
  786. * Create the basic `Help` menu.
  787. */
  788. export function createHelpMenu(
  789. app: JupyterFrontEnd,
  790. menu: HelpMenu,
  791. trans: TranslationBundle
  792. ): void {
  793. menu.menu.title.label = trans.__('Help');
  794. }
  795. export default plugin;
  796. /**
  797. * A namespace for Private data.
  798. */
  799. namespace Private {
  800. /**
  801. * Return the first value of the iterable that satisfies the predicate
  802. * function.
  803. */
  804. function find<T>(
  805. it: Iterable<T>,
  806. predicate: (value: T) => boolean
  807. ): T | undefined {
  808. for (const value of it) {
  809. if (predicate(value)) {
  810. return value;
  811. }
  812. }
  813. return undefined;
  814. }
  815. /**
  816. * A utility function that delegates a portion of a label to an IMenuExtender.
  817. */
  818. export function delegateLabel<E extends IMenuExtender<Widget>>(
  819. app: JupyterFrontEnd,
  820. s: Set<E>,
  821. label: keyof E
  822. ): string {
  823. const widget = app.shell.currentWidget;
  824. const extender = widget
  825. ? find(s, value => value.tracker.has(widget!))
  826. : undefined;
  827. if (!extender) {
  828. return '';
  829. } else {
  830. const count: number = extender.tracker.size;
  831. // Coerce the result to be a string. When Typedoc is updated to use
  832. // Typescript 2.8, we can possibly use conditional types to get Typescript
  833. // to recognize this is a string.
  834. return (extender[label] as any)(count) as string;
  835. }
  836. }
  837. /**
  838. * A utility function that delegates command execution
  839. * to an IMenuExtender.
  840. */
  841. export function delegateExecute<E extends IMenuExtender<Widget>>(
  842. app: JupyterFrontEnd,
  843. s: Set<E>,
  844. executor: keyof E
  845. ): () => Promise<any> {
  846. return () => {
  847. const widget = app.shell.currentWidget;
  848. const extender = widget
  849. ? find(s, value => value.tracker.has(widget!))
  850. : undefined;
  851. if (!extender) {
  852. return Promise.resolve(void 0);
  853. }
  854. // Coerce the result to be a function. When Typedoc is updated to use
  855. // Typescript 2.8, we can possibly use conditional types to get Typescript
  856. // to recognize this is a function.
  857. const f = (extender[executor] as any) as (w: Widget) => Promise<any>;
  858. return f(widget!);
  859. };
  860. }
  861. /**
  862. * A utility function that delegates whether a command is enabled
  863. * to an IMenuExtender.
  864. */
  865. export function delegateEnabled<E extends IMenuExtender<Widget>>(
  866. app: JupyterFrontEnd,
  867. s: Set<E>,
  868. executor: keyof E
  869. ): () => boolean {
  870. return () => {
  871. const widget = app.shell.currentWidget;
  872. const extender = widget
  873. ? find(s, value => value.tracker.has(widget!))
  874. : undefined;
  875. return (
  876. !!extender &&
  877. !!extender[executor] &&
  878. (extender.isEnabled && widget ? extender.isEnabled(widget) : true)
  879. );
  880. };
  881. }
  882. /**
  883. * A utility function that delegates whether a command is toggled
  884. * for an IMenuExtender.
  885. */
  886. export function delegateToggled<E extends IMenuExtender<Widget>>(
  887. app: JupyterFrontEnd,
  888. s: Set<E>,
  889. toggled: keyof E
  890. ): () => boolean {
  891. return () => {
  892. const widget = app.shell.currentWidget;
  893. const extender = widget
  894. ? find(s, value => value.tracker.has(widget!))
  895. : undefined;
  896. // Coerce extender[toggled] to be a function. When Typedoc is updated to use
  897. // Typescript 2.8, we can possibly use conditional types to get Typescript
  898. // to recognize this is a function.
  899. return (
  900. !!extender &&
  901. !!extender[toggled] &&
  902. !!widget &&
  903. !!((extender[toggled] as any) as (w: Widget) => () => boolean)(widget)
  904. );
  905. };
  906. }
  907. }