@@ -41,6 +41,12 @@ function isJSONProperty(obj: unknown): obj is IJSONProperty {
41
41
) ;
42
42
}
43
43
44
+ /**
45
+ * Settings keyed by language server name with values including
46
+ * multiple properties, such as priority or workspace configuration
47
+ */
48
+ type LanguageServerSettings = Record < string , ServerSchemaWrapper > ;
49
+
44
50
/**
45
51
* Get default values from JSON Schema properties field.
46
52
*/
@@ -62,7 +68,26 @@ function getDefaults(
62
68
return defaults ;
63
69
}
64
70
65
- let validationAttempt = 0 ;
71
+ /**
72
+ * Schema and user data that for validation
73
+ */
74
+ interface IValidationData {
75
+ rawUserSettings : string ;
76
+ schema : ISettingRegistry . ISchema ;
77
+ }
78
+
79
+ /**
80
+ * Conflicts encounteredn when dot-collapsing settings
81
+ * organised by server ID, and then as a mapping between
82
+ * (dotted) setting ID and list of encoutnered values.
83
+ * The last encountered values is preferred for use.
84
+ */
85
+ type SettingsMergeConflicts = Record < string , Record < string , any [ ] > > ;
86
+
87
+ interface ISettingsCollapseResult {
88
+ settings : LanguageServerSettings ;
89
+ conflicts : SettingsMergeConflicts ;
90
+ }
66
91
67
92
export class SettingsUIManager {
68
93
constructor (
@@ -91,6 +116,10 @@ export class SettingsUIManager {
91
116
}
92
117
) ;
93
118
}
119
+ this . _validationAttempt = 0 ;
120
+ this . _lastValidation = null ;
121
+ this . _lastUserServerSettings = null ;
122
+ this . _lastUserServerSettingsDoted = null ;
94
123
}
95
124
96
125
protected get console ( ) : ILSPLogConsole {
@@ -205,43 +234,56 @@ export class SettingsUIManager {
205
234
schema . properties ! . language_servers . properties = knownServersConfig ;
206
235
schema . properties ! . language_servers . default = defaults ;
207
236
208
- // test if we can apply the schema without causing validation error
209
- // (is the configuration held by the user compatible with the schema?)
210
- validationAttempt += 1 ;
211
- // the validator will parse raw plugin data into this object;
212
- // we do not do anything with those right now.
213
- const parsedData = { composite : { } , user : { } } ;
214
- const validationErrors =
215
- this . options . settingRegistry . validator . validateData (
216
- {
217
- // the plugin schema is cached so we have to provide a dummy ID.
218
- id : `lsp-validation-attempt-${ validationAttempt } ` ,
219
- raw : plugin . raw ,
220
- data : parsedData ,
221
- version : plugin . version ,
222
- schema : schema
223
- } ,
224
- true
225
- ) ;
226
-
227
- if ( validationErrors ) {
228
- console . error (
229
- 'LSP server settings validation failed; configuration graphical interface will run in schema-free mode; errors:' ,
230
- validationErrors
231
- ) ;
232
- this . _validationErrors = validationErrors ;
233
- if ( ! original ) {
237
+ const lastValidation = this . _lastValidation ;
238
+ // do not re-validate if neither schema, nor user settings changed
239
+ if (
240
+ lastValidation === null ||
241
+ lastValidation . rawUserSettings !== plugin . raw ||
242
+ ! JSONExt . deepEqual ( lastValidation . schema , schema )
243
+ ) {
244
+ // test if we can apply the schema without causing validation error
245
+ // (is the configuration held by the user compatible with the schema?)
246
+ this . _validationAttempt += 1 ;
247
+ // the validator will parse raw plugin data into this object;
248
+ // we do not do anything with those right now.
249
+ const parsedData = { composite : { } , user : { } } ;
250
+ const validationErrors =
251
+ this . options . settingRegistry . validator . validateData (
252
+ {
253
+ // the plugin schema is cached so we have to provide a dummy ID.
254
+ id : `lsp-validation-attempt-${ this . _validationAttempt } ` ,
255
+ raw : plugin . raw ,
256
+ data : parsedData ,
257
+ version : plugin . version ,
258
+ schema : schema
259
+ } ,
260
+ true
261
+ ) ;
262
+
263
+ if ( validationErrors ) {
234
264
console . error (
235
- 'Original language servers schema not available to restore non-transformed values.'
265
+ 'LSP server settings validation failed; graphical interface for settings will run in schema-free mode; errors:' ,
266
+ validationErrors
236
267
) ;
237
- } else {
238
- if ( ! original . properties ! . language_servers . properties ) {
239
- delete schema . properties ! . language_servers . properties ;
240
- }
241
- if ( ! original . properties ! . language_servers . default ) {
242
- delete schema . properties ! . language_servers . default ;
268
+ this . _validationErrors = validationErrors ;
269
+ if ( ! original ) {
270
+ console . error (
271
+ 'Original language servers schema not available to restore non-transformed values.'
272
+ ) ;
273
+ } else {
274
+ if ( ! original . properties ! . language_servers . properties ) {
275
+ delete schema . properties ! . language_servers . properties ;
276
+ }
277
+ if ( ! original . properties ! . language_servers . default ) {
278
+ delete schema . properties ! . language_servers . default ;
279
+ }
243
280
}
244
281
}
282
+
283
+ this . _lastValidation = {
284
+ rawUserSettings : plugin . raw ,
285
+ schema : schema
286
+ } ;
245
287
}
246
288
247
289
this . _defaults = defaults ;
@@ -250,6 +292,15 @@ export class SettingsUIManager {
250
292
// Transform the plugin object to return different schema than the default.
251
293
this . options . settingRegistry . transform ( pluginId , {
252
294
fetch : plugin => {
295
+ // Profiling data:
296
+ // Initial fetch: 61-64 ms
297
+ // Subsequent without change: <1ms
298
+ // Session change: 642 ms.
299
+ // 91% spent on `validateData()`
300
+ // 10% in addSchema().
301
+ // 1.8% spent on `deepCopy()`
302
+ // 1.79% spend on other tasks in `populate()`
303
+
253
304
if ( ! original ) {
254
305
original = JSONExt . deepCopy ( plugin . schema ) ;
255
306
}
@@ -258,6 +309,7 @@ export class SettingsUIManager {
258
309
canonical = JSONExt . deepCopy ( plugin . schema ) ;
259
310
populate ( plugin , canonical ) ;
260
311
}
312
+
261
313
return {
262
314
data : plugin . data ,
263
315
id : plugin . id ,
@@ -267,12 +319,39 @@ export class SettingsUIManager {
267
319
} ;
268
320
} ,
269
321
compose : plugin => {
322
+ // Initial compose: 28 ms
323
+ // Consecutive compose with cached settings: 1-2ms
324
+
270
325
const user = plugin . data . user as Required < LanguageServer > ;
271
326
const composite = JSONExt . deepCopy ( user ) ;
272
327
273
- user . language_servers = this . _collapseServerSettingsDotted (
274
- user . language_servers
275
- ) ;
328
+ // Cache collapsed settings for speed and to only show dialog once.
329
+ // Note that JupyterLab attempts to transform in "preload" step (before splash screen end)
330
+ // and then again for deferred extensions if the initial transform in preload timed out.
331
+ // We are hitting the timeout in preload step.
332
+ if (
333
+ this . _lastUserServerSettings === null ||
334
+ this . _lastUserServerSettingsDoted === null ||
335
+ ! JSONExt . deepEqual (
336
+ this . _lastUserServerSettings ,
337
+ user . language_servers
338
+ )
339
+ ) {
340
+ this . _lastUserServerSettings = user . language_servers ;
341
+ const collapsed = this . _collapseServerSettingsDotted (
342
+ user . language_servers
343
+ ) ;
344
+ user . language_servers = this . _collapseServerSettingsDotted (
345
+ user . language_servers
346
+ ) . settings ;
347
+ this . _lastUserServerSettingsDoted = user . language_servers ;
348
+
349
+ if ( Object . keys ( collapsed . conflicts ) . length > 0 ) {
350
+ this . _warnConflicts ( collapsed . conflicts ) . catch ( this . console . warn ) ;
351
+ }
352
+ } else {
353
+ user . language_servers = this . _lastUserServerSettingsDoted ;
354
+ }
276
355
composite . language_servers = user . language_servers ;
277
356
278
357
// Currently disabled, as it does not provide an obvious benefit:
@@ -288,6 +367,7 @@ export class SettingsUIManager {
288
367
user : user ,
289
368
composite : composite
290
369
} ;
370
+
291
371
return plugin ;
292
372
}
293
373
} ) ;
@@ -300,14 +380,21 @@ export class SettingsUIManager {
300
380
} ) ;
301
381
}
302
382
303
- protected _collapseServerSettingsDotted (
304
- settings : Record < string , ServerSchemaWrapper | null >
305
- ) : ServerSchemaWrapper {
383
+ private async _warnConflicts ( conflicts : SettingsMergeConflicts ) {
384
+ showDialog ( {
385
+ body : renderCollapseConflicts ( {
386
+ conflicts : conflicts ,
387
+ trans : this . options . trans
388
+ } ) ,
389
+ buttons : [ Dialog . okButton ( ) ]
390
+ } ) . catch ( console . warn ) ;
391
+ }
392
+
393
+ private _collapseServerSettingsDotted (
394
+ settings : LanguageServerSettings
395
+ ) : ISettingsCollapseResult {
306
396
const conflicts : Record < string , Record < string , any [ ] > > = { } ;
307
- const result = JSONExt . deepCopy ( settings ) as Record <
308
- string ,
309
- ServerSchemaWrapper
310
- > ;
397
+ const result = JSONExt . deepCopy ( settings ) as LanguageServerSettings ;
311
398
for ( let [ serverKey , serverSettingsGroup ] of Object . entries ( settings ) ) {
312
399
if ( ! serverSettingsGroup || ! serverSettingsGroup . serverSettings ) {
313
400
continue ;
@@ -316,27 +403,18 @@ export class SettingsUIManager {
316
403
serverSettingsGroup . serverSettings as ReadonlyJSONObject
317
404
) ;
318
405
conflicts [ serverKey ] = collapsed . conflicts ;
319
- result [ serverKey ] . serverSettings = collapsed . result ;
320
- }
321
- if ( Object . keys ( conflicts ) . length > 0 ) {
322
- showDialog ( {
323
- body : renderCollapseConflicts ( {
324
- conflicts : conflicts ,
325
- trans : this . options . trans
326
- } ) ,
327
- buttons : [ Dialog . okButton ( ) ]
328
- } ) . catch ( console . warn ) ;
406
+ result [ serverKey ] ! . serverSettings = collapsed . result ;
329
407
}
330
- return result ;
408
+ return {
409
+ settings : result ,
410
+ conflicts : conflicts
411
+ } ;
331
412
}
332
413
333
414
protected _filterOutDefaults (
334
- settings : Record < string , ServerSchemaWrapper | null >
335
- ) : ServerSchemaWrapper {
336
- const result = JSONExt . deepCopy ( settings ) as Record <
337
- string ,
338
- ServerSchemaWrapper
339
- > ;
415
+ settings : LanguageServerSettings
416
+ ) : LanguageServerSettings {
417
+ const result = JSONExt . deepCopy ( settings ) as LanguageServerSettings ;
340
418
for ( let [ serverKey , serverSettingsGroup ] of Object . entries ( result ) ) {
341
419
const serverDefaults = this . _defaults [ serverKey ] ;
342
420
if ( serverDefaults == null ) {
@@ -357,12 +435,12 @@ export class SettingsUIManager {
357
435
settingValue as ReadonlyJSONObject
358
436
) ) {
359
437
if ( JSONExt . deepEqual ( subValue as any , settingDefault [ subKey ] ) ) {
360
- delete result [ serverKey ] [ settingKey ] ! [ subKey ] ;
438
+ delete result [ serverKey ] ! [ settingKey ] ! [ subKey ] ;
361
439
}
362
440
}
363
441
} else {
364
442
if ( JSONExt . deepEqual ( settingValue as any , settingDefault ) ) {
365
- delete result [ serverKey ] [ settingKey ] ;
443
+ delete result [ serverKey ] ! [ settingKey ] ;
366
444
}
367
445
}
368
446
}
@@ -372,4 +450,8 @@ export class SettingsUIManager {
372
450
373
451
private _defaults : Record < string , any > ;
374
452
private _validationErrors : ISchemaValidator . IError [ ] ;
453
+ private _validationAttempt : number ;
454
+ private _lastValidation : IValidationData | null ;
455
+ private _lastUserServerSettings : LanguageServerSettings | null ;
456
+ private _lastUserServerSettingsDoted : LanguageServerSettings | null ;
375
457
}
0 commit comments