UNPKG

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