UNPKG

21.2 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { SessionContextDialogs } from '@jupyterlab/apputils';
4import { PathExt } from '@jupyterlab/coreutils';
5import { Context } from '@jupyterlab/docregistry';
6import { nullTranslator } from '@jupyterlab/translation';
7import { ArrayExt, find } from '@lumino/algorithm';
8import { UUID } from '@lumino/coreutils';
9import { AttachedProperty } from '@lumino/properties';
10import { Signal } from '@lumino/signaling';
11import { SaveHandler } from './savehandler';
12import { DocumentWidgetManager } from './widgetmanager';
13/**
14 * The document manager.
15 *
16 * #### Notes
17 * The document manager is used to register model and widget creators,
18 * and the file browser uses the document manager to create widgets. The
19 * document manager maintains a context for each path and model type that is
20 * open, and a list of widgets for each context. The document manager is in
21 * control of the proper closing and disposal of the widgets and contexts.
22 */
23export class DocumentManager {
24 /**
25 * Construct a new document manager.
26 */
27 constructor(options) {
28 var _a;
29 this._activateRequested = new Signal(this);
30 this._contexts = [];
31 this._isDisposed = false;
32 this._autosave = true;
33 this._autosaveInterval = 120;
34 this._lastModifiedCheckMargin = 500;
35 this._renameUntitledFileOnSave = true;
36 this._stateChanged = new Signal(this);
37 this.translator = options.translator || nullTranslator;
38 this.registry = options.registry;
39 this.services = options.manager;
40 this._dialogs =
41 (_a = options.sessionDialogs) !== null && _a !== void 0 ? _a : new SessionContextDialogs({ translator: options.translator });
42 this._isConnectedCallback = options.isConnectedCallback || (() => true);
43 this._opener = options.opener;
44 this._when = options.when || options.manager.ready;
45 const widgetManager = new DocumentWidgetManager({
46 registry: this.registry,
47 translator: this.translator,
48 recentsManager: options.recentsManager
49 });
50 widgetManager.activateRequested.connect(this._onActivateRequested, this);
51 widgetManager.stateChanged.connect(this._onWidgetStateChanged, this);
52 this._widgetManager = widgetManager;
53 this._setBusy = options.setBusy;
54 }
55 /**
56 * A signal emitted when one of the documents is activated.
57 */
58 get activateRequested() {
59 return this._activateRequested;
60 }
61 /**
62 * Whether to autosave documents.
63 */
64 get autosave() {
65 return this._autosave;
66 }
67 set autosave(value) {
68 if (this._autosave !== value) {
69 const oldValue = this._autosave;
70 this._autosave = value;
71 // For each existing context, start/stop the autosave handler as needed.
72 this._contexts.forEach(context => {
73 const handler = Private.saveHandlerProperty.get(context);
74 if (!handler) {
75 return;
76 }
77 if (value === true && !handler.isActive) {
78 handler.start();
79 }
80 else if (value === false && handler.isActive) {
81 handler.stop();
82 }
83 });
84 this._stateChanged.emit({
85 name: 'autosave',
86 oldValue,
87 newValue: value
88 });
89 }
90 }
91 /**
92 * Determines the time interval for autosave in seconds.
93 */
94 get autosaveInterval() {
95 return this._autosaveInterval;
96 }
97 set autosaveInterval(value) {
98 if (this._autosaveInterval !== value) {
99 const oldValue = this._autosaveInterval;
100 this._autosaveInterval = value;
101 // For each existing context, set the save interval as needed.
102 this._contexts.forEach(context => {
103 const handler = Private.saveHandlerProperty.get(context);
104 if (!handler) {
105 return;
106 }
107 handler.saveInterval = value || 120;
108 });
109 this._stateChanged.emit({
110 name: 'autosaveInterval',
111 oldValue,
112 newValue: value
113 });
114 }
115 }
116 /**
117 * Whether to ask confirmation to close a tab or not.
118 */
119 get confirmClosingDocument() {
120 return this._widgetManager.confirmClosingDocument;
121 }
122 set confirmClosingDocument(value) {
123 if (this._widgetManager.confirmClosingDocument !== value) {
124 const oldValue = this._widgetManager.confirmClosingDocument;
125 this._widgetManager.confirmClosingDocument = value;
126 this._stateChanged.emit({
127 name: 'confirmClosingDocument',
128 oldValue,
129 newValue: value
130 });
131 }
132 }
133 /**
134 * Defines max acceptable difference, in milliseconds, between last modified timestamps on disk and client
135 */
136 get lastModifiedCheckMargin() {
137 return this._lastModifiedCheckMargin;
138 }
139 set lastModifiedCheckMargin(value) {
140 if (this._lastModifiedCheckMargin !== value) {
141 const oldValue = this._lastModifiedCheckMargin;
142 this._lastModifiedCheckMargin = value;
143 // For each existing context, update the margin value.
144 this._contexts.forEach(context => {
145 context.lastModifiedCheckMargin = value;
146 });
147 this._stateChanged.emit({
148 name: 'lastModifiedCheckMargin',
149 oldValue,
150 newValue: value
151 });
152 }
153 }
154 /**
155 * Whether to ask the user to rename untitled file on first manual save.
156 */
157 get renameUntitledFileOnSave() {
158 return this._renameUntitledFileOnSave;
159 }
160 set renameUntitledFileOnSave(value) {
161 if (this._renameUntitledFileOnSave !== value) {
162 const oldValue = this._renameUntitledFileOnSave;
163 this._renameUntitledFileOnSave = value;
164 this._stateChanged.emit({
165 name: 'renameUntitledFileOnSave',
166 oldValue,
167 newValue: value
168 });
169 }
170 }
171 /**
172 * Signal triggered when an attribute changes.
173 */
174 get stateChanged() {
175 return this._stateChanged;
176 }
177 /**
178 * Get whether the document manager has been disposed.
179 */
180 get isDisposed() {
181 return this._isDisposed;
182 }
183 /**
184 * Dispose of the resources held by the document manager.
185 */
186 dispose() {
187 if (this.isDisposed) {
188 return;
189 }
190 this._isDisposed = true;
191 // Clear any listeners for our signals.
192 Signal.clearData(this);
193 // Close all the widgets for our contexts and dispose the widget manager.
194 this._contexts.forEach(context => {
195 return this._widgetManager.closeWidgets(context);
196 });
197 this._widgetManager.dispose();
198 // Clear the context list.
199 this._contexts.length = 0;
200 }
201 /**
202 * Clone a widget.
203 *
204 * @param widget - The source widget.
205 *
206 * @returns A new widget or `undefined`.
207 *
208 * #### Notes
209 * Uses the same widget factory and context as the source, or returns
210 * `undefined` if the source widget is not managed by this manager.
211 */
212 cloneWidget(widget) {
213 return this._widgetManager.cloneWidget(widget);
214 }
215 /**
216 * Close all of the open documents.
217 *
218 * @returns A promise resolving when the widgets are closed.
219 */
220 closeAll() {
221 return Promise.all(this._contexts.map(context => this._widgetManager.closeWidgets(context))).then(() => undefined);
222 }
223 /**
224 * Close the widgets associated with a given path.
225 *
226 * @param path - The target path.
227 *
228 * @returns A promise resolving when the widgets are closed.
229 */
230 closeFile(path) {
231 const close = this._contextsForPath(path).map(c => this._widgetManager.closeWidgets(c));
232 return Promise.all(close).then(x => undefined);
233 }
234 /**
235 * Get the document context for a widget.
236 *
237 * @param widget - The widget of interest.
238 *
239 * @returns The context associated with the widget, or `undefined` if no such
240 * context exists.
241 */
242 contextForWidget(widget) {
243 return this._widgetManager.contextForWidget(widget);
244 }
245 /**
246 * Copy a file.
247 *
248 * @param fromFile - The full path of the original file.
249 *
250 * @param toDir - The full path to the target directory.
251 *
252 * @returns A promise which resolves to the contents of the file.
253 */
254 copy(fromFile, toDir) {
255 return this.services.contents.copy(fromFile, toDir);
256 }
257 /**
258 * Create a new file and return the widget used to view it.
259 *
260 * @param path - The file path to create.
261 *
262 * @param widgetName - The name of the widget factory to use. 'default' will use the default widget.
263 *
264 * @param kernel - An optional kernel name/id to override the default.
265 *
266 * @returns The created widget, or `undefined`.
267 *
268 * #### Notes
269 * This function will return `undefined` if a valid widget factory
270 * cannot be found.
271 */
272 createNew(path, widgetName = 'default', kernel) {
273 return this._createOrOpenDocument('create', path, widgetName, kernel);
274 }
275 /**
276 * Delete a file.
277 *
278 * @param path - The full path to the file to be deleted.
279 *
280 * @returns A promise which resolves when the file is deleted.
281 *
282 * #### Notes
283 * If there is a running session associated with the file and no other
284 * sessions are using the kernel, the session will be shut down.
285 */
286 deleteFile(path) {
287 return this.services.sessions
288 .stopIfNeeded(path)
289 .then(() => {
290 return this.services.contents.delete(path);
291 })
292 .then(() => {
293 this._contextsForPath(path).forEach(context => this._widgetManager.deleteWidgets(context));
294 return Promise.resolve(void 0);
295 });
296 }
297 /**
298 * Duplicate a file.
299 *
300 * @param path - The full path to the file to be duplicated.
301 *
302 * @returns A promise which resolves when the file is duplicated.
303 */
304 duplicate(path) {
305 const basePath = PathExt.dirname(path);
306 return this.services.contents.copy(path, basePath);
307 }
308 /**
309 * See if a widget already exists for the given path and widget name.
310 *
311 * @param path - The file path to use.
312 *
313 * @param widgetName - The name of the widget factory to use. 'default' will use the default widget.
314 *
315 * @returns The found widget, or `undefined`.
316 *
317 * #### Notes
318 * This can be used to find an existing widget instead of opening
319 * a new widget.
320 */
321 findWidget(path, widgetName = 'default') {
322 const newPath = PathExt.normalize(path);
323 let widgetNames = [widgetName];
324 if (widgetName === 'default') {
325 const factory = this.registry.defaultWidgetFactory(newPath);
326 if (!factory) {
327 return undefined;
328 }
329 widgetNames = [factory.name];
330 }
331 else if (widgetName === null) {
332 widgetNames = this.registry
333 .preferredWidgetFactories(newPath)
334 .map(f => f.name);
335 }
336 for (const context of this._contextsForPath(newPath)) {
337 for (const widgetName of widgetNames) {
338 if (widgetName !== null) {
339 const widget = this._widgetManager.findWidget(context, widgetName);
340 if (widget) {
341 return widget;
342 }
343 }
344 }
345 }
346 return undefined;
347 }
348 /**
349 * Create a new untitled file.
350 *
351 * @param options - The file content creation options.
352 */
353 newUntitled(options) {
354 if (options.type === 'file') {
355 options.ext = options.ext || '.txt';
356 }
357 return this.services.contents.newUntitled(options);
358 }
359 /**
360 * Open a file and return the widget used to view it.
361 *
362 * @param path - The file path to open.
363 *
364 * @param widgetName - The name of the widget factory to use. 'default' will use the default widget.
365 *
366 * @param kernel - An optional kernel name/id to override the default.
367 *
368 * @returns The created widget, or `undefined`.
369 *
370 * #### Notes
371 * This function will return `undefined` if a valid widget factory
372 * cannot be found.
373 */
374 open(path, widgetName = 'default', kernel, options) {
375 return this._createOrOpenDocument('open', path, widgetName, kernel, options);
376 }
377 /**
378 * Open a file and return the widget used to view it.
379 * Reveals an already existing editor.
380 *
381 * @param path - The file path to open.
382 *
383 * @param widgetName - The name of the widget factory to use. 'default' will use the default widget.
384 *
385 * @param kernel - An optional kernel name/id to override the default.
386 *
387 * @returns The created widget, or `undefined`.
388 *
389 * #### Notes
390 * This function will return `undefined` if a valid widget factory
391 * cannot be found.
392 */
393 openOrReveal(path, widgetName = 'default', kernel, options) {
394 const widget = this.findWidget(path, widgetName);
395 if (widget) {
396 this._opener.open(widget, {
397 type: widgetName,
398 ...options
399 });
400 return widget;
401 }
402 return this.open(path, widgetName, kernel, options !== null && options !== void 0 ? options : {});
403 }
404 /**
405 * Overwrite a file.
406 *
407 * @param oldPath - The full path to the original file.
408 *
409 * @param newPath - The full path to the new file.
410 *
411 * @returns A promise containing the new file contents model.
412 */
413 overwrite(oldPath, newPath) {
414 // Cleanly overwrite the file by moving it, making sure the original does
415 // not exist, and then renaming to the new path.
416 const tempPath = `${newPath}.${UUID.uuid4()}`;
417 const cb = () => this.rename(tempPath, newPath);
418 return this.rename(oldPath, tempPath)
419 .then(() => {
420 return this.deleteFile(newPath);
421 })
422 .then(cb, cb);
423 }
424 /**
425 * Rename a file or directory.
426 *
427 * @param oldPath - The full path to the original file.
428 *
429 * @param newPath - The full path to the new file.
430 *
431 * @returns A promise containing the new file contents model. The promise
432 * will reject if the newPath already exists. Use [[overwrite]] to overwrite
433 * a file.
434 */
435 rename(oldPath, newPath) {
436 return this.services.contents.rename(oldPath, newPath);
437 }
438 /**
439 * Find a context for a given path and factory name.
440 */
441 _findContext(path, factoryName) {
442 const normalizedPath = this.services.contents.normalize(path);
443 return find(this._contexts, context => {
444 return (context.path === normalizedPath && context.factoryName === factoryName);
445 });
446 }
447 /**
448 * Get the contexts for a given path.
449 *
450 * #### Notes
451 * There may be more than one context for a given path if the path is open
452 * with multiple model factories (for example, a notebook can be open with a
453 * notebook model factory and a text model factory).
454 */
455 _contextsForPath(path) {
456 const normalizedPath = this.services.contents.normalize(path);
457 return this._contexts.filter(context => context.path === normalizedPath);
458 }
459 /**
460 * Create a context from a path and a model factory.
461 */
462 _createContext(path, factory, kernelPreference) {
463 // TODO: Make it impossible to open two different contexts for the same
464 // path. Or at least prompt the closing of all widgets associated with the
465 // old context before opening the new context. This will make things much
466 // more consistent for the users, at the cost of some confusion about what
467 // models are and why sometimes they cannot open the same file in different
468 // widgets that have different models.
469 // Allow options to be passed when adding a sibling.
470 const adopter = (widget, options) => {
471 this._widgetManager.adoptWidget(context, widget);
472 // TODO should we pass the type for layout customization
473 this._opener.open(widget, options);
474 };
475 const context = new Context({
476 opener: adopter,
477 manager: this.services,
478 factory,
479 path,
480 kernelPreference,
481 setBusy: this._setBusy,
482 sessionDialogs: this._dialogs,
483 lastModifiedCheckMargin: this._lastModifiedCheckMargin,
484 translator: this.translator
485 });
486 const handler = new SaveHandler({
487 context,
488 isConnectedCallback: this._isConnectedCallback,
489 saveInterval: this.autosaveInterval
490 });
491 Private.saveHandlerProperty.set(context, handler);
492 void context.ready.then(() => {
493 if (this.autosave) {
494 handler.start();
495 }
496 });
497 context.disposed.connect(this._onContextDisposed, this);
498 this._contexts.push(context);
499 return context;
500 }
501 /**
502 * Handle a context disposal.
503 */
504 _onContextDisposed(context) {
505 ArrayExt.removeFirstOf(this._contexts, context);
506 }
507 /**
508 * Get the widget factory for a given widget name.
509 */
510 _widgetFactoryFor(path, widgetName) {
511 const { registry } = this;
512 if (widgetName === 'default') {
513 const factory = registry.defaultWidgetFactory(path);
514 if (!factory) {
515 return undefined;
516 }
517 widgetName = factory.name;
518 }
519 return registry.getWidgetFactory(widgetName);
520 }
521 /**
522 * Creates a new document, or loads one from disk, depending on the `which` argument.
523 * If `which==='create'`, then it creates a new document. If `which==='open'`,
524 * then it loads the document from disk.
525 *
526 * The two cases differ in how the document context is handled, but the creation
527 * of the widget and launching of the kernel are identical.
528 */
529 _createOrOpenDocument(which, path, widgetName = 'default', kernel, options) {
530 const widgetFactory = this._widgetFactoryFor(path, widgetName);
531 if (!widgetFactory) {
532 return undefined;
533 }
534 const modelName = widgetFactory.modelName || 'text';
535 const factory = this.registry.getModelFactory(modelName);
536 if (!factory) {
537 return undefined;
538 }
539 // Handle the kernel preference.
540 const preference = this.registry.getKernelPreference(path, widgetFactory.name, kernel);
541 let context;
542 let ready = Promise.resolve(undefined);
543 // Handle the load-from-disk case
544 if (which === 'open') {
545 // Use an existing context if available.
546 context = this._findContext(path, factory.name) || null;
547 if (!context) {
548 context = this._createContext(path, factory, preference);
549 // Populate the model, either from disk or a
550 // model backend.
551 ready = this._when.then(() => context.initialize(false));
552 }
553 }
554 else if (which === 'create') {
555 context = this._createContext(path, factory, preference);
556 // Immediately save the contents to disk.
557 ready = this._when.then(() => context.initialize(true));
558 }
559 else {
560 throw new Error(`Invalid argument 'which': ${which}`);
561 }
562 const widget = this._widgetManager.createWidget(widgetFactory, context);
563 this._opener.open(widget, { type: widgetFactory.name, ...options });
564 // If the initial opening of the context fails, dispose of the widget.
565 ready.catch(err => {
566 console.error(`Failed to initialize the context with '${factory.name}' for ${path}`, err);
567 widget.close();
568 });
569 return widget;
570 }
571 /**
572 * Handle an activateRequested signal from the widget manager.
573 */
574 _onActivateRequested(sender, args) {
575 this._activateRequested.emit(args);
576 }
577 _onWidgetStateChanged(sender, args) {
578 if (args.name === 'confirmClosingDocument') {
579 this._stateChanged.emit(args);
580 }
581 }
582}
583/**
584 * A namespace for private data.
585 */
586var Private;
587(function (Private) {
588 /**
589 * An attached property for a context save handler.
590 */
591 Private.saveHandlerProperty = new AttachedProperty({
592 name: 'saveHandler',
593 create: () => undefined
594 });
595})(Private || (Private = {}));
596//# sourceMappingURL=manager.js.map
\No newline at end of file