context.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. Contents,
  5. ServiceManager,
  6. ServerConnection
  7. } from '@jupyterlab/services';
  8. import { JSONValue, PromiseDelegate } from '@phosphor/coreutils';
  9. import { IDisposable, DisposableDelegate } from '@phosphor/disposable';
  10. import { ISignal, Signal } from '@phosphor/signaling';
  11. import { Widget } from '@phosphor/widgets';
  12. import {
  13. showDialog,
  14. ClientSession,
  15. Dialog,
  16. IClientSession
  17. } from '@jupyterlab/apputils';
  18. import { PathExt } from '@jupyterlab/coreutils';
  19. import { IModelDB, ModelDB } from '@jupyterlab/observables';
  20. import { RenderMimeRegistry } from '@jupyterlab/rendermime';
  21. import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
  22. import { DocumentRegistry } from './registry';
  23. /**
  24. * An implementation of a document context.
  25. *
  26. * This class is typically instantiated by the document manager.
  27. */
  28. export class Context<T extends DocumentRegistry.IModel>
  29. implements DocumentRegistry.IContext<T> {
  30. /**
  31. * Construct a new document context.
  32. */
  33. constructor(options: Context.IOptions<T>) {
  34. let manager = (this._manager = options.manager);
  35. this._factory = options.factory;
  36. this._opener = options.opener || Private.noOp;
  37. this._path = options.path;
  38. const localPath = this._manager.contents.localPath(this._path);
  39. let lang = this._factory.preferredLanguage(PathExt.basename(localPath));
  40. let dbFactory = options.modelDBFactory;
  41. if (dbFactory) {
  42. const localPath = manager.contents.localPath(this._path);
  43. this._modelDB = dbFactory.createNew(localPath);
  44. this._model = this._factory.createNew(lang, this._modelDB);
  45. } else {
  46. this._model = this._factory.createNew(lang);
  47. }
  48. this._readyPromise = manager.ready.then(() => {
  49. return this._populatedPromise.promise;
  50. });
  51. let ext = PathExt.extname(this._path);
  52. this.session = new ClientSession({
  53. manager: manager.sessions,
  54. path: this._path,
  55. type: ext === '.ipynb' ? 'notebook' : 'file',
  56. name: PathExt.basename(localPath),
  57. kernelPreference: options.kernelPreference || { shouldStart: false },
  58. setBusy: options.setBusy
  59. });
  60. this.session.propertyChanged.connect(
  61. this._onSessionChanged,
  62. this
  63. );
  64. manager.contents.fileChanged.connect(
  65. this._onFileChanged,
  66. this
  67. );
  68. this.urlResolver = new RenderMimeRegistry.UrlResolver({
  69. session: this.session,
  70. contents: manager.contents
  71. });
  72. }
  73. /**
  74. * A signal emitted when the path changes.
  75. */
  76. get pathChanged(): ISignal<this, string> {
  77. return this._pathChanged;
  78. }
  79. /**
  80. * A signal emitted when the model is saved or reverted.
  81. */
  82. get fileChanged(): ISignal<this, Contents.IModel> {
  83. return this._fileChanged;
  84. }
  85. /**
  86. * A signal emitted on the start and end of a saving operation.
  87. */
  88. get saveState(): ISignal<this, DocumentRegistry.SaveState> {
  89. return this._saveState;
  90. }
  91. /**
  92. * A signal emitted when the context is disposed.
  93. */
  94. get disposed(): ISignal<this, void> {
  95. return this._disposed;
  96. }
  97. /**
  98. * Get the model associated with the document.
  99. */
  100. get model(): T {
  101. return this._model;
  102. }
  103. /**
  104. * The client session object associated with the context.
  105. */
  106. readonly session: ClientSession;
  107. /**
  108. * The current path associated with the document.
  109. */
  110. get path(): string {
  111. return this._path;
  112. }
  113. /**
  114. * The current local path associated with the document.
  115. * If the document is in the default notebook file browser,
  116. * this is the same as the path.
  117. */
  118. get localPath(): string {
  119. return this._manager.contents.localPath(this._path);
  120. }
  121. /**
  122. * The current contents model associated with the document.
  123. *
  124. * #### Notes
  125. * The contents model will be null until the context is populated.
  126. * It will have an empty `contents` field.
  127. */
  128. get contentsModel(): Contents.IModel | null {
  129. return this._contentsModel;
  130. }
  131. /**
  132. * Get the model factory name.
  133. *
  134. * #### Notes
  135. * This is not part of the `IContext` API.
  136. */
  137. get factoryName(): string {
  138. return this.isDisposed ? '' : this._factory.name;
  139. }
  140. /**
  141. * Test whether the context is disposed.
  142. */
  143. get isDisposed(): boolean {
  144. return this._isDisposed;
  145. }
  146. /**
  147. * Dispose of the resources held by the context.
  148. */
  149. dispose(): void {
  150. if (this.isDisposed) {
  151. return;
  152. }
  153. this._isDisposed = true;
  154. this.session.dispose();
  155. if (this._modelDB) {
  156. this._modelDB.dispose();
  157. }
  158. this._model.dispose();
  159. this._disposed.emit(void 0);
  160. Signal.clearData(this);
  161. }
  162. /**
  163. * Whether the context is ready.
  164. */
  165. get isReady(): boolean {
  166. return this._isReady;
  167. }
  168. /**
  169. * A promise that is fulfilled when the context is ready.
  170. */
  171. get ready(): Promise<void> {
  172. return this._readyPromise;
  173. }
  174. /**
  175. * The url resolver for the context.
  176. */
  177. readonly urlResolver: IRenderMime.IResolver;
  178. /**
  179. * Initialize the context.
  180. *
  181. * @param isNew - Whether it is a new file.
  182. *
  183. * @returns a promise that resolves upon initialization.
  184. */
  185. initialize(isNew: boolean): Promise<void> {
  186. if (isNew) {
  187. this._model.initialize();
  188. return this._save();
  189. }
  190. if (this._modelDB) {
  191. return this._modelDB.connected.then(() => {
  192. if (this._modelDB.isPrepopulated) {
  193. this._model.initialize();
  194. this._save();
  195. return void 0;
  196. } else {
  197. return this._revert(true);
  198. }
  199. });
  200. } else {
  201. return this._revert(true);
  202. }
  203. }
  204. /**
  205. * Save the document contents to disk.
  206. */
  207. save(): Promise<void> {
  208. return this.ready.then(() => {
  209. return this._save();
  210. });
  211. }
  212. /**
  213. * Save the document to a different path chosen by the user.
  214. */
  215. saveAs(): Promise<void> {
  216. return this.ready
  217. .then(() => {
  218. return Private.getSavePath(this._path);
  219. })
  220. .then(newPath => {
  221. if (this.isDisposed || !newPath) {
  222. return;
  223. }
  224. if (newPath === this._path) {
  225. return this.save();
  226. }
  227. // Make sure the path does not exist.
  228. return this._manager.ready
  229. .then(() => {
  230. return this._manager.contents.get(newPath);
  231. })
  232. .then(() => {
  233. return this._maybeOverWrite(newPath);
  234. })
  235. .catch(err => {
  236. if (!err.response || err.response.status !== 404) {
  237. throw err;
  238. }
  239. return this._finishSaveAs(newPath);
  240. });
  241. });
  242. }
  243. /**
  244. * Revert the document contents to disk contents.
  245. */
  246. revert(): Promise<void> {
  247. return this.ready.then(() => {
  248. return this._revert();
  249. });
  250. }
  251. /**
  252. * Create a checkpoint for the file.
  253. */
  254. createCheckpoint(): Promise<Contents.ICheckpointModel> {
  255. let contents = this._manager.contents;
  256. return this._manager.ready.then(() => {
  257. return contents.createCheckpoint(this._path);
  258. });
  259. }
  260. /**
  261. * Delete a checkpoint for the file.
  262. */
  263. deleteCheckpoint(checkpointId: string): Promise<void> {
  264. let contents = this._manager.contents;
  265. return this._manager.ready.then(() => {
  266. return contents.deleteCheckpoint(this._path, checkpointId);
  267. });
  268. }
  269. /**
  270. * Restore the file to a known checkpoint state.
  271. */
  272. restoreCheckpoint(checkpointId?: string): Promise<void> {
  273. let contents = this._manager.contents;
  274. let path = this._path;
  275. return this._manager.ready.then(() => {
  276. if (checkpointId) {
  277. return contents.restoreCheckpoint(path, checkpointId);
  278. }
  279. return this.listCheckpoints().then(checkpoints => {
  280. if (this.isDisposed || !checkpoints.length) {
  281. return;
  282. }
  283. checkpointId = checkpoints[checkpoints.length - 1].id;
  284. return contents.restoreCheckpoint(path, checkpointId);
  285. });
  286. });
  287. }
  288. /**
  289. * List available checkpoints for a file.
  290. */
  291. listCheckpoints(): Promise<Contents.ICheckpointModel[]> {
  292. let contents = this._manager.contents;
  293. return this._manager.ready.then(() => {
  294. return contents.listCheckpoints(this._path);
  295. });
  296. }
  297. /**
  298. * Add a sibling widget to the document manager.
  299. *
  300. * @param widget - The widget to add to the document manager.
  301. *
  302. * @param options - The desired options for adding the sibling.
  303. *
  304. * @returns A disposable used to remove the sibling if desired.
  305. *
  306. * #### Notes
  307. * It is assumed that the widget has the same model and context
  308. * as the original widget.
  309. */
  310. addSibling(
  311. widget: Widget,
  312. options: DocumentRegistry.IOpenOptions = {}
  313. ): IDisposable {
  314. let opener = this._opener;
  315. if (opener) {
  316. opener(widget, options);
  317. }
  318. return new DisposableDelegate(() => {
  319. widget.close();
  320. });
  321. }
  322. /**
  323. * Handle a change on the contents manager.
  324. */
  325. private _onFileChanged(
  326. sender: Contents.IManager,
  327. change: Contents.IChangedArgs
  328. ): void {
  329. if (change.type !== 'rename') {
  330. return;
  331. }
  332. let oldPath = change.oldValue && change.oldValue.path;
  333. let newPath = change.newValue && change.newValue.path;
  334. if (newPath && this._path.indexOf(oldPath) === 0) {
  335. let changeModel = change.newValue;
  336. // When folder name changed, `oldPath` is `foo`, `newPath` is `bar` and `this._path` is `foo/test`,
  337. // we should update `foo/test` to `bar/test` as well
  338. if (oldPath !== this._path) {
  339. newPath = this._path.replace(new RegExp(`^${oldPath}`), newPath);
  340. oldPath = this._path;
  341. // Update client file model from folder change
  342. changeModel = {
  343. last_modified: change.newValue.created,
  344. path: newPath
  345. };
  346. }
  347. this._path = newPath;
  348. this.session.setPath(newPath);
  349. const updateModel = {
  350. ...this._contentsModel,
  351. ...changeModel
  352. };
  353. const localPath = this._manager.contents.localPath(newPath);
  354. this.session.setName(PathExt.basename(localPath));
  355. this._updateContentsModel(updateModel as Contents.IModel);
  356. this._pathChanged.emit(this._path);
  357. }
  358. }
  359. /**
  360. * Handle a change to a session property.
  361. */
  362. private _onSessionChanged(sender: IClientSession, type: string): void {
  363. if (type !== 'path') {
  364. return;
  365. }
  366. let path = this.session.path;
  367. if (path !== this._path) {
  368. this._path = path;
  369. this._pathChanged.emit(path);
  370. }
  371. }
  372. /**
  373. * Update our contents model, without the content.
  374. */
  375. private _updateContentsModel(model: Contents.IModel): void {
  376. let newModel: Contents.IModel = {
  377. path: model.path,
  378. name: model.name,
  379. type: model.type,
  380. content: undefined,
  381. writable: model.writable,
  382. created: model.created,
  383. last_modified: model.last_modified,
  384. mimetype: model.mimetype,
  385. format: model.format
  386. };
  387. let mod = this._contentsModel ? this._contentsModel.last_modified : null;
  388. this._contentsModel = newModel;
  389. if (!mod || newModel.last_modified !== mod) {
  390. this._fileChanged.emit(newModel);
  391. }
  392. }
  393. /**
  394. * Handle an initial population.
  395. */
  396. private _populate(): Promise<void> {
  397. this._isPopulated = true;
  398. this._isReady = true;
  399. this._populatedPromise.resolve(void 0);
  400. // Add a checkpoint if none exists and the file is writable.
  401. return this._maybeCheckpoint(false).then(() => {
  402. if (this.isDisposed) {
  403. return;
  404. }
  405. // Update the kernel preference.
  406. let name =
  407. this._model.defaultKernelName || this.session.kernelPreference.name;
  408. this.session.kernelPreference = {
  409. ...this.session.kernelPreference,
  410. name,
  411. language: this._model.defaultKernelLanguage
  412. };
  413. this.session.initialize();
  414. });
  415. }
  416. /**
  417. * Save the document contents to disk.
  418. */
  419. private _save(): Promise<void> {
  420. this._saveState.emit('started');
  421. let model = this._model;
  422. let content: JSONValue;
  423. if (this._factory.fileFormat === 'json') {
  424. content = model.toJSON();
  425. } else {
  426. content = model.toString();
  427. if (this._useCRLF) {
  428. content = content.replace(/\n/g, '\r\n');
  429. }
  430. }
  431. let options = {
  432. type: this._factory.contentType,
  433. format: this._factory.fileFormat,
  434. content
  435. };
  436. return this._manager.ready
  437. .then(() => {
  438. if (!model.modelDB.isCollaborative) {
  439. return this._maybeSave(options);
  440. }
  441. return this._manager.contents.save(this._path, options);
  442. })
  443. .then(value => {
  444. if (this.isDisposed) {
  445. return;
  446. }
  447. model.dirty = false;
  448. this._updateContentsModel(value);
  449. if (!this._isPopulated) {
  450. return this._populate();
  451. }
  452. })
  453. .catch(err => {
  454. // If the save has been canceled by the user,
  455. // throw the error so that whoever called save()
  456. // can decide what to do.
  457. if (err.message === 'Cancel') {
  458. throw err;
  459. }
  460. // Otherwise show an error message and throw the error.
  461. const localPath = this._manager.contents.localPath(this._path);
  462. const name = PathExt.basename(localPath);
  463. this._handleError(err, `File Save Error for ${name}`);
  464. throw err;
  465. })
  466. .then(
  467. value => {
  468. // Capture all success paths and emit completion.
  469. this._saveState.emit('completed');
  470. return value;
  471. },
  472. err => {
  473. // Capture all error paths and emit failure.
  474. this._saveState.emit('failed');
  475. throw err;
  476. }
  477. )
  478. .catch();
  479. }
  480. /**
  481. * Revert the document contents to disk contents.
  482. *
  483. * @param initializeModel - call the model's initialization function after
  484. * deserializing the content.
  485. */
  486. private _revert(initializeModel: boolean = false): Promise<void> {
  487. let opts: Contents.IFetchOptions = {
  488. format: this._factory.fileFormat,
  489. type: this._factory.contentType,
  490. content: true
  491. };
  492. let path = this._path;
  493. let model = this._model;
  494. return this._manager.ready
  495. .then(() => {
  496. return this._manager.contents.get(path, opts);
  497. })
  498. .then(contents => {
  499. if (this.isDisposed) {
  500. return;
  501. }
  502. let dirty = false;
  503. if (contents.format === 'json') {
  504. model.fromJSON(contents.content);
  505. if (initializeModel) {
  506. model.initialize();
  507. }
  508. } else {
  509. let content = contents.content;
  510. // Convert line endings if necessary, marking the file
  511. // as dirty.
  512. if (content.indexOf('\r') !== -1) {
  513. this._useCRLF = true;
  514. content = content.replace(/\r\n/g, '\n');
  515. } else {
  516. this._useCRLF = false;
  517. }
  518. model.fromString(content);
  519. if (initializeModel) {
  520. model.initialize();
  521. }
  522. }
  523. this._updateContentsModel(contents);
  524. model.dirty = dirty;
  525. if (!this._isPopulated) {
  526. return this._populate();
  527. }
  528. })
  529. .catch(err => {
  530. const localPath = this._manager.contents.localPath(this._path);
  531. const name = PathExt.basename(localPath);
  532. if (err.message === 'Invalid response: 400 bad format') {
  533. err = new Error('JupyterLab is unable to open this file type.');
  534. }
  535. this._handleError(err, `File Load Error for ${name}`);
  536. throw err;
  537. });
  538. }
  539. /**
  540. * Save a file, dealing with conflicts.
  541. */
  542. private _maybeSave(
  543. options: Partial<Contents.IModel>
  544. ): Promise<Contents.IModel> {
  545. let path = this._path;
  546. // Make sure the file has not changed on disk.
  547. let promise = this._manager.contents.get(path, { content: false });
  548. return promise.then(
  549. model => {
  550. if (this.isDisposed) {
  551. return Promise.reject(new Error('Disposed'));
  552. }
  553. // We want to check last_modified (disk) > last_modified (client)
  554. // (our last save)
  555. // In some cases the filesystem reports an inconsistent time,
  556. // so we allow 0.5 seconds difference before complaining.
  557. let modified = this.contentsModel && this.contentsModel.last_modified;
  558. let tClient = new Date(modified);
  559. let tDisk = new Date(model.last_modified);
  560. if (modified && tDisk.getTime() - tClient.getTime() > 500) {
  561. // 500 ms
  562. return this._timeConflict(tClient, model, options);
  563. }
  564. return this._manager.contents.save(path, options);
  565. },
  566. err => {
  567. if (err.response && err.response.status === 404) {
  568. return this._manager.contents.save(path, options);
  569. }
  570. throw err;
  571. }
  572. );
  573. }
  574. /**
  575. * Handle a save/load error with a dialog.
  576. */
  577. private _handleError(
  578. err: Error | ServerConnection.ResponseError,
  579. title: string
  580. ): void {
  581. let buttons = [Dialog.okButton()];
  582. // Check for a more specific error message.
  583. if (err instanceof ServerConnection.ResponseError) {
  584. err.response.text().then(text => {
  585. let body = '';
  586. try {
  587. body = JSON.parse(text).message;
  588. } catch (e) {
  589. body = text;
  590. }
  591. body = body || err.message;
  592. showDialog({ title, body, buttons });
  593. });
  594. } else {
  595. let body = err.message;
  596. showDialog({ title, body, buttons });
  597. }
  598. }
  599. /**
  600. * Add a checkpoint the file is writable.
  601. */
  602. private _maybeCheckpoint(force: boolean): Promise<void> {
  603. let writable = this._contentsModel && this._contentsModel.writable;
  604. let promise = Promise.resolve(void 0);
  605. if (!writable) {
  606. return promise;
  607. }
  608. if (force) {
  609. promise = this.createCheckpoint();
  610. } else {
  611. promise = this.listCheckpoints().then(checkpoints => {
  612. writable = this._contentsModel && this._contentsModel.writable;
  613. if (!this.isDisposed && !checkpoints.length && writable) {
  614. return this.createCheckpoint().then(() => {
  615. /* no-op */
  616. });
  617. }
  618. });
  619. }
  620. return promise.catch(err => {
  621. // Handle a read-only folder.
  622. if (!err.response || err.response.status !== 403) {
  623. throw err;
  624. }
  625. });
  626. }
  627. /**
  628. * Handle a time conflict.
  629. */
  630. private _timeConflict(
  631. tClient: Date,
  632. model: Contents.IModel,
  633. options: Partial<Contents.IModel>
  634. ): Promise<Contents.IModel> {
  635. let tDisk = new Date(model.last_modified);
  636. console.warn(
  637. `Last saving performed ${tClient} ` +
  638. `while the current file seems to have been saved ` +
  639. `${tDisk}`
  640. );
  641. let body =
  642. `The file has changed on disk since the last time it ` +
  643. `was opened or saved. ` +
  644. `Do you want to overwrite the file on disk with the version ` +
  645. ` open here, or load the version on disk (revert)?`;
  646. let revertBtn = Dialog.okButton({ label: 'REVERT' });
  647. let overwriteBtn = Dialog.warnButton({ label: 'OVERWRITE' });
  648. return showDialog({
  649. title: 'File Changed',
  650. body,
  651. buttons: [Dialog.cancelButton(), revertBtn, overwriteBtn]
  652. }).then(result => {
  653. if (this.isDisposed) {
  654. return Promise.reject(new Error('Disposed'));
  655. }
  656. if (result.button.label === 'OVERWRITE') {
  657. return this._manager.contents.save(this._path, options);
  658. }
  659. if (result.button.label === 'REVERT') {
  660. return this.revert().then(() => {
  661. return model;
  662. });
  663. }
  664. return Promise.reject(new Error('Cancel')); // Otherwise cancel the save.
  665. });
  666. }
  667. /**
  668. * Handle a time conflict.
  669. */
  670. private _maybeOverWrite(path: string): Promise<void> {
  671. let body = `"${path}" already exists. Do you want to replace it?`;
  672. let overwriteBtn = Dialog.warnButton({ label: 'OVERWRITE' });
  673. return showDialog({
  674. title: 'File Overwrite?',
  675. body,
  676. buttons: [Dialog.cancelButton(), overwriteBtn]
  677. }).then(result => {
  678. if (this.isDisposed) {
  679. return Promise.reject(new Error('Disposed'));
  680. }
  681. if (result.button.label === 'OVERWRITE') {
  682. return this._manager.contents.delete(path).then(() => {
  683. this._finishSaveAs(path);
  684. });
  685. }
  686. });
  687. }
  688. /**
  689. * Finish a saveAs operation given a new path.
  690. */
  691. private _finishSaveAs(newPath: string): Promise<void> {
  692. this._path = newPath;
  693. return this.session
  694. .setPath(newPath)
  695. .then(() => {
  696. this.session.setName(newPath.split('/').pop()!);
  697. return this.save();
  698. })
  699. .then(() => {
  700. this._pathChanged.emit(this._path);
  701. return this._maybeCheckpoint(true);
  702. });
  703. }
  704. private _manager: ServiceManager.IManager;
  705. private _opener: (
  706. widget: Widget,
  707. options?: DocumentRegistry.IOpenOptions
  708. ) => void;
  709. private _model: T;
  710. private _modelDB: IModelDB;
  711. private _path = '';
  712. private _useCRLF = false;
  713. private _factory: DocumentRegistry.IModelFactory<T>;
  714. private _contentsModel: Contents.IModel | null = null;
  715. private _readyPromise: Promise<void>;
  716. private _populatedPromise = new PromiseDelegate<void>();
  717. private _isPopulated = false;
  718. private _isReady = false;
  719. private _isDisposed = false;
  720. private _pathChanged = new Signal<this, string>(this);
  721. private _fileChanged = new Signal<this, Contents.IModel>(this);
  722. private _saveState = new Signal<this, DocumentRegistry.SaveState>(this);
  723. private _disposed = new Signal<this, void>(this);
  724. }
  725. /**
  726. * A namespace for `Context` statics.
  727. */
  728. export namespace Context {
  729. /**
  730. * The options used to initialize a context.
  731. */
  732. export interface IOptions<T extends DocumentRegistry.IModel> {
  733. /**
  734. * A service manager instance.
  735. */
  736. manager: ServiceManager.IManager;
  737. /**
  738. * The model factory used to create the model.
  739. */
  740. factory: DocumentRegistry.IModelFactory<T>;
  741. /**
  742. * The initial path of the file.
  743. */
  744. path: string;
  745. /**
  746. * The kernel preference associated with the context.
  747. */
  748. kernelPreference?: IClientSession.IKernelPreference;
  749. /**
  750. * An IModelDB factory method which may be used for the document.
  751. */
  752. modelDBFactory?: ModelDB.IFactory;
  753. /**
  754. * An optional callback for opening sibling widgets.
  755. */
  756. opener?: (widget: Widget) => void;
  757. /**
  758. * A function to call when the kernel is busy.
  759. */
  760. setBusy?: () => IDisposable;
  761. }
  762. }
  763. /**
  764. * A namespace for private data.
  765. */
  766. namespace Private {
  767. /**
  768. * Get a new file path from the user.
  769. */
  770. export function getSavePath(path: string): Promise<string | undefined> {
  771. let saveBtn = Dialog.okButton({ label: 'SAVE' });
  772. return showDialog({
  773. title: 'Save File As..',
  774. body: new SaveWidget(path),
  775. buttons: [Dialog.cancelButton(), saveBtn]
  776. }).then(result => {
  777. if (result.button.label === 'SAVE') {
  778. return result.value;
  779. }
  780. return;
  781. });
  782. }
  783. /**
  784. * A no-op function.
  785. */
  786. export function noOp() {
  787. /* no-op */
  788. }
  789. /*
  790. * A widget that gets a file path from a user.
  791. */
  792. class SaveWidget extends Widget {
  793. /**
  794. * Construct a new save widget.
  795. */
  796. constructor(path: string) {
  797. super({ node: createSaveNode(path) });
  798. }
  799. /**
  800. * Get the value for the widget.
  801. */
  802. getValue(): string {
  803. return (this.node as HTMLInputElement).value;
  804. }
  805. }
  806. /**
  807. * Create the node for a save widget.
  808. */
  809. function createSaveNode(path: string): HTMLElement {
  810. let input = document.createElement('input');
  811. input.value = path;
  812. return input;
  813. }
  814. }