context.ts 13 KB

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