index.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. Kernel, Session
  5. } from '@jupyterlab/services';
  6. import {
  7. JSONObject
  8. } from '@phosphor/coreutils';
  9. import {
  10. Menu
  11. } from '@phosphor/widgets';
  12. import {
  13. JupyterLab, JupyterLabPlugin
  14. } from '../application';
  15. import {
  16. Dialog, ICommandPalette, InstanceTracker, ILayoutRestorer, IMainMenu,
  17. showDialog
  18. } from '../apputils';
  19. import {
  20. IEditorServices
  21. } from '../codeeditor';
  22. import {
  23. PathExt, Time, uuid
  24. } from '../coreutils';
  25. import {
  26. IConsoleTracker, ICreateConsoleArgs, ConsolePanel
  27. } from '../console';
  28. import {
  29. selectKernel
  30. } from '../docregistry';
  31. import {
  32. IPathTracker
  33. } from '../filebrowser';
  34. import {
  35. IRenderMime, RenderMime
  36. } from '../rendermime';
  37. import {
  38. IServiceManager
  39. } from '../services';
  40. /**
  41. * The command IDs used by the console plugin.
  42. */
  43. namespace CommandIDs {
  44. export
  45. const create = 'console:create';
  46. export
  47. const clear = 'console:clear';
  48. export
  49. const run = 'console:run';
  50. export
  51. const runForced = 'console:run-forced';
  52. export
  53. const linebreak = 'console:linebreak';
  54. export
  55. const interrupt = 'console:interrupt-kernel';
  56. export
  57. const restart = 'console:restart-kernel';
  58. export
  59. const closeAndShutdown = 'console:close-and-shutdown';
  60. export
  61. const open = 'console:open';
  62. export
  63. const inject = 'console:inject';
  64. export
  65. const switchKernel = 'console:switch-kernel';
  66. };
  67. /**
  68. * The console widget tracker provider.
  69. */
  70. export
  71. const trackerPlugin: JupyterLabPlugin<IConsoleTracker> = {
  72. id: 'jupyter.services.console-tracker',
  73. provides: IConsoleTracker,
  74. requires: [
  75. IServiceManager,
  76. IRenderMime,
  77. IMainMenu,
  78. ICommandPalette,
  79. IPathTracker,
  80. ConsolePanel.IContentFactory,
  81. IEditorServices,
  82. ILayoutRestorer
  83. ],
  84. activate: activateConsole,
  85. autoStart: true
  86. };
  87. /**
  88. * The console widget content factory.
  89. */
  90. export
  91. const contentFactoryPlugin: JupyterLabPlugin<ConsolePanel.IContentFactory> = {
  92. id: 'jupyter.services.console-renderer',
  93. provides: ConsolePanel.IContentFactory,
  94. requires: [IEditorServices],
  95. autoStart: true,
  96. activate: (app: JupyterLab, editorServices: IEditorServices) => {
  97. let editorFactory = editorServices.factoryService.newInlineEditor.bind(
  98. editorServices.factoryService);
  99. return new ConsolePanel.ContentFactory({ editorFactory });
  100. }
  101. };
  102. /**
  103. * Export the plugins as the default.
  104. */
  105. const plugins: JupyterLabPlugin<any>[] = [contentFactoryPlugin, trackerPlugin];
  106. export default plugins;
  107. /**
  108. * The class name for the console icon from the default theme.
  109. */
  110. const CONSOLE_ICON_CLASS = 'jp-ImageCodeConsole';
  111. /**
  112. * A regex for console names.
  113. */
  114. const CONSOLE_REGEX = /^console-(\d)+-[0-9a-f]+$/;
  115. /**
  116. * Activate the console extension.
  117. */
  118. function activateConsole(app: JupyterLab, services: IServiceManager, rendermime: IRenderMime, mainMenu: IMainMenu, palette: ICommandPalette, pathTracker: IPathTracker, contentFactory: ConsolePanel.IContentFactory, editorServices: IEditorServices, restorer: ILayoutRestorer): IConsoleTracker {
  119. let manager = services.sessions;
  120. let { commands, shell } = app;
  121. let category = 'Console';
  122. let command: string;
  123. let count = 0;
  124. let menu = new Menu({ commands });
  125. // Create an instance tracker for all console panels.
  126. const tracker = new InstanceTracker<ConsolePanel>({
  127. namespace: 'console',
  128. shell
  129. });
  130. // Handle state restoration.
  131. restorer.restore(tracker, {
  132. command: CommandIDs.create,
  133. args: panel => ({ id: panel.console.session.id }),
  134. name: panel => panel.console.session && panel.console.session.id,
  135. when: manager.ready
  136. });
  137. // Set the main menu title.
  138. menu.title.label = category;
  139. command = CommandIDs.create;
  140. commands.addCommand(command, {
  141. label: 'Start New Console',
  142. execute: (args?: ICreateConsoleArgs) => {
  143. let name = `Console ${++count}`;
  144. args = args || {};
  145. // If we get a session, use it.
  146. if (args.id) {
  147. return manager.ready.then(() => manager.connectTo(args.id))
  148. .then(session => {
  149. name = session.path.split('/').pop();
  150. name = `Console ${name.match(CONSOLE_REGEX)[1]}`;
  151. createConsole(session, name);
  152. return session.id;
  153. });
  154. }
  155. // Find the correct path for the new session.
  156. // Use the given path or the cwd.
  157. let path = args.path || pathTracker.path;
  158. if (PathExt.extname(path)) {
  159. path = PathExt.dirname(path);
  160. }
  161. path = `${path}/console-${count}-${uuid()}`;
  162. // Get the kernel model.
  163. return manager.ready.then(() => getKernel(args, name)).then(kernel => {
  164. if (!kernel || (kernel && !kernel.id && !kernel.name)) {
  165. return;
  166. }
  167. // Start the session.
  168. let options: Session.IOptions = {
  169. path,
  170. kernelName: kernel.name,
  171. kernelId: kernel.id
  172. };
  173. return manager.startNew(options).then(session => {
  174. createConsole(session, name);
  175. return session.id;
  176. });
  177. });
  178. }
  179. });
  180. palette.addItem({ command, category });
  181. // Get the current widget and activate unless the args specify otherwise.
  182. function getCurrent(args: JSONObject): ConsolePanel | null {
  183. let widget = tracker.currentWidget;
  184. let activate = args['activate'] !== false;
  185. if (activate && widget) {
  186. tracker.activate(widget);
  187. }
  188. return widget;
  189. }
  190. command = CommandIDs.clear;
  191. commands.addCommand(command, {
  192. label: 'Clear Cells',
  193. execute: args => {
  194. let current = getCurrent(args);
  195. if (!current) {
  196. return;
  197. }
  198. current.console.clear();
  199. }
  200. });
  201. palette.addItem({ command, category });
  202. command = CommandIDs.run;
  203. commands.addCommand(command, {
  204. label: 'Run Cell',
  205. execute: args => {
  206. let current = getCurrent(args);
  207. if (!current) {
  208. return;
  209. }
  210. current.console.execute();
  211. }
  212. });
  213. palette.addItem({ command, category });
  214. command = CommandIDs.runForced;
  215. commands.addCommand(command, {
  216. label: 'Run Cell (forced)',
  217. execute: args => {
  218. let current = getCurrent(args);
  219. if (!current) {
  220. return;
  221. }
  222. current.console.execute(true);
  223. }
  224. });
  225. palette.addItem({ command, category });
  226. command = CommandIDs.linebreak;
  227. commands.addCommand(command, {
  228. label: 'Insert Line Break',
  229. execute: args => {
  230. let current = getCurrent(args);
  231. if (!current) {
  232. return;
  233. }
  234. current.console.insertLinebreak();
  235. }
  236. });
  237. palette.addItem({ command, category });
  238. command = CommandIDs.interrupt;
  239. commands.addCommand(command, {
  240. label: 'Interrupt Kernel',
  241. execute: args => {
  242. let current = getCurrent(args);
  243. if (!current) {
  244. return;
  245. }
  246. let kernel = current.console.session.kernel;
  247. if (kernel) {
  248. return kernel.interrupt();
  249. }
  250. }
  251. });
  252. palette.addItem({ command, category });
  253. command = CommandIDs.restart;
  254. commands.addCommand(command, {
  255. label: 'Restart Kernel',
  256. execute: args => {
  257. let current = getCurrent(args);
  258. if (!current) {
  259. return;
  260. }
  261. let kernel = current.console.session.kernel;
  262. if (kernel) {
  263. return kernel.restart();
  264. }
  265. }
  266. });
  267. palette.addItem({ command, category });
  268. command = CommandIDs.closeAndShutdown;
  269. commands.addCommand(command, {
  270. label: 'Close and Shutdown',
  271. execute: args => {
  272. let current = getCurrent(args);
  273. if (!current) {
  274. return;
  275. }
  276. return showDialog({
  277. title: 'Shutdown the console?',
  278. body: `Are you sure you want to close "${current.title.label}"?`,
  279. buttons: [Dialog.cancelButton(), Dialog.warnButton()]
  280. }).then(result => {
  281. if (result.accept) {
  282. current.console.session.shutdown().then(() => {
  283. current.dispose();
  284. });
  285. } else {
  286. return false;
  287. }
  288. });
  289. }
  290. });
  291. command = CommandIDs.inject;
  292. commands.addCommand(command, {
  293. execute: (args: JSONObject) => {
  294. let id = args['id'];
  295. tracker.find(widget => {
  296. if (widget.console.session.id === id) {
  297. if (args['activate'] !== false) {
  298. tracker.activate(widget);
  299. }
  300. widget.console.inject(args['code'] as string);
  301. return true;
  302. }
  303. });
  304. }
  305. });
  306. command = CommandIDs.open;
  307. commands.addCommand(command, {
  308. execute: (args: JSONObject) => {
  309. let id = args['id'];
  310. let widget = tracker.find(value => {
  311. if (value.console.session.id === id) {
  312. return true;
  313. }
  314. });
  315. if (widget) {
  316. tracker.activate(widget);
  317. } else {
  318. app.commands.execute(CommandIDs.create, { id });
  319. }
  320. }
  321. });
  322. /**
  323. * Get the kernel given the create args.
  324. */
  325. function getKernel(args: ICreateConsoleArgs, name: string): Promise<Kernel.IModel> {
  326. if (args.kernel) {
  327. return Promise.resolve(args.kernel);
  328. }
  329. return manager.ready.then(() => {
  330. let options = {
  331. name,
  332. specs: manager.specs,
  333. sessions: manager.running(),
  334. preferredLanguage: args.preferredLanguage || '',
  335. };
  336. return selectKernel(options);
  337. });
  338. }
  339. let id = 0; // The ID counter for notebook panels.
  340. /**
  341. * Create a console for a given session.
  342. *
  343. * #### Notes
  344. * The manager must be ready before calling this function.
  345. */
  346. function createConsole(session: Session.ISession, name: string): void {
  347. let options = {
  348. rendermime: rendermime.clone(),
  349. session,
  350. contentFactory,
  351. mimeTypeService: editorServices.mimeTypeService
  352. };
  353. let panel = new ConsolePanel(options);
  354. let resolver = new RenderMime.UrlResolver({
  355. session,
  356. contents: services.contents
  357. });
  358. panel.console.rendermime.resolver = resolver;
  359. let specs = manager.specs;
  360. let displayName = specs.kernelspecs[session.kernel.name].display_name;
  361. let captionOptions: Private.ICaptionOptions = {
  362. label: name,
  363. displayName,
  364. path: session.path,
  365. connected: new Date()
  366. };
  367. // If the console panel does not have an ID, assign it one.
  368. panel.id = panel.id || `console-${++id}`;
  369. panel.title.label = name;
  370. panel.title.caption = Private.caption(captionOptions);
  371. panel.title.icon = CONSOLE_ICON_CLASS;
  372. panel.title.closable = true;
  373. // Update the caption of the tab with the last execution time.
  374. panel.console.executed.connect((sender, executed) => {
  375. captionOptions.executed = executed;
  376. panel.title.caption = Private.caption(captionOptions);
  377. });
  378. // Update the caption of the tab when the kernel changes.
  379. panel.console.session.kernelChanged.connect(() => {
  380. let newName = panel.console.session.kernel.name;
  381. name = specs.kernelspecs[newName].display_name;
  382. captionOptions.displayName = name;
  383. captionOptions.connected = new Date();
  384. captionOptions.executed = null;
  385. panel.title.caption = Private.caption(captionOptions);
  386. });
  387. // Add the console panel to the tracker.
  388. tracker.add(panel);
  389. shell.addToMainArea(panel);
  390. tracker.activate(panel);
  391. }
  392. command = CommandIDs.switchKernel;
  393. commands.addCommand(command, {
  394. label: 'Switch Kernel',
  395. execute: args => {
  396. let current = getCurrent(args);
  397. if (!current) {
  398. return;
  399. }
  400. let widget = current.console;
  401. let session = widget.session;
  402. let lang = '';
  403. manager.ready.then(() => {
  404. let specs = manager.specs;
  405. if (session.kernel) {
  406. lang = specs.kernelspecs[session.kernel.name].language;
  407. }
  408. let options = {
  409. name: widget.parent.title.label,
  410. specs,
  411. sessions: manager.running(),
  412. preferredLanguage: lang,
  413. kernel: session.kernel.model,
  414. host: widget
  415. };
  416. return selectKernel(options);
  417. }).then((kernelId: Kernel.IModel) => {
  418. // If the user cancels, kernelId will be void and should be ignored.
  419. if (kernelId) {
  420. return session.changeKernel(kernelId);
  421. }
  422. });
  423. }
  424. });
  425. palette.addItem({ command, category });
  426. menu.addItem({ command: CommandIDs.run });
  427. menu.addItem({ command: CommandIDs.runForced });
  428. menu.addItem({ command: CommandIDs.linebreak });
  429. menu.addItem({ type: 'separator' });
  430. menu.addItem({ command: CommandIDs.clear });
  431. menu.addItem({ type: 'separator' });
  432. menu.addItem({ command: CommandIDs.interrupt });
  433. menu.addItem({ command: CommandIDs.restart });
  434. menu.addItem({ command: CommandIDs.switchKernel });
  435. menu.addItem({ type: 'separator' });
  436. menu.addItem({ command: CommandIDs.closeAndShutdown });
  437. mainMenu.addMenu(menu, {rank: 50});
  438. return tracker;
  439. }
  440. /**
  441. * A namespace for private data.
  442. */
  443. namespace Private {
  444. /**
  445. * An interface for caption options.
  446. */
  447. export
  448. interface ICaptionOptions {
  449. /**
  450. * The time when the console connected to the current kernel.
  451. */
  452. connected: Date;
  453. /**
  454. * The time when the console last executed its prompt.
  455. */
  456. executed?: Date;
  457. /**
  458. * The path to the file backing the console.
  459. *
  460. * #### Notes
  461. * Currently, the actual file does not exist, but the directory is the
  462. * current working directory at the time the console was opened.
  463. */
  464. path: string;
  465. /**
  466. * The label of the console (as displayed in tabs).
  467. */
  468. label: string;
  469. /**
  470. * The display name of the console's kernel.
  471. */
  472. displayName: string;
  473. }
  474. /**
  475. * Generate a caption for a console's title.
  476. */
  477. export
  478. function caption(options: ICaptionOptions): string {
  479. let { label, path, displayName, connected, executed } = options;
  480. let caption = (
  481. `Name: ${label}\n` +
  482. `Directory: ${PathExt.dirname(path)}\n` +
  483. `Kernel: ${displayName}\n` +
  484. `Connected: ${Time.format(connected.toISOString())}`
  485. );
  486. if (executed) {
  487. caption += `\nLast Execution: ${Time.format(executed.toISOString())}`;
  488. }
  489. return caption;
  490. }
  491. }