UNPKG

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