context.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. 'use strict';
  4. import {
  5. IKernelId, IKernel, IKernelSpecIds, IContentsManager,
  6. INotebookSessionManager, INotebookSession, ISessionId,
  7. IContentsOpts, ISessionOptions, IContentsModel
  8. } from 'jupyter-js-services';
  9. import * as utils
  10. from 'jupyter-js-utils';
  11. import {
  12. IDisposable
  13. } from 'phosphor-disposable';
  14. import {
  15. ISignal, Signal
  16. } from 'phosphor-signaling';
  17. import {
  18. Widget
  19. } from 'phosphor-widget';
  20. import {
  21. IDocumentContext, IDocumentModel, IModelFactoryOptions
  22. } from './index';
  23. /**
  24. * An implementation of a document context.
  25. */
  26. class Context implements IDocumentContext {
  27. /**
  28. * Construct a new document context.
  29. */
  30. constructor(manager: ContextManager) {
  31. this._manager = manager;
  32. this._id = utils.uuid();
  33. }
  34. /**
  35. * A signal emitted when the kernel changes.
  36. */
  37. get kernelChanged(): ISignal<IDocumentContext, IKernel> {
  38. return Private.kernelChangedSignal.bind(this);
  39. }
  40. /**
  41. * A signal emitted when the path changes.
  42. */
  43. get pathChanged(): ISignal<IDocumentContext, string> {
  44. return Private.pathChangedSignal.bind(this);
  45. }
  46. /**
  47. * A signal emitted when the model is saved or reverted.
  48. */
  49. get dirtyCleared(): ISignal<IDocumentContext, void> {
  50. return Private.dirtyClearedSignal.bind(this);
  51. }
  52. /**
  53. * The unique id of the context.
  54. *
  55. * #### Notes
  56. * This is a read-only property.
  57. */
  58. get id(): string {
  59. return this._id;
  60. }
  61. /**
  62. * The current kernel associated with the document.
  63. *
  64. * #### Notes
  65. * This is a read-only propery.
  66. */
  67. get kernel(): IKernel {
  68. return this._manager.getKernel(this._id);
  69. }
  70. /**
  71. * The current path associated with the document.
  72. *
  73. * #### Notes
  74. * This is a read-only property.
  75. */
  76. get path(): string {
  77. return this._manager.getPath(this._id);
  78. }
  79. /**
  80. * The current contents model associated with the document
  81. *
  82. * #### Notes
  83. * This is a read-only property. The model will have an
  84. * empty `contents` field.
  85. */
  86. get contentsModel(): IContentsModel {
  87. return this._manager.getContentsModel(this._id);
  88. }
  89. /**
  90. * Get the kernel spec information.
  91. *
  92. * #### Notes
  93. * This is a read-only property.
  94. */
  95. get kernelSpecs(): IKernelSpecIds {
  96. return this._manager.getKernelSpecs();
  97. }
  98. /**
  99. * Get whether the context has been disposed.
  100. */
  101. get isDisposed(): boolean {
  102. return this._manager === null;
  103. }
  104. /**
  105. * Dispose of the resources held by the context.
  106. */
  107. dispose(): void {
  108. if (this.isDisposed) {
  109. return;
  110. }
  111. this._manager = null;
  112. this._id = '';
  113. }
  114. /**
  115. * Change the current kernel associated with the document.
  116. */
  117. changeKernel(options: IKernelId): Promise<IKernel> {
  118. return this._manager.changeKernel(this._id, options);
  119. }
  120. /**
  121. * Save the document contents to disk.
  122. */
  123. save(): Promise<void> {
  124. return this._manager.save(this._id);
  125. }
  126. /**
  127. * Save the document to a different path.
  128. */
  129. saveAs(path: string): Promise<void> {
  130. return this._manager.saveAs(this._id, path);
  131. }
  132. /**
  133. * Revert the document contents to disk contents.
  134. */
  135. revert(): Promise<void> {
  136. return this._manager.revert(this._id);
  137. }
  138. /**
  139. * Get the list of running sessions.
  140. */
  141. listSessions(): Promise<ISessionId[]> {
  142. return this._manager.listSessions();
  143. }
  144. /**
  145. * Add a sibling widget to the document manager.
  146. *
  147. * @param widget - The widget to add to the document manager.
  148. *
  149. * @returns A disposable used to remove the sibling if desired.
  150. *
  151. * #### Notes
  152. * It is assumed that the widget has the same model and context
  153. * as the original widget.
  154. */
  155. addSibling(widget: Widget): IDisposable {
  156. return this._manager.addSibling(this._id, widget);
  157. }
  158. private _id = '';
  159. private _manager: ContextManager = null;
  160. }
  161. /**
  162. * An object which manages the active contexts.
  163. */
  164. export
  165. class ContextManager implements IDisposable {
  166. /**
  167. * Construct a new context manager.
  168. */
  169. constructor(contentsManager: IContentsManager, sessionManager: INotebookSessionManager, kernelSpecs: IKernelSpecIds, opener: (id: string, widget: Widget) => IDisposable) {
  170. this._contentsManager = contentsManager;
  171. this._sessionManager = sessionManager;
  172. this._opener = opener;
  173. this._kernelspecids = kernelSpecs;
  174. }
  175. /**
  176. * Get whether the context manager has been disposed.
  177. */
  178. get isDisposed(): boolean {
  179. return this._contentsManager === null;
  180. }
  181. /**
  182. * Dispose of the resources held by the document manager.
  183. */
  184. dispose(): void {
  185. if (this.isDisposed) {
  186. return;
  187. }
  188. this._contentsManager = null;
  189. this._sessionManager = null;
  190. this._kernelspecids = null;
  191. for (let id in this._contexts) {
  192. let contextEx = this._contexts[id];
  193. contextEx.context.dispose();
  194. contextEx.model.dispose();
  195. let session = contextEx.session;
  196. if (session) {
  197. session.dispose();
  198. }
  199. }
  200. this._contexts = null;
  201. this._opener = null;
  202. }
  203. /**
  204. * Create a new context.
  205. */
  206. createNew(path: string, model: IDocumentModel, options: IModelFactoryOptions): string {
  207. let context = new Context(this);
  208. let id = context.id;
  209. this._contexts[id] = {
  210. context,
  211. path,
  212. model,
  213. modelName: options.name,
  214. opts: options.contentsOptions,
  215. contentsModel: null,
  216. session: null
  217. };
  218. // Handle the session - use one created for another model on this
  219. // path or see if there is one running otherwise.
  220. if (this.getIdsForPath(path)) {
  221. this._syncSessions(path);
  222. } else {
  223. this._sessionManager.findByPath(path).then(sessionId => {
  224. let contextEx = this._contexts[id];
  225. let session = contextEx.session;
  226. if (session) {
  227. return;
  228. }
  229. let sOptions = {
  230. notebook: { path: contextEx.path },
  231. kernel: { id: sessionId.kernel.id }
  232. };
  233. this._startSession(id, sOptions);
  234. });
  235. }
  236. return id;
  237. }
  238. /**
  239. * Get a context for a given path and model name.
  240. */
  241. findContext(path: string, modelName: string): string {
  242. for (let id in this._contexts) {
  243. let contextEx = this._contexts[id];
  244. if (contextEx.path === path && contextEx.modelName === modelName) {
  245. return id;
  246. }
  247. }
  248. }
  249. /**
  250. * Find a context by path.
  251. */
  252. getIdsForPath(path: string): string[] {
  253. let ids: string[] = [];
  254. for (let id in this._contexts) {
  255. if (this._contexts[id].path === path) {
  256. ids.push(id);
  257. }
  258. }
  259. return ids;
  260. }
  261. /**
  262. * Get a context by id.
  263. */
  264. getContext(id: string): IDocumentContext {
  265. return this._contexts[id].context;
  266. }
  267. /**
  268. * Get the model associated with a context.
  269. */
  270. getModel(id: string): IDocumentModel {
  271. return this._contexts[id].model;
  272. }
  273. /**
  274. * Remove a context.
  275. */
  276. removeContext(id: string): INotebookSession {
  277. let contextEx = this._contexts[id];
  278. contextEx.model.dispose();
  279. contextEx.context.dispose();
  280. delete this._contexts[id];
  281. return contextEx.session;
  282. }
  283. /**
  284. * Get the current kernel associated with a document.
  285. */
  286. getKernel(id: string): IKernel {
  287. let session = this._contexts[id].session;
  288. return session ? session.kernel : null;
  289. }
  290. /**
  291. * Get the current path associated with a document.
  292. */
  293. getPath(id: string): string {
  294. return this._contexts[id].path;
  295. }
  296. /**
  297. * Get the current contents model associated with a document.
  298. */
  299. getContentsModel(id: string): IContentsModel {
  300. return this._contexts[id].contentsModel;
  301. }
  302. /**
  303. * Change the current kernel associated with the document.
  304. */
  305. changeKernel(id: string, options: IKernelId): Promise<IKernel> {
  306. let contextEx = this._contexts[id];
  307. this._syncSessions(contextEx.path);
  308. let session = contextEx.session;
  309. if (!session) {
  310. let path = contextEx.path;
  311. let sOptions = {
  312. notebook: { path },
  313. kernel: { options }
  314. };
  315. return this._startSession(id, sOptions);
  316. } else {
  317. return session.changeKernel(options);
  318. }
  319. }
  320. /**
  321. * Update the path of an open document.
  322. *
  323. * @param id - The id of the context.
  324. *
  325. * @param newPath - The new path.
  326. */
  327. rename(oldPath: string, newPath: string): void {
  328. this._syncSessions(oldPath);
  329. // Update all of the paths, but only update one session
  330. // so there is only one REST API call.
  331. let ids = this.getIdsForPath(oldPath);
  332. let sessionUpdated = false;
  333. for (let id of ids) {
  334. let contextEx = this._contexts[id];
  335. contextEx.path = newPath;
  336. contextEx.context.pathChanged.emit(newPath);
  337. if (!sessionUpdated) {
  338. let session = contextEx.session;
  339. if (session) {
  340. session.renameNotebook(newPath);
  341. sessionUpdated = true;
  342. }
  343. }
  344. }
  345. }
  346. /**
  347. * Get the current kernelspec information.
  348. */
  349. getKernelSpecs(): IKernelSpecIds {
  350. return this._kernelspecids;
  351. }
  352. /**
  353. * Save the document contents to disk.
  354. */
  355. save(id: string): Promise<void> {
  356. let contextEx = this._contexts[id];
  357. let opts = utils.copy(contextEx.opts);
  358. let path = contextEx.path;
  359. let model = contextEx.model;
  360. if (model.readOnly) {
  361. return Promise.reject(new Error('Read only'));
  362. }
  363. if (opts.format === 'json') {
  364. opts.content = model.toJSON();
  365. } else {
  366. opts.content = model.toString();
  367. }
  368. return this._contentsManager.save(path, opts).then(contents => {
  369. contextEx.contentsModel = this._copyContentsModel(contents);
  370. model.dirty = false;
  371. });
  372. }
  373. /**
  374. * Save a document to a new file name.
  375. *
  376. * This results in a new session.
  377. */
  378. saveAs(id: string, newPath: string): Promise<void> {
  379. let contextEx = this._contexts[id];
  380. contextEx.path = newPath;
  381. contextEx.context.pathChanged.emit(newPath);
  382. if (contextEx.session) {
  383. let options = {
  384. notebook: { path: newPath },
  385. kernel: { id: contextEx.session.id }
  386. };
  387. return this._startSession(id, options).then(() => {
  388. return this.save(id);
  389. });
  390. }
  391. return this.save(id);
  392. }
  393. /**
  394. * Revert the contents of a path.
  395. */
  396. revert(id: string): Promise<void> {
  397. let contextEx = this._contexts[id];
  398. let opts = contextEx.opts;
  399. let path = contextEx.path;
  400. let model = contextEx.model;
  401. return this._contentsManager.get(path, opts).then(contents => {
  402. if (contents.format === 'json') {
  403. model.fromJSON(contents.content);
  404. } else {
  405. model.fromString(contents.content);
  406. }
  407. contextEx.contentsModel = this._copyContentsModel(contents);
  408. model.dirty = false;
  409. });
  410. }
  411. /**
  412. * Get the list of running sessions.
  413. */
  414. listSessions(): Promise<ISessionId[]> {
  415. return this._sessionManager.listRunning();
  416. }
  417. /**
  418. * Add a sibling widget to the document manager.
  419. */
  420. addSibling(id: string, widget: Widget): IDisposable {
  421. let opener = this._opener;
  422. return opener(id, widget);
  423. }
  424. /**
  425. * Start a session and set up its signals.
  426. */
  427. private _startSession(id: string, options: ISessionOptions): Promise<IKernel> {
  428. let contextEx = this._contexts[id];
  429. let context = contextEx.context;
  430. return this._sessionManager.startNew(options).then(session => {
  431. if (contextEx.session) {
  432. contextEx.session.dispose();
  433. }
  434. contextEx.session = session;
  435. context.kernelChanged.emit(session.kernel);
  436. session.notebookPathChanged.connect((s, path) => {
  437. if (path !== contextEx.path) {
  438. contextEx.path = path;
  439. context.pathChanged.emit(path);
  440. }
  441. });
  442. this._syncSessions(session.notebookPath);
  443. session.kernelChanged.connect((s, kernel) => {
  444. context.kernelChanged.emit(kernel);
  445. });
  446. return session.kernel;
  447. });
  448. }
  449. /**
  450. * Make sure the same session is used for all of the contexts
  451. * associated with a path.
  452. */
  453. private _syncSessions(path: string): void {
  454. let session: INotebookSession;
  455. let ids = this.getIdsForPath(path);
  456. for (let id of ids) {
  457. if (this._contexts[id].session) {
  458. session = this._contexts[id].session;
  459. break;
  460. }
  461. }
  462. if (!session) {
  463. return;
  464. }
  465. let sOptions = {
  466. notebook: { path: session.notebookPath },
  467. kernel: { id: session.kernel.id }
  468. };
  469. for (let id of ids) {
  470. if (!this._contexts[id].session) {
  471. this._startSession(id, sOptions);
  472. }
  473. }
  474. }
  475. /**
  476. * Copy the contents of a contents model, without the content.
  477. */
  478. private _copyContentsModel(model: IContentsModel): IContentsModel {
  479. return {
  480. path: model.path,
  481. name: model.name,
  482. type: model.type,
  483. writable: model.writable,
  484. created: model.created,
  485. last_modified: model.last_modified,
  486. mimetype: model.mimetype,
  487. format: model.format
  488. };
  489. }
  490. private _contentsManager: IContentsManager = null;
  491. private _sessionManager: INotebookSessionManager = null;
  492. private _kernelspecids: IKernelSpecIds = null;
  493. private _contexts: { [key: string]: Private.IContextEx } = Object.create(null);
  494. private _opener: (id: string, widget: Widget) => IDisposable = null;
  495. }
  496. /**
  497. * A namespace for private data.
  498. */
  499. namespace Private {
  500. /**
  501. * An extended interface for data associated with a context.
  502. */
  503. export
  504. interface IContextEx {
  505. context: IDocumentContext;
  506. model: IDocumentModel;
  507. session: INotebookSession;
  508. opts: IContentsOpts;
  509. path: string;
  510. contentsModel: IContentsModel;
  511. modelName: string;
  512. }
  513. /**
  514. * A signal emitted when the kernel changes.
  515. */
  516. export
  517. const kernelChangedSignal = new Signal<IDocumentContext, IKernel>();
  518. /**
  519. * A signal emitted when the path changes.
  520. */
  521. export
  522. const pathChangedSignal = new Signal<IDocumentContext, string>();
  523. /**
  524. * A signal emitted when the model is saved or reverted.
  525. */
  526. export
  527. const dirtyClearedSignal = new Signal<IDocumentContext, void>();
  528. }