UNPKG

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