panel.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { isMarkdownCellModel } from '@jupyterlab/cells';
  4. import { Kernel, KernelMessage, Session } from '@jupyterlab/services';
  5. import { each } from '@lumino/algorithm';
  6. import { Token } from '@lumino/coreutils';
  7. import {
  8. ISessionContext,
  9. Printing,
  10. showDialog,
  11. Dialog
  12. } from '@jupyterlab/apputils';
  13. import { DocumentWidget, DocumentRegistry } from '@jupyterlab/docregistry';
  14. import { INotebookModel } from './model';
  15. import { Notebook, StaticNotebook } from './widget';
  16. import { PageConfig } from '@jupyterlab/coreutils';
  17. import {
  18. nullTranslator,
  19. ITranslator,
  20. TranslationBundle
  21. } from '@jupyterlab/translation';
  22. /**
  23. * The class name added to notebook panels.
  24. */
  25. const NOTEBOOK_PANEL_CLASS = 'jp-NotebookPanel';
  26. const NOTEBOOK_PANEL_TOOLBAR_CLASS = 'jp-NotebookPanel-toolbar';
  27. const NOTEBOOK_PANEL_NOTEBOOK_CLASS = 'jp-NotebookPanel-notebook';
  28. /**
  29. * A widget that hosts a notebook toolbar and content area.
  30. *
  31. * #### Notes
  32. * The widget keeps the document metadata in sync with the current
  33. * kernel on the context.
  34. */
  35. export class NotebookPanel extends DocumentWidget<Notebook, INotebookModel> {
  36. /**
  37. * Construct a new notebook panel.
  38. */
  39. constructor(options: DocumentWidget.IOptions<Notebook, INotebookModel>) {
  40. super(options);
  41. this.translator = options.translator || nullTranslator;
  42. this._trans = this.translator.load('jupyterlab');
  43. // Set up CSS classes
  44. this.addClass(NOTEBOOK_PANEL_CLASS);
  45. this.toolbar.addClass(NOTEBOOK_PANEL_TOOLBAR_CLASS);
  46. this.content.addClass(NOTEBOOK_PANEL_NOTEBOOK_CLASS);
  47. // Set up things related to the context
  48. this.content.model = this.context.model;
  49. this.context.sessionContext.kernelChanged.connect(
  50. this._onKernelChanged,
  51. this
  52. );
  53. this.context.sessionContext.statusChanged.connect(
  54. this._onSessionStatusChanged,
  55. this
  56. );
  57. this.context.saveState.connect(this._onSave, this);
  58. void this.revealed.then(() => {
  59. if (this.isDisposed) {
  60. // this widget has already been disposed, bail
  61. return;
  62. }
  63. // Set the document edit mode on initial open if it looks like a new document.
  64. if (this.content.widgets.length === 1) {
  65. const cellModel = this.content.widgets[0].model;
  66. if (cellModel.type === 'code' && cellModel.value.text === '') {
  67. this.content.mode = 'edit';
  68. }
  69. }
  70. });
  71. }
  72. _onSave(sender: DocumentRegistry.Context, state: DocumentRegistry.SaveState) {
  73. if (state === 'started' && this.model) {
  74. // Find markdown cells
  75. const { cells } = this.model;
  76. each(cells, cell => {
  77. if (isMarkdownCellModel(cell)) {
  78. for (const key of cell.attachments.keys) {
  79. if (!cell.value.text.includes(key)) {
  80. cell.attachments.remove(key);
  81. }
  82. }
  83. }
  84. });
  85. }
  86. }
  87. /**
  88. * The session context used by the panel.
  89. */
  90. get sessionContext(): ISessionContext {
  91. return this.context.sessionContext;
  92. }
  93. /**
  94. * The model for the widget.
  95. */
  96. get model(): INotebookModel | null {
  97. return this.content.model;
  98. }
  99. /**
  100. * Update the options for the current notebook panel.
  101. *
  102. * @param config new options to set
  103. */
  104. setConfig(config: NotebookPanel.IConfig): void {
  105. this.content.editorConfig = config.editorConfig;
  106. this.content.notebookConfig = config.notebookConfig;
  107. // Update kernel shutdown behavior
  108. const kernelPreference = this.context.sessionContext.kernelPreference;
  109. this.context.sessionContext.kernelPreference = {
  110. ...kernelPreference,
  111. shutdownOnDispose: config.kernelShutdown
  112. };
  113. }
  114. /**
  115. * Set URI fragment identifier.
  116. */
  117. setFragment(fragment: string) {
  118. void this.context.ready.then(() => {
  119. this.content.setFragment(fragment);
  120. });
  121. }
  122. /**
  123. * Dispose of the resources used by the widget.
  124. */
  125. dispose(): void {
  126. this.content.dispose();
  127. super.dispose();
  128. }
  129. /**
  130. * Prints the notebook by converting to HTML with nbconvert.
  131. */
  132. [Printing.symbol]() {
  133. return async () => {
  134. // Save before generating HTML
  135. if (this.context.model.dirty && !this.context.model.readOnly) {
  136. await this.context.save();
  137. }
  138. await Printing.printURL(
  139. PageConfig.getNBConvertURL({
  140. format: 'html',
  141. download: false,
  142. path: this.context.path
  143. })
  144. );
  145. };
  146. }
  147. /**
  148. * Handle a change in the kernel by updating the document metadata.
  149. */
  150. private _onKernelChanged(
  151. sender: any,
  152. args: Session.ISessionConnection.IKernelChangedArgs
  153. ): void {
  154. if (!this.model || !args.newValue) {
  155. return;
  156. }
  157. const { newValue } = args;
  158. void newValue.info.then(info => {
  159. if (
  160. this.model &&
  161. this.context.sessionContext.session?.kernel === newValue
  162. ) {
  163. this._updateLanguage(info.language_info);
  164. }
  165. });
  166. void this._updateSpec(newValue);
  167. }
  168. private _onSessionStatusChanged(
  169. sender: ISessionContext,
  170. status: Kernel.Status
  171. ) {
  172. // If the status is autorestarting, and we aren't already in a series of
  173. // autorestarts, show the dialog.
  174. if (status === 'autorestarting' && !this._autorestarting) {
  175. // The kernel died and the server is restarting it. We notify the user so
  176. // they know why their kernel state is gone.
  177. void showDialog({
  178. title: this._trans.__('Kernel Restarting'),
  179. body: this._trans.__(
  180. 'The kernel for %1 appears to have died. It will restart automatically.',
  181. this.sessionContext.session?.path
  182. ),
  183. buttons: [Dialog.okButton({ label: this._trans.__('Ok') })]
  184. });
  185. this._autorestarting = true;
  186. } else if (status === 'restarting') {
  187. // Another autorestart attempt will first change the status to
  188. // restarting, then to autorestarting again, so we don't reset the
  189. // autorestarting status if the status is 'restarting'.
  190. /* no-op */
  191. } else {
  192. this._autorestarting = false;
  193. }
  194. }
  195. /**
  196. * Update the kernel language.
  197. */
  198. private _updateLanguage(language: KernelMessage.ILanguageInfo): void {
  199. this.model!.metadata.set('language_info', language);
  200. }
  201. /**
  202. * Update the kernel spec.
  203. */
  204. private async _updateSpec(kernel: Kernel.IKernelConnection): Promise<void> {
  205. const spec = await kernel.spec;
  206. if (this.isDisposed) {
  207. return;
  208. }
  209. this.model!.metadata.set('kernelspec', {
  210. name: kernel.name,
  211. display_name: spec?.display_name,
  212. language: spec?.language
  213. });
  214. }
  215. translator: ITranslator;
  216. private _trans: TranslationBundle;
  217. /**
  218. * Whether we are currently in a series of autorestarts we have already
  219. * notified the user about.
  220. */
  221. private _autorestarting = false;
  222. }
  223. /**
  224. * A namespace for `NotebookPanel` statics.
  225. */
  226. export namespace NotebookPanel {
  227. /**
  228. * Notebook config interface for NotebookPanel
  229. */
  230. export interface IConfig {
  231. /**
  232. * A config object for cell editors
  233. */
  234. editorConfig: StaticNotebook.IEditorConfig;
  235. /**
  236. * A config object for notebook widget
  237. */
  238. notebookConfig: StaticNotebook.INotebookConfig;
  239. /**
  240. * Whether to shut down the kernel when closing the panel or not
  241. */
  242. kernelShutdown: boolean;
  243. }
  244. /**
  245. * A content factory interface for NotebookPanel.
  246. */
  247. export interface IContentFactory extends Notebook.IContentFactory {
  248. /**
  249. * Create a new content area for the panel.
  250. */
  251. createNotebook(options: Notebook.IOptions): Notebook;
  252. }
  253. /**
  254. * The default implementation of an `IContentFactory`.
  255. */
  256. export class ContentFactory
  257. extends Notebook.ContentFactory
  258. implements IContentFactory {
  259. /**
  260. * Create a new content area for the panel.
  261. */
  262. createNotebook(options: Notebook.IOptions): Notebook {
  263. return new Notebook(options);
  264. }
  265. }
  266. /**
  267. * Default content factory for the notebook panel.
  268. */
  269. export const defaultContentFactory: ContentFactory = new ContentFactory();
  270. /* tslint:disable */
  271. /**
  272. * The notebook renderer token.
  273. */
  274. export const IContentFactory = new Token<IContentFactory>(
  275. '@jupyterlab/notebook:IContentFactory'
  276. );
  277. /* tslint:enable */
  278. }