UNPKG

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