index.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. ILayoutRestorer, JupyterLab, JupyterLabPlugin
  5. } from '@jupyterlab/application';
  6. import {
  7. Dialog, ICommandPalette, InstanceTracker, showDialog
  8. } from '@jupyterlab/apputils';
  9. import {
  10. IEditorServices
  11. } from '@jupyterlab/codeeditor';
  12. import {
  13. ConsolePanel, IConsoleTracker
  14. } from '@jupyterlab/console';
  15. import {
  16. PageConfig
  17. } from '@jupyterlab/coreutils';
  18. import {
  19. IFileBrowserFactory
  20. } from '@jupyterlab/filebrowser';
  21. import {
  22. ILauncher
  23. } from '@jupyterlab/launcher';
  24. import {
  25. IMainMenu
  26. } from '@jupyterlab/mainmenu';
  27. import {
  28. find
  29. } from '@phosphor/algorithm';
  30. import {
  31. ReadonlyJSONObject
  32. } from '@phosphor/coreutils';
  33. import {
  34. Menu
  35. } from '@phosphor/widgets';
  36. /**
  37. * The command IDs used by the console plugin.
  38. */
  39. namespace CommandIDs {
  40. export
  41. const create = 'console:create';
  42. export
  43. const clear = 'console:clear';
  44. export
  45. const runUnforced = 'console:run-unforced';
  46. export
  47. const runForced = 'console:run-forced';
  48. export
  49. const linebreak = 'console:linebreak';
  50. export
  51. const interrupt = 'console:interrupt-kernel';
  52. export
  53. const restart = 'console:restart-kernel';
  54. export
  55. const closeAndShutdown = 'console:close-and-shutdown';
  56. export
  57. const open = 'console:open';
  58. export
  59. const inject = 'console:inject';
  60. export
  61. const changeKernel = 'console:change-kernel';
  62. }
  63. /**
  64. * The console widget tracker provider.
  65. */
  66. const tracker: JupyterLabPlugin<IConsoleTracker> = {
  67. id: '@jupyterlab/console-extension:tracker',
  68. provides: IConsoleTracker,
  69. requires: [
  70. IMainMenu,
  71. ICommandPalette,
  72. ConsolePanel.IContentFactory,
  73. IEditorServices,
  74. ILayoutRestorer,
  75. IFileBrowserFactory
  76. ],
  77. optional: [ILauncher],
  78. activate: activateConsole,
  79. autoStart: true
  80. };
  81. /**
  82. * The console widget content factory.
  83. */
  84. const factory: JupyterLabPlugin<ConsolePanel.IContentFactory> = {
  85. id: '@jupyterlab/console-extension:factory',
  86. provides: ConsolePanel.IContentFactory,
  87. requires: [IEditorServices],
  88. autoStart: true,
  89. activate: (app: JupyterLab, editorServices: IEditorServices) => {
  90. const editorFactory = editorServices.factoryService.newInlineEditor
  91. .bind(editorServices.factoryService);
  92. return new ConsolePanel.ContentFactory({ editorFactory });
  93. }
  94. };
  95. /**
  96. * Export the plugins as the default.
  97. */
  98. const plugins: JupyterLabPlugin<any>[] = [factory, tracker];
  99. export default plugins;
  100. /**
  101. * Activate the console extension.
  102. */
  103. function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette, contentFactory: ConsolePanel.IContentFactory, editorServices: IEditorServices, restorer: ILayoutRestorer, browserFactory: IFileBrowserFactory, launcher: ILauncher | null): IConsoleTracker {
  104. const manager = app.serviceManager;
  105. const { commands, shell } = app;
  106. const category = 'Console';
  107. const menu = new Menu({ commands });
  108. // Create an instance tracker for all console panels.
  109. const tracker = new InstanceTracker<ConsolePanel>({ namespace: 'console' });
  110. // Handle state restoration.
  111. restorer.restore(tracker, {
  112. command: CommandIDs.open,
  113. args: panel => ({
  114. path: panel.console.session.path,
  115. name: panel.console.session.name
  116. }),
  117. name: panel => panel.console.session.path,
  118. when: manager.ready
  119. });
  120. // Update the command registry when the console state changes.
  121. tracker.currentChanged.connect(() => {
  122. if (tracker.size <= 1) {
  123. commands.notifyCommandChanged(CommandIDs.interrupt);
  124. }
  125. });
  126. // The launcher callback.
  127. let callback = (cwd: string, name: string) => {
  128. return createConsole({ basePath: cwd, kernelPreference: { name } });
  129. };
  130. // Add a launcher item if the launcher is available.
  131. if (launcher) {
  132. manager.ready.then(() => {
  133. const specs = manager.specs;
  134. if (!specs) {
  135. return;
  136. }
  137. let baseUrl = PageConfig.getBaseUrl();
  138. for (let name in specs.kernelspecs) {
  139. let displayName = specs.kernelspecs[name].display_name;
  140. let rank = name === specs.default ? 0 : Infinity;
  141. let kernelIconUrl = specs.kernelspecs[name].resources['logo-64x64'];
  142. if (kernelIconUrl) {
  143. let index = kernelIconUrl.indexOf('kernelspecs');
  144. kernelIconUrl = baseUrl + kernelIconUrl.slice(index);
  145. }
  146. launcher.add({
  147. displayName,
  148. category: 'Console',
  149. name,
  150. iconClass: 'jp-CodeConsoleIcon',
  151. callback,
  152. rank,
  153. kernelIconUrl
  154. });
  155. }
  156. });
  157. }
  158. // Set the main menu title.
  159. menu.title.label = category;
  160. /**
  161. * Create a console for a given path.
  162. */
  163. function createConsole(options: Partial<ConsolePanel.IOptions>): Promise<ConsolePanel> {
  164. return manager.ready.then(() => {
  165. let panel = new ConsolePanel({
  166. manager,
  167. rendermime: app.rendermime.clone(),
  168. contentFactory,
  169. mimeTypeService: editorServices.mimeTypeService,
  170. ...options
  171. });
  172. // Add the console panel to the tracker.
  173. tracker.add(panel);
  174. shell.addToMainArea(panel);
  175. shell.activateById(panel.id);
  176. return panel;
  177. });
  178. }
  179. /**
  180. * Whether there is an active console.
  181. */
  182. function hasWidget(): boolean {
  183. return tracker.currentWidget !== null;
  184. }
  185. let command = CommandIDs.open;
  186. commands.addCommand(command, {
  187. execute: (args: Partial<ConsolePanel.IOptions>) => {
  188. let path = args['path'];
  189. let widget = tracker.find(value => {
  190. return value.console.session.path === path;
  191. });
  192. if (widget) {
  193. shell.activateById(widget.id);
  194. } else {
  195. return manager.ready.then(() => {
  196. let model = find(manager.sessions.running(), item => {
  197. return item.path === path;
  198. });
  199. if (model) {
  200. return createConsole(args);
  201. }
  202. return Promise.reject(`No running console for path: ${path}`);
  203. });
  204. }
  205. },
  206. });
  207. command = CommandIDs.create;
  208. commands.addCommand(command, {
  209. label: 'New Console',
  210. execute: (args: Partial<ConsolePanel.IOptions>) => {
  211. let basePath = args.basePath || browserFactory.defaultBrowser.model.path;
  212. return createConsole({ basePath, ...args });
  213. }
  214. });
  215. palette.addItem({ command, category });
  216. // Get the current widget and activate unless the args specify otherwise.
  217. function getCurrent(args: ReadonlyJSONObject): ConsolePanel | null {
  218. let widget = tracker.currentWidget;
  219. let activate = args['activate'] !== false;
  220. if (activate && widget) {
  221. shell.activateById(widget.id);
  222. }
  223. return widget;
  224. }
  225. command = CommandIDs.clear;
  226. commands.addCommand(command, {
  227. label: 'Clear Cells',
  228. execute: args => {
  229. let current = getCurrent(args);
  230. if (!current) {
  231. return;
  232. }
  233. current.console.clear();
  234. },
  235. isEnabled: hasWidget
  236. });
  237. palette.addItem({ command, category });
  238. command = CommandIDs.runUnforced;
  239. commands.addCommand(command, {
  240. label: 'Run Cell (unforced)',
  241. execute: args => {
  242. let current = getCurrent(args);
  243. if (!current) {
  244. return;
  245. }
  246. return current.console.execute();
  247. },
  248. isEnabled: hasWidget
  249. });
  250. palette.addItem({ command, category });
  251. command = CommandIDs.runForced;
  252. commands.addCommand(command, {
  253. label: 'Run Cell (forced)',
  254. execute: args => {
  255. let current = getCurrent(args);
  256. if (!current) {
  257. return;
  258. }
  259. current.console.execute(true);
  260. },
  261. isEnabled: hasWidget
  262. });
  263. palette.addItem({ command, category });
  264. command = CommandIDs.linebreak;
  265. commands.addCommand(command, {
  266. label: 'Insert Line Break',
  267. execute: args => {
  268. let current = getCurrent(args);
  269. if (!current) {
  270. return;
  271. }
  272. current.console.insertLinebreak();
  273. },
  274. isEnabled: hasWidget
  275. });
  276. palette.addItem({ command, category });
  277. command = CommandIDs.interrupt;
  278. commands.addCommand(command, {
  279. label: 'Interrupt Kernel',
  280. execute: args => {
  281. let current = getCurrent(args);
  282. if (!current) {
  283. return;
  284. }
  285. let kernel = current.console.session.kernel;
  286. if (kernel) {
  287. return kernel.interrupt();
  288. }
  289. },
  290. isEnabled: hasWidget
  291. });
  292. palette.addItem({ command, category });
  293. command = CommandIDs.restart;
  294. commands.addCommand(command, {
  295. label: 'Restart Kernel',
  296. execute: args => {
  297. let current = getCurrent(args);
  298. if (!current) {
  299. return;
  300. }
  301. return current.console.session.restart();
  302. },
  303. isEnabled: hasWidget
  304. });
  305. palette.addItem({ command, category });
  306. command = CommandIDs.closeAndShutdown;
  307. commands.addCommand(command, {
  308. label: 'Close and Shutdown',
  309. execute: args => {
  310. const current = getCurrent(args);
  311. if (!current) {
  312. return;
  313. }
  314. return showDialog({
  315. title: 'Shutdown the console?',
  316. body: `Are you sure you want to close "${current.title.label}"?`,
  317. buttons: [Dialog.cancelButton(), Dialog.warnButton()]
  318. }).then(result => {
  319. if (result.button.accept) {
  320. current.console.session.shutdown().then(() => {
  321. current.dispose();
  322. });
  323. } else {
  324. return false;
  325. }
  326. });
  327. },
  328. isEnabled: hasWidget
  329. });
  330. command = CommandIDs.inject;
  331. commands.addCommand(command, {
  332. execute: args => {
  333. let path = args['path'];
  334. tracker.find(widget => {
  335. if (widget.console.session.path === path) {
  336. if (args['activate'] !== false) {
  337. shell.activateById(widget.id);
  338. }
  339. widget.console.inject(args['code'] as string);
  340. return true;
  341. }
  342. return false;
  343. });
  344. },
  345. isEnabled: hasWidget
  346. });
  347. command = CommandIDs.changeKernel;
  348. commands.addCommand(command, {
  349. label: 'Change Kernel',
  350. execute: args => {
  351. let current = getCurrent(args);
  352. if (!current) {
  353. return;
  354. }
  355. return current.console.session.selectKernel();
  356. },
  357. isEnabled: hasWidget
  358. });
  359. palette.addItem({ command, category });
  360. // Add a console creator to the File menu
  361. mainMenu.fileMenu.newMenu.addItem({ command: CommandIDs.create });
  362. // Add a kernel user to the Kernel menu
  363. mainMenu.kernelMenu.addUser<ConsolePanel>({
  364. tracker,
  365. interruptKernel: current => {
  366. let kernel = current.console.session.kernel;
  367. if (kernel) {
  368. return kernel.interrupt();
  369. }
  370. return Promise.resolve(void 0);
  371. },
  372. restartKernel: current => current.console.session.restart(),
  373. changeKernel: current => current.console.session.selectKernel()
  374. });
  375. // Add a code runner to the Run menu.
  376. mainMenu.runMenu.addRunner<ConsolePanel>({
  377. tracker,
  378. run: current => current.console.execute(true)
  379. });
  380. // Add the console menu.
  381. menu.addItem({ command: CommandIDs.runUnforced });
  382. menu.addItem({ command: CommandIDs.runForced });
  383. menu.addItem({ command: CommandIDs.linebreak });
  384. menu.addItem({ type: 'separator' });
  385. menu.addItem({ command: CommandIDs.clear });
  386. menu.addItem({ type: 'separator' });
  387. menu.addItem({ command: CommandIDs.closeAndShutdown });
  388. mainMenu.addMenu(menu, {rank: 50});
  389. app.contextMenu.addItem({command: CommandIDs.clear, selector: '.jp-CodeConsole'});
  390. app.contextMenu.addItem({command: CommandIDs.restart, selector: '.jp-CodeConsole'});
  391. return tracker;
  392. }