context.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. ContentsManager, IContents, IKernel, IServiceManager, ISession, utils
  5. } from 'jupyter-js-services';
  6. import {
  7. findIndex
  8. } from 'phosphor/lib/algorithm/searching';
  9. import {
  10. IDisposable, DisposableDelegate
  11. } from 'phosphor/lib/core/disposable';
  12. import {
  13. clearSignalData, defineSignal, ISignal
  14. } from 'phosphor/lib/core/signaling';
  15. import {
  16. Widget
  17. } from 'phosphor/lib/ui/widget';
  18. import {
  19. showDialog, okButton
  20. } from '../dialog';
  21. import {
  22. IDocumentContext, IDocumentModel, IModelFactory
  23. } from '../docregistry';
  24. import {
  25. SaveHandler
  26. } from './savehandler';
  27. /**
  28. * An implementation of a document context.
  29. *
  30. * This class is typically instantiated by the document manger.
  31. */
  32. export
  33. class Context<T extends IDocumentModel> implements IDocumentContext<T> {
  34. /**
  35. * Construct a new document context.
  36. */
  37. constructor(options: Context.IOptions<T>) {
  38. let manager = this._manager = options.manager;
  39. this._factory = options.factory;
  40. this._opener = options.opener;
  41. this._path = options.path;
  42. let ext = ContentsManager.extname(this._path);
  43. let lang = this._factory.preferredLanguage(ext);
  44. this._model = this._factory.createNew(lang);
  45. manager.sessions.runningChanged.connect(this._onSessionsChanged, this);
  46. this._saver = new SaveHandler({ context: this, manager });
  47. this._saver.start();
  48. }
  49. /**
  50. * A signal emitted when the kernel changes.
  51. */
  52. kernelChanged: ISignal<IDocumentContext<T>, IKernel>;
  53. /**
  54. * A signal emitted when the path changes.
  55. */
  56. pathChanged: ISignal<IDocumentContext<T>, string>;
  57. /**
  58. * A signal emitted when the model is saved or reverted.
  59. */
  60. contentsModelChanged: ISignal<IDocumentContext<T>, IContents.IModel>;
  61. /**
  62. * A signal emitted when the context is fully populated for the first time.
  63. */
  64. populated: ISignal<IDocumentContext<T>, void>;
  65. /**
  66. * A signal emitted when the context is disposed.
  67. */
  68. disposed: ISignal<IDocumentContext<T>, void>;
  69. /**
  70. * Get the model associated with the document.
  71. *
  72. * #### Notes
  73. * This is a read-only property
  74. */
  75. get model(): T {
  76. return this._model;
  77. }
  78. /**
  79. * The current kernel associated with the document.
  80. *
  81. * #### Notes
  82. * This is a read-only propery.
  83. */
  84. get kernel(): IKernel {
  85. return this._session ? this._session.kernel : null;
  86. }
  87. /**
  88. * The current path associated with the document.
  89. */
  90. get path(): string {
  91. return this._path;
  92. }
  93. /**
  94. * The current contents model associated with the document
  95. *
  96. * #### Notes
  97. * This is a read-only property. The model will have an
  98. * empty `contents` field.
  99. */
  100. get contentsModel(): IContents.IModel {
  101. return this._contentsModel;
  102. }
  103. /**
  104. * Get the kernel spec information.
  105. *
  106. * #### Notes
  107. * This is a read-only property.
  108. */
  109. get kernelspecs(): IKernel.ISpecModels {
  110. return this._manager.kernelspecs;
  111. }
  112. /**
  113. * Test whether the context is fully populated.
  114. *
  115. * #### Notes
  116. * This is a read-only property.
  117. */
  118. get isPopulated(): boolean {
  119. return this._isPopulated;
  120. }
  121. /**
  122. * Get the model factory name.
  123. *
  124. * #### Notes
  125. * This is a read-only property.
  126. */
  127. get factoryName(): string {
  128. return this.isDisposed ? '' : this._factory.name;
  129. }
  130. /**
  131. * Test whether the context has been disposed (read-only).
  132. */
  133. get isDisposed(): boolean {
  134. return this._manager === null;
  135. }
  136. /**
  137. * Dispose of the resources held by the context.
  138. */
  139. dispose(): void {
  140. if (this.isDisposed) {
  141. return;
  142. }
  143. this.disposed.emit(void 0);
  144. clearSignalData(this);
  145. this._model.dispose();
  146. this._manager = null;
  147. this._factory = null;
  148. }
  149. /**
  150. * Change the current kernel associated with the document.
  151. */
  152. changeKernel(options: IKernel.IModel): Promise<IKernel> {
  153. let session = this._session;
  154. if (options) {
  155. if (session) {
  156. return session.changeKernel(options);
  157. } else {
  158. let path = this._path;
  159. let sOptions: ISession.IOptions = {
  160. path,
  161. kernelName: options.name,
  162. kernelId: options.id
  163. };
  164. return this._startSession(sOptions);
  165. }
  166. } else {
  167. if (session) {
  168. return session.shutdown().then(() => {
  169. session.dispose();
  170. this._session = null;
  171. this.kernelChanged.emit(null);
  172. return void 0;
  173. });
  174. } else {
  175. return Promise.resolve(void 0);
  176. }
  177. }
  178. }
  179. /**
  180. * Set the path of the context.
  181. *
  182. * #### Notes
  183. * This is not intended to be called by the user.
  184. * It is assumed that the file has been renamed on the
  185. * contents manager prior to this operation.
  186. */
  187. setPath(value: string): void {
  188. this._path = value;
  189. let session = this._session;
  190. if (session) {
  191. session.rename(value);
  192. }
  193. this.pathChanged.emit(value);
  194. }
  195. /**
  196. * Save the document contents to disk.
  197. */
  198. save(): Promise<void> {
  199. let model = this._model;
  200. let contents = this._contentsModel || {};
  201. let path = this._path;
  202. contents.type = this._factory.fileType;
  203. contents.format = this._factory.fileFormat;
  204. if (model.readOnly) {
  205. return Promise.reject(new Error('Read only'));
  206. }
  207. if (contents.format === 'json') {
  208. contents.content = model.toJSON();
  209. } else {
  210. contents.content = model.toString();
  211. }
  212. return this._manager.contents.save(path, contents).then(newContents => {
  213. this._contentsModel = this._copyContentsModel(newContents);
  214. model.dirty = false;
  215. if (!this._isPopulated) {
  216. this._populate();
  217. }
  218. }).catch(err => {
  219. showDialog({
  220. title: 'File Save Error',
  221. body: err.xhr.responseText,
  222. buttons: [okButton]
  223. });
  224. });
  225. }
  226. /**
  227. * Save the document to a different path chosen by the user.
  228. */
  229. saveAs(): Promise<void> {
  230. return Private.getSavePath(this._path).then(newPath => {
  231. if (!newPath) {
  232. return;
  233. }
  234. this.setPath(newPath);
  235. let session = this._session;
  236. if (session) {
  237. let options: ISession.IOptions = {
  238. path: newPath,
  239. kernelId: session.kernel.id,
  240. kernelName: session.kernel.name
  241. };
  242. return this._startSession(options).then(() => {
  243. return this.save();
  244. });
  245. }
  246. return this.save();
  247. });
  248. }
  249. /**
  250. * Revert the document contents to disk contents.
  251. */
  252. revert(): Promise<void> {
  253. let opts: IContents.IFetchOptions = {
  254. format: this._factory.fileFormat,
  255. type: this._factory.fileType,
  256. content: true
  257. };
  258. let path = this._path;
  259. let model = this._model;
  260. return this._manager.contents.get(path, opts).then(contents => {
  261. if (contents.format === 'json') {
  262. model.fromJSON(contents.content);
  263. } else {
  264. model.fromString(contents.content);
  265. }
  266. let contentsModel = this._copyContentsModel(contents);
  267. this._contentsModel = contentsModel;
  268. if (contentsModel.last_modified !== this._contentsModel.last_modified) {
  269. this.contentsModelChanged.emit(contentsModel);
  270. }
  271. model.dirty = false;
  272. if (!this._isPopulated) {
  273. this._populate();
  274. }
  275. }).catch(err => {
  276. showDialog({
  277. title: 'File Load Error',
  278. body: err.xhr.responseText,
  279. buttons: [okButton]
  280. });
  281. });
  282. }
  283. /**
  284. * Create a checkpoint for the file.
  285. */
  286. createCheckpoint(): Promise<IContents.ICheckpointModel> {
  287. return this._manager.contents.createCheckpoint(this._path);
  288. }
  289. /**
  290. * Delete a checkpoint for the file.
  291. */
  292. deleteCheckpoint(checkpointID: string): Promise<void> {
  293. return this._manager.contents.deleteCheckpoint(this._path, checkpointID);
  294. }
  295. /**
  296. * Restore the file to a known checkpoint state.
  297. */
  298. restoreCheckpoint(checkpointID?: string): Promise<void> {
  299. let contents = this._manager.contents;
  300. let path = this._path;
  301. if (checkpointID) {
  302. return contents.restoreCheckpoint(path, checkpointID);
  303. }
  304. return this.listCheckpoints().then(checkpoints => {
  305. if (!checkpoints.length) {
  306. return;
  307. }
  308. checkpointID = checkpoints[checkpoints.length - 1].id;
  309. return contents.restoreCheckpoint(path, checkpointID);
  310. });
  311. }
  312. /**
  313. * List available checkpoints for a file.
  314. */
  315. listCheckpoints(): Promise<IContents.ICheckpointModel[]> {
  316. return this._manager.contents.listCheckpoints(this._path);
  317. }
  318. /**
  319. * Get the list of running sessions.
  320. */
  321. listSessions(): Promise<ISession.IModel[]> {
  322. return this._manager.sessions.listRunning();
  323. }
  324. /**
  325. * Resolve a relative url to a correct server path.
  326. */
  327. resolveUrl(url: string): string {
  328. // Ignore urls that have a protocol.
  329. if (utils.urlParse(url).protocol || url.indexOf('//') === 0) {
  330. return url;
  331. }
  332. let cwd = ContentsManager.dirname(this._path);
  333. let path = ContentsManager.getAbsolutePath(url, cwd);
  334. return this._manager.contents.getDownloadUrl(path);
  335. }
  336. /**
  337. * Add a sibling widget to the document manager.
  338. */
  339. addSibling(widget: Widget): IDisposable {
  340. let opener = this._opener;
  341. if (opener) {
  342. opener(widget);
  343. }
  344. return new DisposableDelegate(() => {
  345. widget.close();
  346. });
  347. }
  348. /**
  349. * Start a session and set up its signals.
  350. */
  351. private _startSession(options: ISession.IOptions): Promise<IKernel> {
  352. return this._manager.sessions.startNew(options).then(session => {
  353. if (this._session) {
  354. this._session.dispose();
  355. }
  356. this._session = session;
  357. this.kernelChanged.emit(session.kernel);
  358. session.pathChanged.connect((s, path) => {
  359. if (path !== this._path) {
  360. this.setPath(path);
  361. }
  362. });
  363. session.kernelChanged.connect((s, kernel) => {
  364. this.kernelChanged.emit(kernel);
  365. });
  366. return session.kernel;
  367. });
  368. }
  369. /**
  370. * Copy the contents of a contents model, without the content.
  371. */
  372. private _copyContentsModel(model: IContents.IModel): IContents.IModel {
  373. return {
  374. path: model.path,
  375. name: model.name,
  376. type: model.type,
  377. writable: model.writable,
  378. created: model.created,
  379. last_modified: model.last_modified,
  380. mimetype: model.mimetype,
  381. format: model.format
  382. };
  383. }
  384. /**
  385. * Handle a change to the running sessions.
  386. */
  387. private _onSessionsChanged(sender: ISession.IManager, models: ISession.IModel[]): void {
  388. let session = this._session;
  389. if (!session) {
  390. return;
  391. }
  392. let index = findIndex(models, model => model.id === session.id);
  393. if (index === -1) {
  394. session.dispose();
  395. this._session = null;
  396. this.kernelChanged.emit(null);
  397. }
  398. }
  399. /**
  400. * Handle an initial population.
  401. */
  402. private _populate(): void {
  403. this._isPopulated = true;
  404. // Add a checkpoint if none exists.
  405. this.listCheckpoints().then(checkpoints => {
  406. if (!checkpoints) {
  407. return this.createCheckpoint();
  408. }
  409. }).then(() => {
  410. this.populated.emit(void 0);
  411. });
  412. }
  413. private _manager: IServiceManager = null;
  414. private _opener: (widget: Widget) => void = null;
  415. private _model: T = null;
  416. private _path = '';
  417. private _session: ISession = null;
  418. private _factory: IModelFactory<T> = null;
  419. private _saver: SaveHandler = null;
  420. private _isPopulated = false;
  421. private _contentsModel: IContents.IModel = null;
  422. }
  423. // Define the signals for the `Context` class.
  424. defineSignal(Context.prototype, 'kernelChanged');
  425. defineSignal(Context.prototype, 'pathChanged');
  426. defineSignal(Context.prototype, 'contentsModelChanged');
  427. defineSignal(Context.prototype, 'populated');
  428. defineSignal(Context.prototype, 'disposed');
  429. /**
  430. * A namespace for `Context` statics.
  431. */
  432. export namespace Context {
  433. /**
  434. * The options used to initialize a context.
  435. */
  436. export
  437. interface IOptions<T extends IDocumentModel> {
  438. /**
  439. * A service manager instance.
  440. */
  441. manager: IServiceManager;
  442. /**
  443. * The model factory used to create the model.
  444. */
  445. factory: IModelFactory<T>;
  446. /**
  447. * The initial path of the file.
  448. */
  449. path: string;
  450. /**
  451. * An optional callback for opening sibling widgets.
  452. */
  453. opener?: (widget: Widget) => void;
  454. }
  455. }
  456. /**
  457. * A namespace for private data.
  458. */
  459. namespace Private {
  460. /**
  461. * Get a new file path from the user.
  462. */
  463. export
  464. function getSavePath(path: string): Promise<string> {
  465. let input = document.createElement('input');
  466. input.value = path;
  467. return showDialog({
  468. title: 'Save File As..',
  469. body: input,
  470. okText: 'SAVE'
  471. }).then(result => {
  472. if (result.text === 'SAVE') {
  473. return input.value;
  474. }
  475. });
  476. }
  477. }