context.ts 14 KB

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