UNPKG

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