@jupyterlab/application 0.19.1-alpha.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +34 -0
- package/lib/index.d.ts +241 -241
- package/lib/index.js +283 -283
- package/lib/layoutrestorer.d.ts +205 -205
- package/lib/layoutrestorer.js +423 -423
- package/lib/mimerenderers.d.ts +22 -22
- package/lib/mimerenderers.js +137 -134
- package/lib/router.d.ts +204 -204
- package/lib/router.js +145 -145
- package/lib/shell.d.ts +277 -277
- package/lib/shell.js +857 -854
- package/package.json +13 -10
package/lib/layoutrestorer.js
CHANGED
|
@@ -1,423 +1,423 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/*-----------------------------------------------------------------------------
|
|
3
|
-
| Copyright (c) Jupyter Development Team.
|
|
4
|
-
| Distributed under the terms of the Modified BSD License.
|
|
5
|
-
|----------------------------------------------------------------------------*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
const coreutils_1 = require("@phosphor/coreutils");
|
|
8
|
-
const properties_1 = require("@phosphor/properties");
|
|
9
|
-
/* tslint:disable */
|
|
10
|
-
/**
|
|
11
|
-
* The layout restorer token.
|
|
12
|
-
*/
|
|
13
|
-
exports.ILayoutRestorer = new coreutils_1.Token('@jupyterlab/application:ILayoutRestorer');
|
|
14
|
-
/**
|
|
15
|
-
* The state database key for restorer data.
|
|
16
|
-
*/
|
|
17
|
-
const KEY = 'layout-restorer:data';
|
|
18
|
-
/**
|
|
19
|
-
* The default implementation of a layout restorer.
|
|
20
|
-
*
|
|
21
|
-
* #### Notes
|
|
22
|
-
* The lifecycle for state restoration is subtle. The sequence of events is:
|
|
23
|
-
*
|
|
24
|
-
* 1. The layout restorer plugin is instantiated and makes a `fetch` call to
|
|
25
|
-
* the database that stores the layout restoration data. The `fetch` call
|
|
26
|
-
* returns a promise that resolves in step 6, below.
|
|
27
|
-
*
|
|
28
|
-
* 2. Other plugins that care about state restoration require the layout
|
|
29
|
-
* restorer as a dependency.
|
|
30
|
-
*
|
|
31
|
-
* 3. As each load-time plugin initializes (which happens before the lab
|
|
32
|
-
* application has `started`), it instructs the layout restorer whether
|
|
33
|
-
* the restorer ought to `restore` its state by passing in its tracker.
|
|
34
|
-
* Alternatively, a plugin that does not require its own instance tracker
|
|
35
|
-
* (because perhaps it only creates a single widget, like a command palette),
|
|
36
|
-
* can simply `add` its widget along with a persistent unique name to the
|
|
37
|
-
* layout restorer so that its layout state can be restored when the lab
|
|
38
|
-
* application restores.
|
|
39
|
-
*
|
|
40
|
-
* 4. After all the load-time plugins have finished initializing, the lab
|
|
41
|
-
* application `started` promise will resolve. This is the `first`
|
|
42
|
-
* promise that the layout restorer waits for. By this point, all of the
|
|
43
|
-
* plugins that care about restoration will have instructed the layout
|
|
44
|
-
* restorer to `restore` their state.
|
|
45
|
-
*
|
|
46
|
-
* 5. The layout restorer will then instruct each plugin's instance tracker
|
|
47
|
-
* to restore its state and reinstantiate whichever widgets it wants. The
|
|
48
|
-
* tracker returns a promise to the layout restorer that resolves when it
|
|
49
|
-
* has completed restoring the tracked widgets it cares about.
|
|
50
|
-
*
|
|
51
|
-
* 6. As each instance tracker finishes restoring the widget instances it cares
|
|
52
|
-
* about, it resolves the promise that was made to the layout restorer
|
|
53
|
-
* (in step 5). After all of the promises that the restorer is awaiting have
|
|
54
|
-
* resolved, the restorer then resolves the outstanding `fetch` promise
|
|
55
|
-
* (from step 1) and hands off a layout state object to the application
|
|
56
|
-
* shell's `restoreLayout` method for restoration.
|
|
57
|
-
*
|
|
58
|
-
* 7. Once the application shell has finished restoring the layout, the
|
|
59
|
-
* JupyterLab application's `restored` promise is resolved.
|
|
60
|
-
*
|
|
61
|
-
* Of particular note are steps 5 and 6: since state restoration of plugins
|
|
62
|
-
* is accomplished by executing commands, the command that is used to restore
|
|
63
|
-
* the state of each plugin must return a promise that only resolves when the
|
|
64
|
-
* widget has been created and added to the plugin's instance tracker.
|
|
65
|
-
*/
|
|
66
|
-
class LayoutRestorer {
|
|
67
|
-
/**
|
|
68
|
-
* Create a layout restorer.
|
|
69
|
-
*/
|
|
70
|
-
constructor(options) {
|
|
71
|
-
this._firstDone = false;
|
|
72
|
-
this._promisesDone = false;
|
|
73
|
-
this._promises = [];
|
|
74
|
-
this._restored = new coreutils_1.PromiseDelegate();
|
|
75
|
-
this._trackers = new Set();
|
|
76
|
-
this._widgets = new Map();
|
|
77
|
-
this._registry = options.registry;
|
|
78
|
-
this._state = options.state;
|
|
79
|
-
this._first = options.first;
|
|
80
|
-
this._first
|
|
81
|
-
.then(() => {
|
|
82
|
-
this._firstDone = true;
|
|
83
|
-
})
|
|
84
|
-
.then(() => Promise.all(this._promises))
|
|
85
|
-
.then(() => {
|
|
86
|
-
this._promisesDone = true;
|
|
87
|
-
// Release the tracker set.
|
|
88
|
-
this._trackers.clear();
|
|
89
|
-
})
|
|
90
|
-
.then(() => {
|
|
91
|
-
this._restored.resolve(void 0);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* A promise resolved when the layout restorer is ready to receive signals.
|
|
96
|
-
*/
|
|
97
|
-
get restored() {
|
|
98
|
-
return this._restored.promise;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Add a widget to be tracked by the layout restorer.
|
|
102
|
-
*/
|
|
103
|
-
add(widget, name) {
|
|
104
|
-
Private.nameProperty.set(widget, name);
|
|
105
|
-
this._widgets.set(name, widget);
|
|
106
|
-
widget.disposed.connect(this._onWidgetDisposed, this);
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Fetch the layout state for the application.
|
|
110
|
-
*
|
|
111
|
-
* #### Notes
|
|
112
|
-
* Fetching the layout relies on all widget restoration to be complete, so
|
|
113
|
-
* calls to `fetch` are guaranteed to return after restoration is complete.
|
|
114
|
-
*/
|
|
115
|
-
fetch() {
|
|
116
|
-
const blank = {
|
|
117
|
-
fresh: true,
|
|
118
|
-
mainArea: null,
|
|
119
|
-
leftArea: null,
|
|
120
|
-
rightArea: null
|
|
121
|
-
};
|
|
122
|
-
let layout = this._state.fetch(KEY);
|
|
123
|
-
return Promise.all([layout, this.restored])
|
|
124
|
-
.then(([data]) => {
|
|
125
|
-
if (!data) {
|
|
126
|
-
return blank;
|
|
127
|
-
}
|
|
128
|
-
const { main, left, right } = data;
|
|
129
|
-
// If any data exists, then this is not a fresh session.
|
|
130
|
-
const fresh = false;
|
|
131
|
-
// Rehydrate main area.
|
|
132
|
-
const mainArea = this._rehydrateMainArea(main);
|
|
133
|
-
// Rehydrate left area.
|
|
134
|
-
const leftArea = this._rehydrateSideArea(left);
|
|
135
|
-
// Rehydrate right area.
|
|
136
|
-
const rightArea = this._rehydrateSideArea(right);
|
|
137
|
-
return { fresh, mainArea, leftArea, rightArea };
|
|
138
|
-
})
|
|
139
|
-
.catch(() => blank); // Let fetch fail gracefully; return blank slate.
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Restore the widgets of a particular instance tracker.
|
|
143
|
-
*
|
|
144
|
-
* @param tracker - The instance tracker whose widgets will be restored.
|
|
145
|
-
*
|
|
146
|
-
* @param options - The restoration options.
|
|
147
|
-
*/
|
|
148
|
-
restore(tracker, options) {
|
|
149
|
-
const warning = 'restore() can only be called before `first` has resolved.';
|
|
150
|
-
if (this._firstDone) {
|
|
151
|
-
console.warn(warning);
|
|
152
|
-
return Promise.reject(warning);
|
|
153
|
-
}
|
|
154
|
-
const { namespace } = tracker;
|
|
155
|
-
if (this._trackers.has(namespace)) {
|
|
156
|
-
let warning = `A tracker namespaced ${namespace} was already restored.`;
|
|
157
|
-
console.warn(warning);
|
|
158
|
-
return Promise.reject(warning);
|
|
159
|
-
}
|
|
160
|
-
const { args, command, name, when } = options;
|
|
161
|
-
// Add the tracker to the private trackers collection.
|
|
162
|
-
this._trackers.add(namespace);
|
|
163
|
-
// Whenever a new widget is added to the tracker, record its name.
|
|
164
|
-
tracker.widgetAdded.connect((sender, widget) => {
|
|
165
|
-
const widgetName = name(widget);
|
|
166
|
-
if (widgetName) {
|
|
167
|
-
this.add(widget, `${namespace}:${widgetName}`);
|
|
168
|
-
}
|
|
169
|
-
}, this);
|
|
170
|
-
// Whenever a widget is updated, get its new name.
|
|
171
|
-
tracker.widgetUpdated.connect((sender, widget) => {
|
|
172
|
-
const widgetName = name(widget);
|
|
173
|
-
if (widgetName) {
|
|
174
|
-
let name = `${namespace}:${widgetName}`;
|
|
175
|
-
Private.nameProperty.set(widget, name);
|
|
176
|
-
this._widgets.set(name, widget);
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
const first = this._first;
|
|
180
|
-
const promise = tracker
|
|
181
|
-
.restore({
|
|
182
|
-
args,
|
|
183
|
-
command,
|
|
184
|
-
name,
|
|
185
|
-
registry: this._registry,
|
|
186
|
-
state: this._state,
|
|
187
|
-
when: when ? [first].concat(when) : first
|
|
188
|
-
})
|
|
189
|
-
.catch(error => {
|
|
190
|
-
console.error(error);
|
|
191
|
-
});
|
|
192
|
-
this._promises.push(promise);
|
|
193
|
-
return promise;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Save the layout state for the application.
|
|
197
|
-
*/
|
|
198
|
-
save(data) {
|
|
199
|
-
// If there are promises that are unresolved, bail.
|
|
200
|
-
if (!this._promisesDone) {
|
|
201
|
-
let warning = 'save() was called prematurely.';
|
|
202
|
-
console.warn(warning);
|
|
203
|
-
return Promise.reject(warning);
|
|
204
|
-
}
|
|
205
|
-
let dehydrated = {};
|
|
206
|
-
// Dehydrate main area.
|
|
207
|
-
dehydrated.main = this._dehydrateMainArea(data.mainArea);
|
|
208
|
-
// Dehydrate left area.
|
|
209
|
-
dehydrated.left = this._dehydrateSideArea(data.leftArea);
|
|
210
|
-
// Dehydrate right area.
|
|
211
|
-
dehydrated.right = this._dehydrateSideArea(data.rightArea);
|
|
212
|
-
return this._state.save(KEY, dehydrated);
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Dehydrate a main area description into a serializable object.
|
|
216
|
-
*/
|
|
217
|
-
_dehydrateMainArea(area) {
|
|
218
|
-
if (!area) {
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
return Private.serializeMain(area);
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Reydrate a serialized main area description object.
|
|
225
|
-
*
|
|
226
|
-
* #### Notes
|
|
227
|
-
* This function consumes data that can become corrupted, so it uses type
|
|
228
|
-
* coercion to guarantee the dehydrated object is safely processed.
|
|
229
|
-
*/
|
|
230
|
-
_rehydrateMainArea(area) {
|
|
231
|
-
if (!area) {
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
return Private.deserializeMain(area, this._widgets);
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Dehydrate a side area description into a serializable object.
|
|
238
|
-
*/
|
|
239
|
-
_dehydrateSideArea(area) {
|
|
240
|
-
if (!area) {
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
let dehydrated = { collapsed: area.collapsed };
|
|
244
|
-
if (area.currentWidget) {
|
|
245
|
-
let current = Private.nameProperty.get(area.currentWidget);
|
|
246
|
-
if (current) {
|
|
247
|
-
dehydrated.current = current;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (area.widgets) {
|
|
251
|
-
dehydrated.widgets = area.widgets
|
|
252
|
-
.map(widget => Private.nameProperty.get(widget))
|
|
253
|
-
.filter(name => !!name);
|
|
254
|
-
}
|
|
255
|
-
return dehydrated;
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Reydrate a serialized side area description object.
|
|
259
|
-
*
|
|
260
|
-
* #### Notes
|
|
261
|
-
* This function consumes data that can become corrupted, so it uses type
|
|
262
|
-
* coercion to guarantee the dehydrated object is safely processed.
|
|
263
|
-
*/
|
|
264
|
-
_rehydrateSideArea(area) {
|
|
265
|
-
if (!area) {
|
|
266
|
-
return { collapsed: true, currentWidget: null, widgets: null };
|
|
267
|
-
}
|
|
268
|
-
let internal = this._widgets;
|
|
269
|
-
const collapsed = area.hasOwnProperty('collapsed')
|
|
270
|
-
? !!area.collapsed
|
|
271
|
-
: false;
|
|
272
|
-
const currentWidget = area.current && internal.has(`${area.current}`)
|
|
273
|
-
? internal.get(`${area.current}`)
|
|
274
|
-
: null;
|
|
275
|
-
const widgets = !Array.isArray(area.widgets)
|
|
276
|
-
? null
|
|
277
|
-
: area.widgets
|
|
278
|
-
.map(name => (internal.has(`${name}`) ? internal.get(`${name}`) : null))
|
|
279
|
-
.filter(widget => !!widget);
|
|
280
|
-
return {
|
|
281
|
-
collapsed,
|
|
282
|
-
currentWidget: currentWidget,
|
|
283
|
-
widgets: widgets
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Handle a widget disposal.
|
|
288
|
-
*/
|
|
289
|
-
_onWidgetDisposed(widget) {
|
|
290
|
-
let name = Private.nameProperty.get(widget);
|
|
291
|
-
this._widgets.delete(name);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
exports.LayoutRestorer = LayoutRestorer;
|
|
295
|
-
/*
|
|
296
|
-
* A namespace for private data.
|
|
297
|
-
*/
|
|
298
|
-
var Private;
|
|
299
|
-
(function (Private) {
|
|
300
|
-
/**
|
|
301
|
-
* An attached property for a widget's ID in the state database.
|
|
302
|
-
*/
|
|
303
|
-
Private.nameProperty = new properties_1.AttachedProperty({
|
|
304
|
-
name: 'name',
|
|
305
|
-
create: owner => ''
|
|
306
|
-
});
|
|
307
|
-
/**
|
|
308
|
-
* Serialize individual areas within the main area.
|
|
309
|
-
*/
|
|
310
|
-
function serializeArea(area) {
|
|
311
|
-
if (!area || !area.type) {
|
|
312
|
-
return null;
|
|
313
|
-
}
|
|
314
|
-
if (area.type === 'tab-area') {
|
|
315
|
-
return {
|
|
316
|
-
type: 'tab-area',
|
|
317
|
-
currentIndex: area.currentIndex,
|
|
318
|
-
widgets: area.widgets
|
|
319
|
-
.map(widget => Private.nameProperty.get(widget))
|
|
320
|
-
.filter(name => !!name)
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
return {
|
|
324
|
-
type: 'split-area',
|
|
325
|
-
orientation: area.orientation,
|
|
326
|
-
sizes: area.sizes,
|
|
327
|
-
children: area.children.map(serializeArea).filter(area => !!area)
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Return a dehydrated, serializable version of the main dock panel.
|
|
332
|
-
*/
|
|
333
|
-
function serializeMain(area) {
|
|
334
|
-
let dehydrated = {
|
|
335
|
-
dock: (area && area.dock && serializeArea(area.dock.main)) || null
|
|
336
|
-
};
|
|
337
|
-
if (area) {
|
|
338
|
-
dehydrated.mode = area.mode;
|
|
339
|
-
if (area.currentWidget) {
|
|
340
|
-
let current = Private.nameProperty.get(area.currentWidget);
|
|
341
|
-
if (current) {
|
|
342
|
-
dehydrated.current = current;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return dehydrated;
|
|
347
|
-
}
|
|
348
|
-
Private.serializeMain = serializeMain;
|
|
349
|
-
/**
|
|
350
|
-
* Deserialize individual areas within the main area.
|
|
351
|
-
*
|
|
352
|
-
* #### Notes
|
|
353
|
-
* Because this data comes from a potentially unreliable foreign source, it is
|
|
354
|
-
* typed as a `JSONObject`; but the actual expected type is:
|
|
355
|
-
* `ITabArea | ISplitArea`.
|
|
356
|
-
*
|
|
357
|
-
* For fault tolerance, types are manually checked in deserialization.
|
|
358
|
-
*/
|
|
359
|
-
function deserializeArea(area, names) {
|
|
360
|
-
if (!area) {
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
// Because this data is saved to a foreign data source, its type safety is
|
|
364
|
-
// not guaranteed when it is retrieved, so exhaustive checks are necessary.
|
|
365
|
-
const type = area.type || 'unknown';
|
|
366
|
-
if (type === 'unknown' || (type !== 'tab-area' && type !== 'split-area')) {
|
|
367
|
-
console.warn(`Attempted to deserialize unknown type: ${type}`);
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
if (type === 'tab-area') {
|
|
371
|
-
const { currentIndex, widgets } = area;
|
|
372
|
-
let hydrated = {
|
|
373
|
-
type: 'tab-area',
|
|
374
|
-
currentIndex: currentIndex || 0,
|
|
375
|
-
widgets: (widgets &&
|
|
376
|
-
widgets
|
|
377
|
-
.map(widget => names.get(widget))
|
|
378
|
-
.filter(widget => !!widget)) ||
|
|
379
|
-
[]
|
|
380
|
-
};
|
|
381
|
-
// Make sure the current index is within bounds.
|
|
382
|
-
if (hydrated.currentIndex > hydrated.widgets.length - 1) {
|
|
383
|
-
hydrated.currentIndex = 0;
|
|
384
|
-
}
|
|
385
|
-
return hydrated;
|
|
386
|
-
}
|
|
387
|
-
const { orientation, sizes, children } = area;
|
|
388
|
-
let hydrated = {
|
|
389
|
-
type: 'split-area',
|
|
390
|
-
orientation: orientation,
|
|
391
|
-
sizes: sizes || [],
|
|
392
|
-
children: (children &&
|
|
393
|
-
children
|
|
394
|
-
.map(child => deserializeArea(child, names))
|
|
395
|
-
.filter(widget => !!widget)) ||
|
|
396
|
-
[]
|
|
397
|
-
};
|
|
398
|
-
return hydrated;
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Return the hydrated version of the main dock panel, ready to restore.
|
|
402
|
-
*
|
|
403
|
-
* #### Notes
|
|
404
|
-
* Because this data comes from a potentially unreliable foreign source, it is
|
|
405
|
-
* typed as a `JSONObject`; but the actual expected type is: `IMainArea`.
|
|
406
|
-
*
|
|
407
|
-
* For fault tolerance, types are manually checked in deserialization.
|
|
408
|
-
*/
|
|
409
|
-
function deserializeMain(area, names) {
|
|
410
|
-
if (!area) {
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
const name = area.current || null;
|
|
414
|
-
const dock = area.dock || null;
|
|
415
|
-
const mode = area.mode || null;
|
|
416
|
-
return {
|
|
417
|
-
currentWidget: (name && names.has(name) && names.get(name)) || null,
|
|
418
|
-
dock: dock ? { main: deserializeArea(dock, names) } : null,
|
|
419
|
-
mode: mode === 'multiple-document' || mode === 'single-document' ? mode : null
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
Private.deserializeMain = deserializeMain;
|
|
423
|
-
})(Private || (Private = {}));
|
|
1
|
+
"use strict";
|
|
2
|
+
/*-----------------------------------------------------------------------------
|
|
3
|
+
| Copyright (c) Jupyter Development Team.
|
|
4
|
+
| Distributed under the terms of the Modified BSD License.
|
|
5
|
+
|----------------------------------------------------------------------------*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const coreutils_1 = require("@phosphor/coreutils");
|
|
8
|
+
const properties_1 = require("@phosphor/properties");
|
|
9
|
+
/* tslint:disable */
|
|
10
|
+
/**
|
|
11
|
+
* The layout restorer token.
|
|
12
|
+
*/
|
|
13
|
+
exports.ILayoutRestorer = new coreutils_1.Token('@jupyterlab/application:ILayoutRestorer');
|
|
14
|
+
/**
|
|
15
|
+
* The state database key for restorer data.
|
|
16
|
+
*/
|
|
17
|
+
const KEY = 'layout-restorer:data';
|
|
18
|
+
/**
|
|
19
|
+
* The default implementation of a layout restorer.
|
|
20
|
+
*
|
|
21
|
+
* #### Notes
|
|
22
|
+
* The lifecycle for state restoration is subtle. The sequence of events is:
|
|
23
|
+
*
|
|
24
|
+
* 1. The layout restorer plugin is instantiated and makes a `fetch` call to
|
|
25
|
+
* the database that stores the layout restoration data. The `fetch` call
|
|
26
|
+
* returns a promise that resolves in step 6, below.
|
|
27
|
+
*
|
|
28
|
+
* 2. Other plugins that care about state restoration require the layout
|
|
29
|
+
* restorer as a dependency.
|
|
30
|
+
*
|
|
31
|
+
* 3. As each load-time plugin initializes (which happens before the lab
|
|
32
|
+
* application has `started`), it instructs the layout restorer whether
|
|
33
|
+
* the restorer ought to `restore` its state by passing in its tracker.
|
|
34
|
+
* Alternatively, a plugin that does not require its own instance tracker
|
|
35
|
+
* (because perhaps it only creates a single widget, like a command palette),
|
|
36
|
+
* can simply `add` its widget along with a persistent unique name to the
|
|
37
|
+
* layout restorer so that its layout state can be restored when the lab
|
|
38
|
+
* application restores.
|
|
39
|
+
*
|
|
40
|
+
* 4. After all the load-time plugins have finished initializing, the lab
|
|
41
|
+
* application `started` promise will resolve. This is the `first`
|
|
42
|
+
* promise that the layout restorer waits for. By this point, all of the
|
|
43
|
+
* plugins that care about restoration will have instructed the layout
|
|
44
|
+
* restorer to `restore` their state.
|
|
45
|
+
*
|
|
46
|
+
* 5. The layout restorer will then instruct each plugin's instance tracker
|
|
47
|
+
* to restore its state and reinstantiate whichever widgets it wants. The
|
|
48
|
+
* tracker returns a promise to the layout restorer that resolves when it
|
|
49
|
+
* has completed restoring the tracked widgets it cares about.
|
|
50
|
+
*
|
|
51
|
+
* 6. As each instance tracker finishes restoring the widget instances it cares
|
|
52
|
+
* about, it resolves the promise that was made to the layout restorer
|
|
53
|
+
* (in step 5). After all of the promises that the restorer is awaiting have
|
|
54
|
+
* resolved, the restorer then resolves the outstanding `fetch` promise
|
|
55
|
+
* (from step 1) and hands off a layout state object to the application
|
|
56
|
+
* shell's `restoreLayout` method for restoration.
|
|
57
|
+
*
|
|
58
|
+
* 7. Once the application shell has finished restoring the layout, the
|
|
59
|
+
* JupyterLab application's `restored` promise is resolved.
|
|
60
|
+
*
|
|
61
|
+
* Of particular note are steps 5 and 6: since state restoration of plugins
|
|
62
|
+
* is accomplished by executing commands, the command that is used to restore
|
|
63
|
+
* the state of each plugin must return a promise that only resolves when the
|
|
64
|
+
* widget has been created and added to the plugin's instance tracker.
|
|
65
|
+
*/
|
|
66
|
+
class LayoutRestorer {
|
|
67
|
+
/**
|
|
68
|
+
* Create a layout restorer.
|
|
69
|
+
*/
|
|
70
|
+
constructor(options) {
|
|
71
|
+
this._firstDone = false;
|
|
72
|
+
this._promisesDone = false;
|
|
73
|
+
this._promises = [];
|
|
74
|
+
this._restored = new coreutils_1.PromiseDelegate();
|
|
75
|
+
this._trackers = new Set();
|
|
76
|
+
this._widgets = new Map();
|
|
77
|
+
this._registry = options.registry;
|
|
78
|
+
this._state = options.state;
|
|
79
|
+
this._first = options.first;
|
|
80
|
+
this._first
|
|
81
|
+
.then(() => {
|
|
82
|
+
this._firstDone = true;
|
|
83
|
+
})
|
|
84
|
+
.then(() => Promise.all(this._promises))
|
|
85
|
+
.then(() => {
|
|
86
|
+
this._promisesDone = true;
|
|
87
|
+
// Release the tracker set.
|
|
88
|
+
this._trackers.clear();
|
|
89
|
+
})
|
|
90
|
+
.then(() => {
|
|
91
|
+
this._restored.resolve(void 0);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A promise resolved when the layout restorer is ready to receive signals.
|
|
96
|
+
*/
|
|
97
|
+
get restored() {
|
|
98
|
+
return this._restored.promise;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Add a widget to be tracked by the layout restorer.
|
|
102
|
+
*/
|
|
103
|
+
add(widget, name) {
|
|
104
|
+
Private.nameProperty.set(widget, name);
|
|
105
|
+
this._widgets.set(name, widget);
|
|
106
|
+
widget.disposed.connect(this._onWidgetDisposed, this);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Fetch the layout state for the application.
|
|
110
|
+
*
|
|
111
|
+
* #### Notes
|
|
112
|
+
* Fetching the layout relies on all widget restoration to be complete, so
|
|
113
|
+
* calls to `fetch` are guaranteed to return after restoration is complete.
|
|
114
|
+
*/
|
|
115
|
+
fetch() {
|
|
116
|
+
const blank = {
|
|
117
|
+
fresh: true,
|
|
118
|
+
mainArea: null,
|
|
119
|
+
leftArea: null,
|
|
120
|
+
rightArea: null
|
|
121
|
+
};
|
|
122
|
+
let layout = this._state.fetch(KEY);
|
|
123
|
+
return Promise.all([layout, this.restored])
|
|
124
|
+
.then(([data]) => {
|
|
125
|
+
if (!data) {
|
|
126
|
+
return blank;
|
|
127
|
+
}
|
|
128
|
+
const { main, left, right } = data;
|
|
129
|
+
// If any data exists, then this is not a fresh session.
|
|
130
|
+
const fresh = false;
|
|
131
|
+
// Rehydrate main area.
|
|
132
|
+
const mainArea = this._rehydrateMainArea(main);
|
|
133
|
+
// Rehydrate left area.
|
|
134
|
+
const leftArea = this._rehydrateSideArea(left);
|
|
135
|
+
// Rehydrate right area.
|
|
136
|
+
const rightArea = this._rehydrateSideArea(right);
|
|
137
|
+
return { fresh, mainArea, leftArea, rightArea };
|
|
138
|
+
})
|
|
139
|
+
.catch(() => blank); // Let fetch fail gracefully; return blank slate.
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Restore the widgets of a particular instance tracker.
|
|
143
|
+
*
|
|
144
|
+
* @param tracker - The instance tracker whose widgets will be restored.
|
|
145
|
+
*
|
|
146
|
+
* @param options - The restoration options.
|
|
147
|
+
*/
|
|
148
|
+
restore(tracker, options) {
|
|
149
|
+
const warning = 'restore() can only be called before `first` has resolved.';
|
|
150
|
+
if (this._firstDone) {
|
|
151
|
+
console.warn(warning);
|
|
152
|
+
return Promise.reject(warning);
|
|
153
|
+
}
|
|
154
|
+
const { namespace } = tracker;
|
|
155
|
+
if (this._trackers.has(namespace)) {
|
|
156
|
+
let warning = `A tracker namespaced ${namespace} was already restored.`;
|
|
157
|
+
console.warn(warning);
|
|
158
|
+
return Promise.reject(warning);
|
|
159
|
+
}
|
|
160
|
+
const { args, command, name, when } = options;
|
|
161
|
+
// Add the tracker to the private trackers collection.
|
|
162
|
+
this._trackers.add(namespace);
|
|
163
|
+
// Whenever a new widget is added to the tracker, record its name.
|
|
164
|
+
tracker.widgetAdded.connect((sender, widget) => {
|
|
165
|
+
const widgetName = name(widget);
|
|
166
|
+
if (widgetName) {
|
|
167
|
+
this.add(widget, `${namespace}:${widgetName}`);
|
|
168
|
+
}
|
|
169
|
+
}, this);
|
|
170
|
+
// Whenever a widget is updated, get its new name.
|
|
171
|
+
tracker.widgetUpdated.connect((sender, widget) => {
|
|
172
|
+
const widgetName = name(widget);
|
|
173
|
+
if (widgetName) {
|
|
174
|
+
let name = `${namespace}:${widgetName}`;
|
|
175
|
+
Private.nameProperty.set(widget, name);
|
|
176
|
+
this._widgets.set(name, widget);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
const first = this._first;
|
|
180
|
+
const promise = tracker
|
|
181
|
+
.restore({
|
|
182
|
+
args,
|
|
183
|
+
command,
|
|
184
|
+
name,
|
|
185
|
+
registry: this._registry,
|
|
186
|
+
state: this._state,
|
|
187
|
+
when: when ? [first].concat(when) : first
|
|
188
|
+
})
|
|
189
|
+
.catch(error => {
|
|
190
|
+
console.error(error);
|
|
191
|
+
});
|
|
192
|
+
this._promises.push(promise);
|
|
193
|
+
return promise;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Save the layout state for the application.
|
|
197
|
+
*/
|
|
198
|
+
save(data) {
|
|
199
|
+
// If there are promises that are unresolved, bail.
|
|
200
|
+
if (!this._promisesDone) {
|
|
201
|
+
let warning = 'save() was called prematurely.';
|
|
202
|
+
console.warn(warning);
|
|
203
|
+
return Promise.reject(warning);
|
|
204
|
+
}
|
|
205
|
+
let dehydrated = {};
|
|
206
|
+
// Dehydrate main area.
|
|
207
|
+
dehydrated.main = this._dehydrateMainArea(data.mainArea);
|
|
208
|
+
// Dehydrate left area.
|
|
209
|
+
dehydrated.left = this._dehydrateSideArea(data.leftArea);
|
|
210
|
+
// Dehydrate right area.
|
|
211
|
+
dehydrated.right = this._dehydrateSideArea(data.rightArea);
|
|
212
|
+
return this._state.save(KEY, dehydrated);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Dehydrate a main area description into a serializable object.
|
|
216
|
+
*/
|
|
217
|
+
_dehydrateMainArea(area) {
|
|
218
|
+
if (!area) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return Private.serializeMain(area);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Reydrate a serialized main area description object.
|
|
225
|
+
*
|
|
226
|
+
* #### Notes
|
|
227
|
+
* This function consumes data that can become corrupted, so it uses type
|
|
228
|
+
* coercion to guarantee the dehydrated object is safely processed.
|
|
229
|
+
*/
|
|
230
|
+
_rehydrateMainArea(area) {
|
|
231
|
+
if (!area) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
return Private.deserializeMain(area, this._widgets);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Dehydrate a side area description into a serializable object.
|
|
238
|
+
*/
|
|
239
|
+
_dehydrateSideArea(area) {
|
|
240
|
+
if (!area) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
let dehydrated = { collapsed: area.collapsed };
|
|
244
|
+
if (area.currentWidget) {
|
|
245
|
+
let current = Private.nameProperty.get(area.currentWidget);
|
|
246
|
+
if (current) {
|
|
247
|
+
dehydrated.current = current;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (area.widgets) {
|
|
251
|
+
dehydrated.widgets = area.widgets
|
|
252
|
+
.map(widget => Private.nameProperty.get(widget))
|
|
253
|
+
.filter(name => !!name);
|
|
254
|
+
}
|
|
255
|
+
return dehydrated;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Reydrate a serialized side area description object.
|
|
259
|
+
*
|
|
260
|
+
* #### Notes
|
|
261
|
+
* This function consumes data that can become corrupted, so it uses type
|
|
262
|
+
* coercion to guarantee the dehydrated object is safely processed.
|
|
263
|
+
*/
|
|
264
|
+
_rehydrateSideArea(area) {
|
|
265
|
+
if (!area) {
|
|
266
|
+
return { collapsed: true, currentWidget: null, widgets: null };
|
|
267
|
+
}
|
|
268
|
+
let internal = this._widgets;
|
|
269
|
+
const collapsed = area.hasOwnProperty('collapsed')
|
|
270
|
+
? !!area.collapsed
|
|
271
|
+
: false;
|
|
272
|
+
const currentWidget = area.current && internal.has(`${area.current}`)
|
|
273
|
+
? internal.get(`${area.current}`)
|
|
274
|
+
: null;
|
|
275
|
+
const widgets = !Array.isArray(area.widgets)
|
|
276
|
+
? null
|
|
277
|
+
: area.widgets
|
|
278
|
+
.map(name => (internal.has(`${name}`) ? internal.get(`${name}`) : null))
|
|
279
|
+
.filter(widget => !!widget);
|
|
280
|
+
return {
|
|
281
|
+
collapsed,
|
|
282
|
+
currentWidget: currentWidget,
|
|
283
|
+
widgets: widgets
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Handle a widget disposal.
|
|
288
|
+
*/
|
|
289
|
+
_onWidgetDisposed(widget) {
|
|
290
|
+
let name = Private.nameProperty.get(widget);
|
|
291
|
+
this._widgets.delete(name);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
exports.LayoutRestorer = LayoutRestorer;
|
|
295
|
+
/*
|
|
296
|
+
* A namespace for private data.
|
|
297
|
+
*/
|
|
298
|
+
var Private;
|
|
299
|
+
(function (Private) {
|
|
300
|
+
/**
|
|
301
|
+
* An attached property for a widget's ID in the state database.
|
|
302
|
+
*/
|
|
303
|
+
Private.nameProperty = new properties_1.AttachedProperty({
|
|
304
|
+
name: 'name',
|
|
305
|
+
create: owner => ''
|
|
306
|
+
});
|
|
307
|
+
/**
|
|
308
|
+
* Serialize individual areas within the main area.
|
|
309
|
+
*/
|
|
310
|
+
function serializeArea(area) {
|
|
311
|
+
if (!area || !area.type) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
if (area.type === 'tab-area') {
|
|
315
|
+
return {
|
|
316
|
+
type: 'tab-area',
|
|
317
|
+
currentIndex: area.currentIndex,
|
|
318
|
+
widgets: area.widgets
|
|
319
|
+
.map(widget => Private.nameProperty.get(widget))
|
|
320
|
+
.filter(name => !!name)
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
type: 'split-area',
|
|
325
|
+
orientation: area.orientation,
|
|
326
|
+
sizes: area.sizes,
|
|
327
|
+
children: area.children.map(serializeArea).filter(area => !!area)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Return a dehydrated, serializable version of the main dock panel.
|
|
332
|
+
*/
|
|
333
|
+
function serializeMain(area) {
|
|
334
|
+
let dehydrated = {
|
|
335
|
+
dock: (area && area.dock && serializeArea(area.dock.main)) || null
|
|
336
|
+
};
|
|
337
|
+
if (area) {
|
|
338
|
+
dehydrated.mode = area.mode;
|
|
339
|
+
if (area.currentWidget) {
|
|
340
|
+
let current = Private.nameProperty.get(area.currentWidget);
|
|
341
|
+
if (current) {
|
|
342
|
+
dehydrated.current = current;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return dehydrated;
|
|
347
|
+
}
|
|
348
|
+
Private.serializeMain = serializeMain;
|
|
349
|
+
/**
|
|
350
|
+
* Deserialize individual areas within the main area.
|
|
351
|
+
*
|
|
352
|
+
* #### Notes
|
|
353
|
+
* Because this data comes from a potentially unreliable foreign source, it is
|
|
354
|
+
* typed as a `JSONObject`; but the actual expected type is:
|
|
355
|
+
* `ITabArea | ISplitArea`.
|
|
356
|
+
*
|
|
357
|
+
* For fault tolerance, types are manually checked in deserialization.
|
|
358
|
+
*/
|
|
359
|
+
function deserializeArea(area, names) {
|
|
360
|
+
if (!area) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
// Because this data is saved to a foreign data source, its type safety is
|
|
364
|
+
// not guaranteed when it is retrieved, so exhaustive checks are necessary.
|
|
365
|
+
const type = area.type || 'unknown';
|
|
366
|
+
if (type === 'unknown' || (type !== 'tab-area' && type !== 'split-area')) {
|
|
367
|
+
console.warn(`Attempted to deserialize unknown type: ${type}`);
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
if (type === 'tab-area') {
|
|
371
|
+
const { currentIndex, widgets } = area;
|
|
372
|
+
let hydrated = {
|
|
373
|
+
type: 'tab-area',
|
|
374
|
+
currentIndex: currentIndex || 0,
|
|
375
|
+
widgets: (widgets &&
|
|
376
|
+
widgets
|
|
377
|
+
.map(widget => names.get(widget))
|
|
378
|
+
.filter(widget => !!widget)) ||
|
|
379
|
+
[]
|
|
380
|
+
};
|
|
381
|
+
// Make sure the current index is within bounds.
|
|
382
|
+
if (hydrated.currentIndex > hydrated.widgets.length - 1) {
|
|
383
|
+
hydrated.currentIndex = 0;
|
|
384
|
+
}
|
|
385
|
+
return hydrated;
|
|
386
|
+
}
|
|
387
|
+
const { orientation, sizes, children } = area;
|
|
388
|
+
let hydrated = {
|
|
389
|
+
type: 'split-area',
|
|
390
|
+
orientation: orientation,
|
|
391
|
+
sizes: sizes || [],
|
|
392
|
+
children: (children &&
|
|
393
|
+
children
|
|
394
|
+
.map(child => deserializeArea(child, names))
|
|
395
|
+
.filter(widget => !!widget)) ||
|
|
396
|
+
[]
|
|
397
|
+
};
|
|
398
|
+
return hydrated;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Return the hydrated version of the main dock panel, ready to restore.
|
|
402
|
+
*
|
|
403
|
+
* #### Notes
|
|
404
|
+
* Because this data comes from a potentially unreliable foreign source, it is
|
|
405
|
+
* typed as a `JSONObject`; but the actual expected type is: `IMainArea`.
|
|
406
|
+
*
|
|
407
|
+
* For fault tolerance, types are manually checked in deserialization.
|
|
408
|
+
*/
|
|
409
|
+
function deserializeMain(area, names) {
|
|
410
|
+
if (!area) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const name = area.current || null;
|
|
414
|
+
const dock = area.dock || null;
|
|
415
|
+
const mode = area.mode || null;
|
|
416
|
+
return {
|
|
417
|
+
currentWidget: (name && names.has(name) && names.get(name)) || null,
|
|
418
|
+
dock: dock ? { main: deserializeArea(dock, names) } : null,
|
|
419
|
+
mode: mode === 'multiple-document' || mode === 'single-document' ? mode : null
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
Private.deserializeMain = deserializeMain;
|
|
423
|
+
})(Private || (Private = {}));
|