Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/preferences/browser/settingsTree.ts
5251 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { BrowserFeatures } from '../../../../base/browser/canIUse.js';
7
import * as DOM from '../../../../base/browser/dom.js';
8
import * as domStylesheetsJs from '../../../../base/browser/domStylesheets.js';
9
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
10
import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js';
11
import { IMouseEvent } from '../../../../base/browser/mouseEvent.js';
12
import * as aria from '../../../../base/browser/ui/aria/aria.js';
13
import { Button } from '../../../../base/browser/ui/button/button.js';
14
import { SimpleIconLabel } from '../../../../base/browser/ui/iconLabel/simpleIconLabel.js';
15
import { IInputOptions, InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';
16
import { CachedListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
17
import { DefaultStyleController, IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';
18
import { ISelectOptionItem, SelectBox } from '../../../../base/browser/ui/selectBox/selectBox.js';
19
import { Toggle, unthemedToggleStyles } from '../../../../base/browser/ui/toggle/toggle.js';
20
import { ToolBar } from '../../../../base/browser/ui/toolbar/toolbar.js';
21
import { RenderIndentGuides } from '../../../../base/browser/ui/tree/abstractTree.js';
22
import { IObjectTreeOptions } from '../../../../base/browser/ui/tree/objectTree.js';
23
import { ObjectTreeModel } from '../../../../base/browser/ui/tree/objectTreeModel.js';
24
import { ITreeFilter, ITreeModel, ITreeNode, ITreeRenderer, TreeFilterResult, TreeVisibility } from '../../../../base/browser/ui/tree/tree.js';
25
import { Action, IAction, Separator } from '../../../../base/common/actions.js';
26
import { distinct } from '../../../../base/common/arrays.js';
27
import { Codicon } from '../../../../base/common/codicons.js';
28
import { onUnexpectedError } from '../../../../base/common/errors.js';
29
import { Emitter, Event } from '../../../../base/common/event.js';
30
import { IJSONSchema } from '../../../../base/common/jsonSchema.js';
31
import { KeyCode } from '../../../../base/common/keyCodes.js';
32
import { Disposable, DisposableStore, isDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
33
import { isIOS } from '../../../../base/common/platform.js';
34
import { escapeRegExpCharacters } from '../../../../base/common/strings.js';
35
import { isDefined, isUndefinedOrNull } from '../../../../base/common/types.js';
36
import { URI } from '../../../../base/common/uri.js';
37
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
38
import { ILanguageService } from '../../../../editor/common/languages/language.js';
39
import { localize } from '../../../../nls.js';
40
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
41
import { ICommandService } from '../../../../platform/commands/common/commands.js';
42
import { ConfigurationTarget, IConfigurationService, getLanguageTagSettingPlainKey } from '../../../../platform/configuration/common/configuration.js';
43
import { ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js';
44
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
45
import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
46
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
47
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
48
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
49
import { IListService, WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js';
50
import { ILogService } from '../../../../platform/log/common/log.js';
51
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
52
import { IProductService } from '../../../../platform/product/common/productService.js';
53
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
54
import { defaultButtonStyles, getInputBoxStyle, getListStyles, getSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
55
import { editorBackground, foreground } from '../../../../platform/theme/common/colorRegistry.js';
56
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
57
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
58
import { getIgnoredSettings } from '../../../../platform/userDataSync/common/settingsMerge.js';
59
import { IUserDataSyncEnablementService, getDefaultIgnoredSettings } from '../../../../platform/userDataSync/common/userDataSync.js';
60
import { hasNativeContextMenu } from '../../../../platform/window/common/window.js';
61
import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js';
62
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
63
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
64
import { ISetting, ISettingsGroup, SETTINGS_AUTHORITY, SettingValueType } from '../../../services/preferences/common/preferences.js';
65
import { getInvalidTypeError } from '../../../services/preferences/common/preferencesValidation.js';
66
import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
67
import { LANGUAGE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, compareTwoNullableNumbers } from '../common/preferences.js';
68
import { settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js';
69
import { settingsMoreActionIcon } from './preferencesIcons.js';
70
import { SettingsTarget } from './preferencesWidgets.js';
71
import { ISettingOverrideClickEvent, SettingsTreeIndicatorsLabel, getIndicatorsLabelAriaLabel } from './settingsEditorSettingIndicators.js';
72
import { ITOCEntry, ITOCFilter } from './settingsLayout.js';
73
import { ISettingsEditorViewState, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement, inspectSetting, objectSettingSupportsRemoveDefaultValue, settingKeyToDisplayFormat } from './settingsTreeModels.js';
74
import { ExcludeSettingWidget, IBoolObjectDataItem, IIncludeExcludeDataItem, IListDataItem, IObjectDataItem, IObjectEnumOption, IObjectKeySuggester, IObjectValueSuggester, IncludeSettingWidget, ListSettingWidget, ObjectSettingCheckboxWidget, ObjectSettingDropdownWidget, ObjectValue, SettingListEvent } from './settingsWidgets.js';
75
76
const $ = DOM.$;
77
78
function getIncludeExcludeDisplayValue(element: SettingsTreeSettingElement): IIncludeExcludeDataItem[] {
79
const elementDefaultValue: Record<string, unknown> = typeof element.defaultValue === 'object'
80
? element.defaultValue ?? {}
81
: {};
82
83
const data = element.isConfigured ?
84
{ ...elementDefaultValue, ...element.scopeValue } :
85
elementDefaultValue;
86
87
return Object.keys(data)
88
.filter(key => !!data[key])
89
.map(key => {
90
const defaultValue = elementDefaultValue[key];
91
92
// Get source if it's a default value
93
let source: string | undefined;
94
if (defaultValue === data[key] && element.setting.type === 'object' && element.defaultValueSource instanceof Map) {
95
const defaultSource = element.defaultValueSource.get(`${element.setting.key}.${key}`);
96
source = typeof defaultSource === 'string' ? defaultSource : defaultSource?.displayName;
97
}
98
99
const value = data[key];
100
const sibling = typeof value === 'boolean' ? undefined : value.when;
101
return {
102
value: {
103
type: 'string',
104
data: key
105
},
106
sibling,
107
elementType: element.valueType,
108
source
109
};
110
});
111
}
112
113
function areAllPropertiesDefined(properties: string[], itemsToDisplay: IObjectDataItem[]): boolean {
114
const staticProperties = new Set(properties);
115
itemsToDisplay.forEach(({ key }) => staticProperties.delete(key.data));
116
return staticProperties.size === 0;
117
}
118
119
function getEnumOptionsFromSchema(schema: IJSONSchema): IObjectEnumOption[] {
120
if (schema.anyOf) {
121
return schema.anyOf.map(getEnumOptionsFromSchema).flat();
122
}
123
124
const enumDescriptions = schema.enumDescriptions ?? [];
125
126
return (schema.enum ?? []).map((value, idx) => {
127
const description = idx < enumDescriptions.length
128
? enumDescriptions[idx]
129
: undefined;
130
131
return { value, description };
132
});
133
}
134
135
function getObjectValueType(schema: IJSONSchema): ObjectValue['type'] {
136
if (schema.anyOf) {
137
const subTypes = schema.anyOf.map(getObjectValueType);
138
if (subTypes.some(type => type === 'enum')) {
139
return 'enum';
140
}
141
return 'string';
142
}
143
144
if (schema.type === 'boolean') {
145
return 'boolean';
146
} else if (schema.type === 'string' && isDefined(schema.enum) && schema.enum.length > 0) {
147
return 'enum';
148
} else {
149
return 'string';
150
}
151
}
152
153
function getObjectEntryValueDisplayValue(type: ObjectValue['type'], data: unknown, options: IObjectEnumOption[]): ObjectValue {
154
if (type === 'boolean') {
155
return { type, data: !!data };
156
} else if (type === 'enum') {
157
return { type, data: '' + data, options };
158
} else {
159
return { type, data: '' + data };
160
}
161
}
162
163
function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectDataItem[] {
164
const elementDefaultValue: Record<string, unknown> = typeof element.defaultValue === 'object'
165
? element.defaultValue ?? {}
166
: {};
167
168
const elementScopeValue: Record<string, unknown> = typeof element.scopeValue === 'object'
169
? element.scopeValue ?? {}
170
: {};
171
172
const data = element.isConfigured ?
173
{ ...elementDefaultValue, ...elementScopeValue } :
174
element.hasPolicyValue ? element.scopeValue :
175
elementDefaultValue;
176
177
const { objectProperties, objectPatternProperties, objectAdditionalProperties } = element.setting;
178
const patternsAndSchemas = Object
179
.entries(objectPatternProperties ?? {})
180
.map(([pattern, schema]) => ({
181
pattern: new RegExp(pattern),
182
schema
183
}));
184
185
const wellDefinedKeyEnumOptions = Object.entries(objectProperties ?? {}).map(
186
([key, schema]) => ({ value: key, description: schema.description })
187
);
188
189
return Object.keys(data).map(key => {
190
const defaultValue = elementDefaultValue[key];
191
192
// Get source if it's a default value
193
let source: string | undefined;
194
if (defaultValue === data[key] && element.setting.type === 'object' && element.defaultValueSource instanceof Map) {
195
const defaultSource = element.defaultValueSource.get(`${element.setting.key}.${key}`);
196
source = typeof defaultSource === 'string' ? defaultSource : defaultSource?.displayName;
197
}
198
199
if (isDefined(objectProperties) && key in objectProperties) {
200
const valueEnumOptions = getEnumOptionsFromSchema(objectProperties[key]);
201
return {
202
key: {
203
type: 'enum',
204
data: key,
205
options: wellDefinedKeyEnumOptions,
206
},
207
value: getObjectEntryValueDisplayValue(getObjectValueType(objectProperties[key]), data[key], valueEnumOptions),
208
keyDescription: objectProperties[key].description,
209
removable: isUndefinedOrNull(defaultValue),
210
resetable: !isUndefinedOrNull(defaultValue),
211
source
212
} satisfies IObjectDataItem;
213
}
214
215
// The row is removable if it doesn't have a default value assigned or the setting supports removing the default value.
216
// If a default value is assigned and the user modified the default, it can be reset back to the default.
217
const removable = defaultValue === undefined || objectSettingSupportsRemoveDefaultValue(element.setting.key);
218
const resetable = !!defaultValue && defaultValue !== data[key];
219
const schema = patternsAndSchemas.find(({ pattern }) => pattern.test(key))?.schema;
220
if (schema) {
221
const valueEnumOptions = getEnumOptionsFromSchema(schema);
222
return {
223
key: { type: 'string', data: key },
224
value: getObjectEntryValueDisplayValue(getObjectValueType(schema), data[key], valueEnumOptions),
225
keyDescription: schema.description,
226
removable,
227
resetable,
228
source
229
} satisfies IObjectDataItem;
230
}
231
232
const additionalValueEnums = getEnumOptionsFromSchema(
233
typeof objectAdditionalProperties === 'boolean'
234
? {}
235
: objectAdditionalProperties ?? {}
236
);
237
238
return {
239
key: { type: 'string', data: key },
240
value: getObjectEntryValueDisplayValue(
241
typeof objectAdditionalProperties === 'object' ? getObjectValueType(objectAdditionalProperties) : 'string',
242
data[key],
243
additionalValueEnums,
244
),
245
keyDescription: typeof objectAdditionalProperties === 'object' ? objectAdditionalProperties.description : undefined,
246
removable,
247
resetable,
248
source
249
} satisfies IObjectDataItem;
250
}).filter(item => !isUndefinedOrNull(item.value.data));
251
}
252
253
function getBoolObjectDisplayValue(element: SettingsTreeSettingElement): IBoolObjectDataItem[] {
254
const elementDefaultValue: Record<string, unknown> = typeof element.defaultValue === 'object'
255
? element.defaultValue ?? {}
256
: {};
257
258
const elementScopeValue: Record<string, unknown> = typeof element.scopeValue === 'object'
259
? element.scopeValue ?? {}
260
: {};
261
262
const data = element.isConfigured ?
263
{ ...elementDefaultValue, ...elementScopeValue } :
264
elementDefaultValue;
265
266
const { objectProperties } = element.setting;
267
const displayValues: IBoolObjectDataItem[] = [];
268
for (const key in objectProperties) {
269
const defaultValue = elementDefaultValue[key];
270
271
// Get source if it's a default value
272
let source: string | undefined;
273
if (defaultValue === data[key] && element.setting.type === 'object' && element.defaultValueSource instanceof Map) {
274
const defaultSource = element.defaultValueSource.get(key);
275
source = typeof defaultSource === 'string' ? defaultSource : defaultSource?.displayName;
276
}
277
278
displayValues.push({
279
key: {
280
type: 'string',
281
data: key
282
},
283
value: {
284
type: 'boolean',
285
data: !!data[key]
286
},
287
keyDescription: objectProperties[key].description,
288
removable: false,
289
resetable: true,
290
source
291
});
292
}
293
return displayValues;
294
}
295
296
function createArraySuggester(element: SettingsTreeSettingElement): IObjectKeySuggester {
297
return (keys, idx) => {
298
const enumOptions: IObjectEnumOption[] = [];
299
300
if (element.setting.enum) {
301
element.setting.enum.forEach((key, i) => {
302
// include the currently selected value, even if uniqueItems is true
303
if (!element.setting.uniqueItems || (idx !== undefined && key === keys[idx]) || !keys.includes(key)) {
304
const description = element.setting.enumDescriptions?.[i];
305
enumOptions.push({ value: key, description });
306
}
307
});
308
}
309
310
return enumOptions.length > 0
311
? { type: 'enum', data: enumOptions[0].value, options: enumOptions }
312
: undefined;
313
};
314
}
315
316
function createObjectKeySuggester(element: SettingsTreeSettingElement): IObjectKeySuggester {
317
const { objectProperties } = element.setting;
318
const allStaticKeys = Object.keys(objectProperties ?? {});
319
320
return keys => {
321
const existingKeys = new Set(keys);
322
const enumOptions: IObjectEnumOption[] = [];
323
324
allStaticKeys.forEach(staticKey => {
325
if (!existingKeys.has(staticKey)) {
326
enumOptions.push({ value: staticKey, description: objectProperties![staticKey].description });
327
}
328
});
329
330
return enumOptions.length > 0
331
? { type: 'enum', data: enumOptions[0].value, options: enumOptions }
332
: undefined;
333
};
334
}
335
336
function createObjectValueSuggester(element: SettingsTreeSettingElement): IObjectValueSuggester {
337
const { objectProperties, objectPatternProperties, objectAdditionalProperties } = element.setting;
338
339
const patternsAndSchemas = Object
340
.entries(objectPatternProperties ?? {})
341
.map(([pattern, schema]) => ({
342
pattern: new RegExp(pattern),
343
schema
344
}));
345
346
return (key: string) => {
347
let suggestedSchema: IJSONSchema | undefined;
348
349
if (isDefined(objectProperties) && key in objectProperties) {
350
suggestedSchema = objectProperties[key];
351
}
352
353
const patternSchema = suggestedSchema ?? patternsAndSchemas.find(({ pattern }) => pattern.test(key))?.schema;
354
355
if (isDefined(patternSchema)) {
356
suggestedSchema = patternSchema;
357
} else if (isDefined(objectAdditionalProperties) && typeof objectAdditionalProperties === 'object') {
358
suggestedSchema = objectAdditionalProperties;
359
}
360
361
if (isDefined(suggestedSchema)) {
362
const type = getObjectValueType(suggestedSchema);
363
364
if (type === 'boolean') {
365
return { type, data: suggestedSchema.default ?? true };
366
} else if (type === 'enum') {
367
const options = getEnumOptionsFromSchema(suggestedSchema);
368
return { type, data: suggestedSchema.default ?? options[0].value, options };
369
} else {
370
return { type, data: suggestedSchema.default ?? '' };
371
}
372
}
373
374
return;
375
};
376
}
377
378
function isNonNullableNumericType(type: unknown): type is 'number' | 'integer' {
379
return type === 'number' || type === 'integer';
380
}
381
382
function parseNumericObjectValues(dataElement: SettingsTreeSettingElement, v: Record<string, unknown>): Record<string, unknown> {
383
const newRecord: Record<string, unknown> = {};
384
for (const key in v) {
385
// Set to true/false once we're sure of the answer
386
let keyMatchesNumericProperty: boolean | undefined;
387
const patternProperties = dataElement.setting.objectPatternProperties;
388
const properties = dataElement.setting.objectProperties;
389
const additionalProperties = dataElement.setting.objectAdditionalProperties;
390
391
// Match the current record key against the properties of the object
392
if (properties) {
393
for (const propKey in properties) {
394
if (propKey === key) {
395
keyMatchesNumericProperty = isNonNullableNumericType(properties[propKey].type);
396
break;
397
}
398
}
399
}
400
if (keyMatchesNumericProperty === undefined && patternProperties) {
401
for (const patternKey in patternProperties) {
402
if (key.match(patternKey)) {
403
keyMatchesNumericProperty = isNonNullableNumericType(patternProperties[patternKey].type);
404
break;
405
}
406
}
407
}
408
if (keyMatchesNumericProperty === undefined && additionalProperties && typeof additionalProperties !== 'boolean') {
409
if (isNonNullableNumericType(additionalProperties.type)) {
410
keyMatchesNumericProperty = true;
411
}
412
}
413
newRecord[key] = keyMatchesNumericProperty ? Number(v[key]) : v[key];
414
}
415
return newRecord;
416
}
417
418
function getListDisplayValue(element: SettingsTreeSettingElement): IListDataItem[] {
419
if (!element.value || !Array.isArray(element.value)) {
420
return [];
421
}
422
423
if (element.setting.arrayItemType === 'enum') {
424
let enumOptions: IObjectEnumOption[] = [];
425
if (element.setting.enum) {
426
enumOptions = element.setting.enum.map((setting, i) => {
427
return {
428
value: setting,
429
description: element.setting.enumDescriptions?.[i]
430
};
431
});
432
}
433
return element.value.map((key: string) => {
434
return {
435
value: {
436
type: 'enum',
437
data: key,
438
options: enumOptions
439
}
440
};
441
});
442
} else {
443
return element.value.map((key: string) => {
444
return {
445
value: {
446
type: 'string',
447
data: key
448
}
449
};
450
});
451
}
452
}
453
454
function getShowAddButtonList(dataElement: SettingsTreeSettingElement, listDisplayValue: IListDataItem[]): boolean {
455
if (dataElement.setting.enum && dataElement.setting.uniqueItems) {
456
return dataElement.setting.enum.length - listDisplayValue.length > 0;
457
} else {
458
return true;
459
}
460
}
461
462
export function resolveSettingsTree(tocData: ITOCEntry<string>, coreSettingsGroups: ISettingsGroup[], filter: ITOCFilter | undefined, logService: ILogService): { tree: ITOCEntry<ISetting>; leftoverSettings: Set<ISetting> } {
463
const allSettings = getFlatSettings(coreSettingsGroups);
464
return {
465
tree: _resolveSettingsTree(tocData, allSettings, filter, logService),
466
leftoverSettings: allSettings
467
};
468
}
469
470
export function resolveConfiguredUntrustedSettings(groups: ISettingsGroup[], target: SettingsTarget, languageFilter: string | undefined, configurationService: IWorkbenchConfigurationService): ISetting[] {
471
const allSettings = getFlatSettings(groups);
472
return [...allSettings].filter(setting => setting.restricted && inspectSetting(setting.key, target, languageFilter, configurationService).isConfigured);
473
}
474
475
export async function createTocTreeForExtensionSettings(extensionService: IExtensionService, groups: ISettingsGroup[], filter: ITOCFilter | undefined): Promise<ITOCEntry<ISetting>> {
476
const extGroupTree = new Map<string, ITOCEntry<ISetting>>();
477
const addEntryToTree = (extensionId: string, extensionName: string, childEntry: ITOCEntry<ISetting>) => {
478
if (!extGroupTree.has(extensionId)) {
479
const rootEntry = {
480
id: extensionId,
481
label: extensionName,
482
children: []
483
};
484
extGroupTree.set(extensionId, rootEntry);
485
}
486
extGroupTree.get(extensionId)!.children!.push(childEntry);
487
};
488
const processGroupEntry = async (group: ISettingsGroup) => {
489
const flatSettings = group.sections.map(section => section.settings).flat();
490
const settings = filter ? getMatchingSettings(new Set(flatSettings), filter) : flatSettings;
491
sortSettings(settings);
492
493
const extensionId = group.extensionInfo!.id;
494
const extension = await extensionService.getExtension(extensionId);
495
const extensionName = extension?.displayName ?? extension?.name ?? extensionId;
496
497
// There could be multiple groups with the same extension id that all belong to the same extension.
498
// To avoid highlighting all groups upon expanding the extension's ToC entry,
499
// use the group ID only if it is non-empty and isn't the extension ID.
500
// Ref https://github.com/microsoft/vscode/issues/241521.
501
const settingGroupId = (group.id && group.id !== extensionId) ? group.id : group.title;
502
503
const childEntry: ITOCEntry<ISetting> = {
504
id: settingGroupId,
505
label: group.title,
506
order: group.order,
507
settings
508
};
509
addEntryToTree(extensionId, extensionName, childEntry);
510
};
511
512
const processPromises = groups.map(g => processGroupEntry(g));
513
return Promise.all(processPromises).then(() => {
514
const extGroups: ITOCEntry<ISetting>[] = [];
515
for (const extensionRootEntry of extGroupTree.values()) {
516
if (extensionRootEntry.children!.length === 1) {
517
// There is a single category for this extension.
518
// Push a flattened setting.
519
extGroups.push({
520
id: extensionRootEntry.id,
521
label: extensionRootEntry.children![0].label,
522
settings: extensionRootEntry.children![0].settings
523
});
524
} else {
525
// Sort the categories.
526
// Leave the undefined order categories untouched.
527
extensionRootEntry.children!.sort((a, b) => {
528
return compareTwoNullableNumbers(a.order, b.order);
529
});
530
531
// If there is a category that matches the setting name,
532
// add the settings in manually as "ungrouped" settings.
533
// https://github.com/microsoft/vscode/issues/137259
534
const ungroupedChild = extensionRootEntry.children!.find(child => child.label === extensionRootEntry.label);
535
if (ungroupedChild && !ungroupedChild.children) {
536
const groupedChildren = extensionRootEntry.children!.filter(child => child !== ungroupedChild);
537
extGroups.push({
538
id: extensionRootEntry.id,
539
label: extensionRootEntry.label,
540
settings: ungroupedChild.settings,
541
children: groupedChildren
542
});
543
} else {
544
// Push all the groups as-is.
545
extGroups.push(extensionRootEntry);
546
}
547
}
548
}
549
550
// Sort the outermost settings.
551
extGroups.sort((a, b) => a.label.localeCompare(b.label));
552
553
return {
554
id: 'extensions',
555
label: localize('extensions', "Extensions"),
556
children: extGroups
557
};
558
});
559
}
560
561
function _resolveSettingsTree(tocData: ITOCEntry<string>, allSettings: Set<ISetting>, filter: ITOCFilter | undefined, logService: ILogService): ITOCEntry<ISetting> {
562
let children: ITOCEntry<ISetting>[] | undefined;
563
if (tocData.children) {
564
children = tocData.children
565
.filter(child => child.hide !== true)
566
.map(child => _resolveSettingsTree(child, allSettings, filter, logService))
567
.filter(child => child.children?.length || child.settings?.length);
568
}
569
570
let settings: ISetting[] | undefined;
571
if (filter || tocData.settings) {
572
settings = getMatchingSettings(allSettings, {
573
include: {
574
keyPatterns: [...filter?.include?.keyPatterns ?? [], ...tocData.settings ?? []],
575
tags: filter?.include?.tags ? [...filter.include.tags] : []
576
},
577
exclude: filter?.exclude ?? {}
578
});
579
sortSettings(settings);
580
}
581
582
if (!children && !settings) {
583
throw new Error(`TOC node has no child groups or settings: ${tocData.id}`);
584
}
585
586
return {
587
id: tocData.id,
588
label: tocData.label,
589
children,
590
settings
591
};
592
}
593
594
/**
595
* Sort settings so that preview and experimental settings are deprioritized.
596
* Within each tier, sort the settings by order, then alphabetically.
597
*/
598
function sortSettings(settings: ISetting[]): void {
599
const SETTING_STATUS_NORMAL = 0;
600
const SETTING_STATUS_PREVIEW = 1;
601
const SETTING_STATUS_EXPERIMENTAL = 2;
602
603
const getExperimentalStatus = (setting: ISetting) => {
604
if (setting.tags?.includes('experimental')) {
605
return SETTING_STATUS_EXPERIMENTAL;
606
} else if (setting.tags?.includes('preview')) {
607
return SETTING_STATUS_PREVIEW;
608
}
609
return SETTING_STATUS_NORMAL;
610
};
611
612
settings.sort((a, b) => {
613
const experimentalStatusA = getExperimentalStatus(a);
614
const experimentalStatusB = getExperimentalStatus(b);
615
if (experimentalStatusA !== experimentalStatusB) {
616
return experimentalStatusA - experimentalStatusB;
617
}
618
619
const orderComparison = compareTwoNullableNumbers(a.order, b.order);
620
return orderComparison !== 0 ? orderComparison : a.key.localeCompare(b.key);
621
});
622
}
623
624
function getMatchingSettings(allSettings: Set<ISetting>, filter: ITOCFilter): ISetting[] {
625
const result: ISetting[] = [];
626
627
allSettings.forEach(setting => {
628
let shouldInclude = false;
629
let shouldExclude = false;
630
631
// Check include filters
632
if (filter.include?.keyPatterns) {
633
shouldInclude = filter.include.keyPatterns.some(pattern => {
634
if (pattern.startsWith('@tag:')) {
635
const tagName = pattern.substring(5);
636
return setting.tags?.includes(tagName);
637
} else {
638
return settingMatches(setting, pattern);
639
}
640
});
641
} else {
642
shouldInclude = true;
643
}
644
645
if (shouldInclude && filter.include?.tags?.length) {
646
shouldInclude = filter.include.tags.some(tag => setting.tags?.includes(tag));
647
}
648
649
// Check exclude filters (takes precedence)
650
if (filter.exclude?.keyPatterns) {
651
shouldExclude = filter.exclude.keyPatterns.some(pattern => {
652
if (pattern.startsWith('@tag:')) {
653
const tagName = pattern.substring(5);
654
return setting.tags?.includes(tagName);
655
} else {
656
return settingMatches(setting, pattern);
657
}
658
});
659
}
660
661
if (!shouldExclude && filter.exclude?.tags?.length) {
662
shouldExclude = filter.exclude.tags.some(tag => setting.tags?.includes(tag));
663
}
664
665
// Include if matches include filter and doesn't match exclude filter
666
if (shouldInclude && !shouldExclude) {
667
result.push(setting);
668
allSettings.delete(setting);
669
}
670
});
671
672
return result;
673
}
674
675
const settingPatternCache = new Map<string, RegExp>();
676
677
export function createSettingMatchRegExp(pattern: string): RegExp {
678
pattern = escapeRegExpCharacters(pattern)
679
.replace(/\\\*/g, '.*');
680
681
return new RegExp(`^${pattern}$`, 'i');
682
}
683
684
function settingMatches(s: ISetting, pattern: string): boolean {
685
let regExp = settingPatternCache.get(pattern);
686
if (!regExp) {
687
regExp = createSettingMatchRegExp(pattern);
688
settingPatternCache.set(pattern, regExp);
689
}
690
691
return regExp.test(s.key);
692
}
693
694
function getFlatSettings(settingsGroups: ISettingsGroup[]) {
695
const result: Set<ISetting> = new Set();
696
697
for (const group of settingsGroups) {
698
for (const section of group.sections) {
699
for (const s of section.settings) {
700
if (!s.overrides || !s.overrides.length) {
701
result.add(s);
702
}
703
}
704
}
705
}
706
707
return result;
708
}
709
710
interface IDisposableTemplate {
711
readonly toDispose: DisposableStore;
712
}
713
714
interface ISettingItemTemplate<T = any> extends IDisposableTemplate {
715
onChange?: (value: T) => void;
716
717
context?: SettingsTreeSettingElement;
718
containerElement: HTMLElement;
719
categoryElement: HTMLElement;
720
labelElement: SimpleIconLabel;
721
descriptionElement: HTMLElement;
722
controlElement: HTMLElement;
723
deprecationWarningElement: HTMLElement;
724
indicatorsLabel: SettingsTreeIndicatorsLabel;
725
toolbar: ToolBar;
726
readonly elementDisposables: DisposableStore;
727
}
728
729
interface ISettingBoolItemTemplate extends ISettingItemTemplate<boolean> {
730
checkbox: Toggle;
731
}
732
733
interface ISettingExtensionToggleItemTemplate extends ISettingItemTemplate<undefined> {
734
actionButton: Button;
735
dismissButton: Button;
736
}
737
738
interface ISettingTextItemTemplate extends ISettingItemTemplate<string> {
739
inputBox: InputBox;
740
validationErrorMessageElement: HTMLElement;
741
}
742
743
type ISettingNumberItemTemplate = ISettingTextItemTemplate;
744
745
interface ISettingEnumItemTemplate extends ISettingItemTemplate<number> {
746
selectBox: SelectBox;
747
selectElement: HTMLSelectElement | null;
748
enumDescriptionElement: HTMLElement;
749
}
750
751
interface ISettingComplexItemTemplate extends ISettingItemTemplate<void> {
752
button: HTMLElement;
753
validationErrorMessageElement: HTMLElement;
754
}
755
756
interface ISettingComplexObjectItemTemplate extends ISettingComplexItemTemplate {
757
objectSettingWidget: ObjectSettingDropdownWidget;
758
}
759
760
interface ISettingListItemTemplate extends ISettingItemTemplate<string[] | undefined> {
761
listWidget: ListSettingWidget<IListDataItem>;
762
validationErrorMessageElement: HTMLElement;
763
}
764
765
interface ISettingIncludeExcludeItemTemplate extends ISettingItemTemplate<void> {
766
includeExcludeWidget: ListSettingWidget<IIncludeExcludeDataItem>;
767
}
768
769
interface ISettingObjectItemTemplate extends ISettingItemTemplate<Record<string, unknown> | undefined> {
770
objectDropdownWidget?: ObjectSettingDropdownWidget;
771
objectCheckboxWidget?: ObjectSettingCheckboxWidget;
772
validationErrorMessageElement: HTMLElement;
773
}
774
775
interface ISettingNewExtensionsTemplate extends IDisposableTemplate {
776
button: Button;
777
context?: SettingsTreeNewExtensionsElement;
778
}
779
780
interface IGroupTitleTemplate extends IDisposableTemplate {
781
context?: SettingsTreeGroupElement;
782
parent: HTMLElement;
783
}
784
785
const SETTINGS_TEXT_TEMPLATE_ID = 'settings.text.template';
786
const SETTINGS_MULTILINE_TEXT_TEMPLATE_ID = 'settings.multilineText.template';
787
const SETTINGS_NUMBER_TEMPLATE_ID = 'settings.number.template';
788
const SETTINGS_ENUM_TEMPLATE_ID = 'settings.enum.template';
789
const SETTINGS_BOOL_TEMPLATE_ID = 'settings.bool.template';
790
const SETTINGS_ARRAY_TEMPLATE_ID = 'settings.array.template';
791
const SETTINGS_EXCLUDE_TEMPLATE_ID = 'settings.exclude.template';
792
const SETTINGS_INCLUDE_TEMPLATE_ID = 'settings.include.template';
793
const SETTINGS_OBJECT_TEMPLATE_ID = 'settings.object.template';
794
const SETTINGS_BOOL_OBJECT_TEMPLATE_ID = 'settings.boolObject.template';
795
const SETTINGS_COMPLEX_TEMPLATE_ID = 'settings.complex.template';
796
const SETTINGS_COMPLEX_OBJECT_TEMPLATE_ID = 'settings.complexObject.template';
797
const SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID = 'settings.newExtensions.template';
798
const SETTINGS_ELEMENT_TEMPLATE_ID = 'settings.group.template';
799
const SETTINGS_EXTENSION_TOGGLE_TEMPLATE_ID = 'settings.extensionToggle.template';
800
801
export interface ISettingChangeEvent {
802
key: string;
803
value: unknown; // undefined => reset/unconfigure
804
type: SettingValueType | SettingValueType[];
805
manualReset: boolean;
806
scope: ConfigurationScope | undefined;
807
}
808
809
export interface ISettingLinkClickEvent {
810
source: SettingsTreeSettingElement;
811
targetKey: string;
812
}
813
814
function removeChildrenFromTabOrder(node: Element): void {
815
// eslint-disable-next-line no-restricted-syntax
816
const focusableElements = node.querySelectorAll(`
817
[tabindex="0"],
818
input:not([tabindex="-1"]),
819
select:not([tabindex="-1"]),
820
textarea:not([tabindex="-1"]),
821
a:not([tabindex="-1"]),
822
button:not([tabindex="-1"]),
823
area:not([tabindex="-1"])
824
`);
825
826
focusableElements.forEach(element => {
827
element.setAttribute(AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR, 'true');
828
element.setAttribute('tabindex', '-1');
829
});
830
}
831
832
function addChildrenToTabOrder(node: Element): void {
833
// eslint-disable-next-line no-restricted-syntax
834
const focusableElements = node.querySelectorAll(
835
`[${AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR}="true"]`
836
);
837
838
focusableElements.forEach(element => {
839
element.removeAttribute(AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR);
840
element.setAttribute('tabindex', '0');
841
});
842
}
843
844
export interface HeightChangeParams {
845
element: SettingsTreeElement;
846
height: number;
847
}
848
849
export abstract class AbstractSettingRenderer extends Disposable implements ITreeRenderer<SettingsTreeElement, never, any> {
850
/** To override */
851
abstract get templateId(): string;
852
853
static readonly CONTROL_CLASS = 'setting-control-focus-target';
854
static readonly CONTROL_SELECTOR = '.' + this.CONTROL_CLASS;
855
static readonly CONTENTS_CLASS = 'setting-item-contents';
856
static readonly CONTENTS_SELECTOR = '.' + this.CONTENTS_CLASS;
857
static readonly ALL_ROWS_SELECTOR = '.monaco-list-row';
858
859
static readonly SETTING_KEY_ATTR = 'data-key';
860
static readonly SETTING_ID_ATTR = 'data-id';
861
static readonly ELEMENT_FOCUSABLE_ATTR = 'data-focusable';
862
863
private readonly _onDidClickOverrideElement = this._register(new Emitter<ISettingOverrideClickEvent>());
864
readonly onDidClickOverrideElement: Event<ISettingOverrideClickEvent> = this._onDidClickOverrideElement.event;
865
866
protected readonly _onDidChangeSetting = this._register(new Emitter<ISettingChangeEvent>());
867
readonly onDidChangeSetting: Event<ISettingChangeEvent> = this._onDidChangeSetting.event;
868
869
protected readonly _onDidOpenSettings = this._register(new Emitter<string>());
870
readonly onDidOpenSettings: Event<string> = this._onDidOpenSettings.event;
871
872
private readonly _onDidClickSettingLink = this._register(new Emitter<ISettingLinkClickEvent>());
873
readonly onDidClickSettingLink: Event<ISettingLinkClickEvent> = this._onDidClickSettingLink.event;
874
875
protected readonly _onDidFocusSetting = this._register(new Emitter<SettingsTreeSettingElement>());
876
readonly onDidFocusSetting: Event<SettingsTreeSettingElement> = this._onDidFocusSetting.event;
877
878
private ignoredSettings: string[];
879
private readonly _onDidChangeIgnoredSettings = this._register(new Emitter<void>());
880
readonly onDidChangeIgnoredSettings: Event<void> = this._onDidChangeIgnoredSettings.event;
881
882
protected readonly _onDidChangeSettingHeight = this._register(new Emitter<HeightChangeParams>());
883
readonly onDidChangeSettingHeight: Event<HeightChangeParams> = this._onDidChangeSettingHeight.event;
884
885
protected readonly _onApplyFilter = this._register(new Emitter<string>());
886
readonly onApplyFilter: Event<string> = this._onApplyFilter.event;
887
888
constructor(
889
private readonly settingActions: IAction[],
890
private readonly disposableActionFactory: (setting: ISetting, settingTarget: SettingsTarget) => IAction[],
891
@IThemeService protected readonly _themeService: IThemeService,
892
@IContextViewService protected readonly _contextViewService: IContextViewService,
893
@IOpenerService protected readonly _openerService: IOpenerService,
894
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
895
@ICommandService protected readonly _commandService: ICommandService,
896
@IContextMenuService protected readonly _contextMenuService: IContextMenuService,
897
@IKeybindingService protected readonly _keybindingService: IKeybindingService,
898
@IConfigurationService protected readonly _configService: IConfigurationService,
899
@IExtensionService protected readonly _extensionsService: IExtensionService,
900
@IExtensionsWorkbenchService protected readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,
901
@IProductService protected readonly _productService: IProductService,
902
@ITelemetryService protected readonly _telemetryService: ITelemetryService,
903
@IHoverService protected readonly _hoverService: IHoverService,
904
@IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService,
905
) {
906
super();
907
908
this.ignoredSettings = getIgnoredSettings(getDefaultIgnoredSettings(), this._configService);
909
this._register(this._configService.onDidChangeConfiguration(e => {
910
this.ignoredSettings = getIgnoredSettings(getDefaultIgnoredSettings(), this._configService);
911
this._onDidChangeIgnoredSettings.fire();
912
}));
913
}
914
915
abstract renderTemplate(container: HTMLElement): any;
916
917
abstract renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: unknown): void;
918
919
protected renderCommonTemplate(tree: unknown, _container: HTMLElement, typeClass: string): ISettingItemTemplate {
920
_container.classList.add('setting-item');
921
_container.classList.add('setting-item-' + typeClass);
922
923
const toDispose = new DisposableStore();
924
925
const container = DOM.append(_container, $(AbstractSettingRenderer.CONTENTS_SELECTOR));
926
container.classList.add('settings-row-inner-container');
927
const titleElement = DOM.append(container, $('.setting-item-title'));
928
const labelCategoryContainer = DOM.append(titleElement, $('.setting-item-cat-label-container'));
929
const categoryElement = DOM.append(labelCategoryContainer, $('span.setting-item-category'));
930
const labelElementContainer = DOM.append(labelCategoryContainer, $('span.setting-item-label'));
931
const labelElement = toDispose.add(new SimpleIconLabel(labelElementContainer));
932
const indicatorsLabel = toDispose.add(this._instantiationService.createInstance(SettingsTreeIndicatorsLabel, titleElement));
933
934
const descriptionElement = DOM.append(container, $('.setting-item-description'));
935
const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator'));
936
toDispose.add(this._hoverService.setupDelayedHover(modifiedIndicatorElement, {
937
content: localize('modified', "The setting has been configured in the current scope.")
938
}));
939
940
const valueElement = DOM.append(container, $('.setting-item-value'));
941
const controlElement = DOM.append(valueElement, $('div.setting-item-control'));
942
943
const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message'));
944
945
const toolbarContainer = DOM.append(container, $('.setting-toolbar-container'));
946
const toolbar = this.renderSettingToolbar(toolbarContainer);
947
948
const template: ISettingItemTemplate = {
949
toDispose,
950
elementDisposables: toDispose.add(new DisposableStore()),
951
952
containerElement: container,
953
categoryElement,
954
labelElement,
955
descriptionElement,
956
controlElement,
957
deprecationWarningElement,
958
indicatorsLabel,
959
toolbar
960
};
961
962
// Prevent clicks from being handled by list
963
toDispose.add(DOM.addDisposableListener(controlElement, DOM.EventType.MOUSE_DOWN, e => e.stopPropagation()));
964
965
toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_ENTER, e => container.classList.add('mouseover')));
966
toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover')));
967
968
return template;
969
}
970
971
protected addSettingElementFocusHandler(template: ISettingItemTemplate): void {
972
const focusTracker = DOM.trackFocus(template.containerElement);
973
template.toDispose.add(focusTracker);
974
template.toDispose.add(focusTracker.onDidBlur(() => {
975
if (template.containerElement.classList.contains('focused')) {
976
template.containerElement.classList.remove('focused');
977
}
978
}));
979
980
template.toDispose.add(focusTracker.onDidFocus(() => {
981
template.containerElement.classList.add('focused');
982
983
if (template.context) {
984
this._onDidFocusSetting.fire(template.context);
985
}
986
}));
987
}
988
989
protected renderSettingToolbar(container: HTMLElement): ToolBar {
990
const toggleMenuTitle = this._keybindingService.appendKeybinding(
991
localize('settingsContextMenuTitle', "More Actions... "),
992
SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU);
993
994
const toolbar = new ToolBar(container, this._contextMenuService, {
995
toggleMenuTitle,
996
renderDropdownAsChildElement: !isIOS,
997
moreIcon: settingsMoreActionIcon
998
});
999
return toolbar;
1000
}
1001
1002
protected renderSettingElement(node: ITreeNode<SettingsTreeSettingElement, never>, index: number, template: ISettingItemTemplate | ISettingBoolItemTemplate): void {
1003
const element = node.element;
1004
1005
// The element must inspect itself to get information for
1006
// the modified indicator and the overridden Settings indicators.
1007
element.inspectSelf();
1008
1009
template.context = element;
1010
template.toolbar.context = element;
1011
const actions = this.disposableActionFactory(element.setting, element.settingsTarget);
1012
actions.forEach(a => isDisposable(a) && template.elementDisposables.add(a));
1013
template.toolbar.setActions([], [...this.settingActions, ...actions]);
1014
1015
const setting = element.setting;
1016
1017
template.containerElement.classList.toggle('is-configured', element.isConfigured);
1018
template.containerElement.setAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR, element.setting.key);
1019
template.containerElement.setAttribute(AbstractSettingRenderer.SETTING_ID_ATTR, element.id);
1020
1021
const titleTooltip = setting.key + (element.isConfigured ? ' - Modified' : '');
1022
template.categoryElement.textContent = element.displayCategory ? (element.displayCategory + ': ') : '';
1023
template.elementDisposables.add(this._hoverService.setupDelayedHover(template.categoryElement, { content: titleTooltip }));
1024
1025
template.labelElement.text = element.displayLabel;
1026
template.labelElement.title = titleTooltip;
1027
1028
template.descriptionElement.innerText = '';
1029
if (element.setting.descriptionIsMarkdown) {
1030
const renderedDescription = this.renderSettingMarkdown(element, template.containerElement, element.description, template.elementDisposables);
1031
template.descriptionElement.appendChild(renderedDescription);
1032
} else {
1033
template.descriptionElement.innerText = element.description;
1034
}
1035
1036
template.indicatorsLabel.updateScopeOverrides(element, this._onDidClickOverrideElement, this._onApplyFilter);
1037
template.elementDisposables.add(this._configService.onDidChangeConfiguration(e => {
1038
if (e.affectsConfiguration(APPLY_ALL_PROFILES_SETTING)) {
1039
template.indicatorsLabel.updateScopeOverrides(element, this._onDidClickOverrideElement, this._onApplyFilter);
1040
}
1041
}));
1042
1043
const onChange = (value: unknown) => this._onDidChangeSetting.fire({
1044
key: element.setting.key,
1045
value,
1046
type: template.context!.valueType,
1047
manualReset: false,
1048
scope: element.setting.scope
1049
});
1050
const deprecationText = element.setting.deprecationMessage || '';
1051
if (deprecationText && element.setting.deprecationMessageIsMarkdown) {
1052
template.deprecationWarningElement.innerText = '';
1053
template.deprecationWarningElement.appendChild(this.renderSettingMarkdown(element, template.containerElement, element.setting.deprecationMessage!, template.elementDisposables));
1054
} else {
1055
template.deprecationWarningElement.innerText = deprecationText;
1056
}
1057
template.deprecationWarningElement.prepend($('.codicon.codicon-error'));
1058
template.containerElement.classList.toggle('is-deprecated', !!deprecationText);
1059
1060
this.renderValue(element, <ISettingItemTemplate>template, onChange);
1061
1062
template.indicatorsLabel.updateWorkspaceTrust(element);
1063
template.indicatorsLabel.updateSyncIgnored(element, this.ignoredSettings);
1064
template.indicatorsLabel.updateDefaultOverrideIndicator(element);
1065
template.indicatorsLabel.updatePreviewIndicator(element);
1066
template.indicatorsLabel.updateAdvancedIndicator(element);
1067
template.elementDisposables.add(this.onDidChangeIgnoredSettings(() => {
1068
template.indicatorsLabel.updateSyncIgnored(element, this.ignoredSettings);
1069
}));
1070
1071
this.updateSettingTabbable(element, template);
1072
template.elementDisposables.add(element.onDidChangeTabbable(() => {
1073
this.updateSettingTabbable(element, template);
1074
}));
1075
}
1076
1077
private updateSettingTabbable(element: SettingsTreeSettingElement, template: ISettingItemTemplate | ISettingBoolItemTemplate): void {
1078
if (element.tabbable) {
1079
addChildrenToTabOrder(template.containerElement);
1080
} else {
1081
removeChildrenFromTabOrder(template.containerElement);
1082
}
1083
}
1084
1085
private renderSettingMarkdown(element: SettingsTreeSettingElement, container: HTMLElement, text: string, disposables: DisposableStore): HTMLElement {
1086
// Rewrite `#editor.fontSize#` to link format
1087
text = fixSettingLinks(text);
1088
1089
const renderedMarkdown = disposables.add(this._markdownRendererService.render({ value: text, isTrusted: true }, {
1090
actionHandler: (content: string) => {
1091
if (content.startsWith('#')) {
1092
const e: ISettingLinkClickEvent = {
1093
source: element,
1094
targetKey: content.substring(1)
1095
};
1096
this._onDidClickSettingLink.fire(e);
1097
} else {
1098
this._openerService.open(content, { allowCommands: true }).catch(onUnexpectedError);
1099
}
1100
},
1101
asyncRenderCallback: () => {
1102
const height = container.clientHeight;
1103
if (height) {
1104
this._onDidChangeSettingHeight.fire({ element, height });
1105
}
1106
},
1107
}));
1108
1109
renderedMarkdown.element.classList.add('setting-item-markdown');
1110
cleanRenderedMarkdown(renderedMarkdown.element);
1111
return renderedMarkdown.element;
1112
}
1113
1114
protected abstract renderValue(dataElement: SettingsTreeSettingElement, template: ISettingItemTemplate, onChange: (value: unknown) => void): void;
1115
1116
disposeTemplate(template: IDisposableTemplate): void {
1117
template.toDispose.dispose();
1118
}
1119
1120
disposeElement(_element: ITreeNode<SettingsTreeElement>, _index: number, template: IDisposableTemplate): void {
1121
(template as ISettingItemTemplate).elementDisposables?.clear();
1122
}
1123
}
1124
1125
class SettingGroupRenderer implements ITreeRenderer<SettingsTreeGroupElement, never, IGroupTitleTemplate> {
1126
templateId = SETTINGS_ELEMENT_TEMPLATE_ID;
1127
1128
renderTemplate(container: HTMLElement): IGroupTitleTemplate {
1129
container.classList.add('group-title');
1130
1131
const template: IGroupTitleTemplate = {
1132
parent: container,
1133
toDispose: new DisposableStore()
1134
};
1135
1136
return template;
1137
}
1138
1139
renderElement(element: ITreeNode<SettingsTreeGroupElement, never>, index: number, templateData: IGroupTitleTemplate): void {
1140
templateData.parent.innerText = '';
1141
const labelElement = DOM.append(templateData.parent, $('div.settings-group-title-label.settings-row-inner-container'));
1142
labelElement.classList.add(`settings-group-level-${element.element.level}`);
1143
labelElement.textContent = element.element.label;
1144
1145
if (element.element.isFirstGroup) {
1146
labelElement.classList.add('settings-group-first');
1147
}
1148
}
1149
1150
disposeTemplate(templateData: IGroupTitleTemplate): void {
1151
templateData.toDispose.dispose();
1152
}
1153
}
1154
1155
export class SettingNewExtensionsRenderer implements ITreeRenderer<SettingsTreeNewExtensionsElement, never, ISettingNewExtensionsTemplate> {
1156
templateId = SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID;
1157
1158
constructor(
1159
@ICommandService private readonly _commandService: ICommandService,
1160
) {
1161
}
1162
1163
renderTemplate(container: HTMLElement): ISettingNewExtensionsTemplate {
1164
const toDispose = new DisposableStore();
1165
1166
container.classList.add('setting-item-new-extensions');
1167
1168
const button = new Button(container, { title: true, ...defaultButtonStyles });
1169
toDispose.add(button);
1170
toDispose.add(button.onDidClick(() => {
1171
if (template.context) {
1172
this._commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', template.context.extensionIds);
1173
}
1174
}));
1175
button.label = localize('newExtensionsButtonLabel', "Show matching extensions");
1176
button.element.classList.add('settings-new-extensions-button');
1177
1178
const template: ISettingNewExtensionsTemplate = {
1179
button,
1180
toDispose
1181
};
1182
1183
return template;
1184
}
1185
1186
renderElement(element: ITreeNode<SettingsTreeNewExtensionsElement, never>, index: number, templateData: ISettingNewExtensionsTemplate): void {
1187
templateData.context = element.element;
1188
}
1189
1190
disposeTemplate(template: IDisposableTemplate): void {
1191
template.toDispose.dispose();
1192
}
1193
}
1194
1195
export class SettingComplexRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingComplexItemTemplate> {
1196
private static readonly EDIT_IN_JSON_LABEL = localize('editInSettingsJson', "Edit in settings.json");
1197
1198
templateId = SETTINGS_COMPLEX_TEMPLATE_ID;
1199
1200
renderTemplate(container: HTMLElement): ISettingComplexItemTemplate {
1201
const common = this.renderCommonTemplate(null, container, 'complex');
1202
1203
const openSettingsButton = DOM.append(common.controlElement, $('a.edit-in-settings-button'));
1204
openSettingsButton.classList.add(AbstractSettingRenderer.CONTROL_CLASS);
1205
openSettingsButton.role = 'button';
1206
1207
const validationErrorMessageElement = $('.setting-item-validation-message');
1208
common.containerElement.appendChild(validationErrorMessageElement);
1209
1210
const template: ISettingComplexItemTemplate = {
1211
...common,
1212
button: openSettingsButton,
1213
validationErrorMessageElement
1214
};
1215
1216
this.addSettingElementFocusHandler(template);
1217
1218
return template;
1219
}
1220
1221
renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingComplexItemTemplate): void {
1222
super.renderSettingElement(element, index, templateData);
1223
}
1224
1225
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingComplexItemTemplate, onChange: (value: string) => void): void {
1226
const plainKey = getLanguageTagSettingPlainKey(dataElement.setting.key);
1227
const editLanguageSettingLabel = localize('editLanguageSettingLabel', "Edit settings for {0}", plainKey);
1228
const isLanguageTagSetting = dataElement.setting.isLanguageTagSetting;
1229
template.button.textContent = isLanguageTagSetting
1230
? editLanguageSettingLabel
1231
: SettingComplexRenderer.EDIT_IN_JSON_LABEL;
1232
1233
const onClickOrKeydown = (e: UIEvent) => {
1234
if (isLanguageTagSetting) {
1235
this._onApplyFilter.fire(`@${LANGUAGE_SETTING_TAG}${plainKey.replaceAll(' ', '')}`);
1236
} else {
1237
this._onDidOpenSettings.fire(dataElement.setting.key);
1238
}
1239
e.preventDefault();
1240
e.stopPropagation();
1241
};
1242
template.elementDisposables.add(DOM.addDisposableListener(template.button, DOM.EventType.CLICK, (e) => {
1243
onClickOrKeydown(e);
1244
}));
1245
template.elementDisposables.add(DOM.addDisposableListener(template.button, DOM.EventType.KEY_DOWN, (e) => {
1246
const ev = new StandardKeyboardEvent(e);
1247
if (ev.equals(KeyCode.Space) || ev.equals(KeyCode.Enter)) {
1248
onClickOrKeydown(e);
1249
}
1250
}));
1251
1252
this.renderValidations(dataElement, template);
1253
1254
if (isLanguageTagSetting) {
1255
template.button.setAttribute('aria-label', editLanguageSettingLabel);
1256
} else {
1257
template.button.setAttribute('aria-label', `${SettingComplexRenderer.EDIT_IN_JSON_LABEL}: ${dataElement.setting.key}`);
1258
}
1259
}
1260
1261
private renderValidations(dataElement: SettingsTreeSettingElement, template: ISettingComplexItemTemplate) {
1262
const errMsg = dataElement.isConfigured && getInvalidTypeError(dataElement.value, dataElement.setting.type);
1263
if (errMsg) {
1264
template.containerElement.classList.add('invalid-input');
1265
template.validationErrorMessageElement.innerText = errMsg;
1266
return;
1267
}
1268
1269
template.containerElement.classList.remove('invalid-input');
1270
}
1271
}
1272
1273
class SettingComplexObjectRenderer extends SettingComplexRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingComplexObjectItemTemplate> {
1274
1275
override templateId = SETTINGS_COMPLEX_OBJECT_TEMPLATE_ID;
1276
1277
override renderTemplate(container: HTMLElement): ISettingComplexObjectItemTemplate {
1278
const common = this.renderCommonTemplate(null, container, 'list');
1279
1280
const objectSettingWidget = common.toDispose.add(this._instantiationService.createInstance(ObjectSettingDropdownWidget, common.controlElement));
1281
objectSettingWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS);
1282
1283
const openSettingsButton = DOM.append(DOM.append(common.controlElement, $('.complex-object-edit-in-settings-button-container')), $('a.complex-object.edit-in-settings-button'));
1284
openSettingsButton.classList.add(AbstractSettingRenderer.CONTROL_CLASS);
1285
openSettingsButton.role = 'button';
1286
1287
const validationErrorMessageElement = $('.setting-item-validation-message');
1288
common.containerElement.appendChild(validationErrorMessageElement);
1289
1290
const template: ISettingComplexObjectItemTemplate = {
1291
...common,
1292
button: openSettingsButton,
1293
validationErrorMessageElement,
1294
objectSettingWidget
1295
};
1296
1297
this.addSettingElementFocusHandler(template);
1298
1299
return template;
1300
}
1301
1302
protected override renderValue(dataElement: SettingsTreeSettingElement, template: ISettingComplexObjectItemTemplate, onChange: (value: string) => void): void {
1303
const items = getObjectDisplayValue(dataElement);
1304
template.objectSettingWidget.setValue(items, {
1305
settingKey: dataElement.setting.key,
1306
showAddButton: false,
1307
isReadOnly: true,
1308
});
1309
template.button.parentElement?.classList.toggle('hide', dataElement.hasPolicyValue);
1310
super.renderValue(dataElement, template, onChange);
1311
}
1312
}
1313
1314
class SettingArrayRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingListItemTemplate> {
1315
templateId = SETTINGS_ARRAY_TEMPLATE_ID;
1316
1317
renderTemplate(container: HTMLElement): ISettingListItemTemplate {
1318
const common = this.renderCommonTemplate(null, container, 'list');
1319
// eslint-disable-next-line no-restricted-syntax
1320
const descriptionElement = common.containerElement.querySelector('.setting-item-description')!;
1321
const validationErrorMessageElement = $('.setting-item-validation-message');
1322
descriptionElement.after(validationErrorMessageElement);
1323
1324
const listWidget = this._instantiationService.createInstance(ListSettingWidget, common.controlElement);
1325
listWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS);
1326
common.toDispose.add(listWidget);
1327
1328
const template: ISettingListItemTemplate = {
1329
...common,
1330
listWidget,
1331
validationErrorMessageElement
1332
};
1333
1334
this.addSettingElementFocusHandler(template);
1335
1336
common.toDispose.add(
1337
listWidget.onDidChangeList(e => {
1338
const newList = this.computeNewList(template, e);
1339
template.onChange?.(newList);
1340
})
1341
);
1342
1343
return template;
1344
}
1345
1346
private computeNewList(template: ISettingListItemTemplate, e: SettingListEvent<IListDataItem>): string[] | undefined {
1347
if (template.context) {
1348
let newValue: string[] = [];
1349
if (Array.isArray(template.context.scopeValue)) {
1350
newValue = [...template.context.scopeValue];
1351
} else if (Array.isArray(template.context.value)) {
1352
newValue = [...template.context.value];
1353
}
1354
1355
if (e.type === 'move') {
1356
// A drag and drop occurred
1357
const sourceIndex = e.sourceIndex;
1358
const targetIndex = e.targetIndex;
1359
const splicedElem = newValue.splice(sourceIndex, 1)[0];
1360
newValue.splice(targetIndex, 0, splicedElem);
1361
} else if (e.type === 'remove' || e.type === 'reset') {
1362
newValue.splice(e.targetIndex, 1);
1363
} else if (e.type === 'change') {
1364
const itemValueData = e.newItem.value.data.toString();
1365
1366
// Update value
1367
if (e.targetIndex > -1) {
1368
newValue[e.targetIndex] = itemValueData;
1369
}
1370
// For some reason, we are updating and cannot find original value
1371
// Just append the value in this case
1372
else {
1373
newValue.push(itemValueData);
1374
}
1375
} else if (e.type === 'add') {
1376
newValue.push(e.newItem.value.data.toString());
1377
}
1378
1379
if (
1380
template.context.defaultValue &&
1381
Array.isArray(template.context.defaultValue) &&
1382
template.context.defaultValue.length === newValue.length &&
1383
template.context.defaultValue.join() === newValue.join()
1384
) {
1385
return undefined;
1386
}
1387
return newValue;
1388
}
1389
1390
return undefined;
1391
}
1392
1393
renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingListItemTemplate): void {
1394
super.renderSettingElement(element, index, templateData);
1395
}
1396
1397
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingListItemTemplate, onChange: (value: string[] | number[] | undefined) => void): void {
1398
const value = getListDisplayValue(dataElement);
1399
const keySuggester = dataElement.setting.enum ? createArraySuggester(dataElement) : undefined;
1400
template.listWidget.setValue(value, {
1401
showAddButton: getShowAddButtonList(dataElement, value),
1402
keySuggester
1403
});
1404
template.context = dataElement;
1405
1406
template.elementDisposables.add(toDisposable(() => {
1407
template.listWidget.cancelEdit();
1408
}));
1409
1410
template.onChange = (v: string[] | undefined) => {
1411
if (v && !renderArrayValidations(dataElement, template, v, false)) {
1412
const itemType = dataElement.setting.arrayItemType;
1413
const arrToSave = isNonNullableNumericType(itemType) ? v.map(a => +a) : v;
1414
onChange(arrToSave);
1415
} else {
1416
// Save the setting unparsed and containing the errors.
1417
// renderArrayValidations will render relevant error messages.
1418
onChange(v);
1419
}
1420
};
1421
1422
renderArrayValidations(dataElement, template, value.map(v => v.value.data.toString()), true);
1423
}
1424
}
1425
1426
abstract class AbstractSettingObjectRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingObjectItemTemplate> {
1427
1428
protected renderTemplateWithWidget(common: ISettingItemTemplate, widget: ObjectSettingCheckboxWidget | ObjectSettingDropdownWidget): ISettingObjectItemTemplate {
1429
widget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS);
1430
common.toDispose.add(widget);
1431
1432
// eslint-disable-next-line no-restricted-syntax
1433
const descriptionElement = common.containerElement.querySelector('.setting-item-description')!;
1434
const validationErrorMessageElement = $('.setting-item-validation-message');
1435
descriptionElement.after(validationErrorMessageElement);
1436
1437
const template: ISettingObjectItemTemplate = {
1438
...common,
1439
validationErrorMessageElement
1440
};
1441
if (widget instanceof ObjectSettingCheckboxWidget) {
1442
template.objectCheckboxWidget = widget;
1443
} else {
1444
template.objectDropdownWidget = widget;
1445
}
1446
1447
this.addSettingElementFocusHandler(template);
1448
return template;
1449
}
1450
1451
renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingObjectItemTemplate): void {
1452
super.renderSettingElement(element, index, templateData);
1453
}
1454
}
1455
1456
class SettingObjectRenderer extends AbstractSettingObjectRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingObjectItemTemplate> {
1457
override templateId = SETTINGS_OBJECT_TEMPLATE_ID;
1458
1459
renderTemplate(container: HTMLElement): ISettingObjectItemTemplate {
1460
const common = this.renderCommonTemplate(null, container, 'list');
1461
const widget = this._instantiationService.createInstance(ObjectSettingDropdownWidget, common.controlElement);
1462
const template = this.renderTemplateWithWidget(common, widget);
1463
common.toDispose.add(widget.onDidChangeList(e => {
1464
this.onDidChangeObject(template, e);
1465
}));
1466
return template;
1467
}
1468
1469
private onDidChangeObject(template: ISettingObjectItemTemplate, e: SettingListEvent<IObjectDataItem>): void {
1470
const widget = template.objectDropdownWidget!;
1471
if (template.context) {
1472
const settingSupportsRemoveDefault = objectSettingSupportsRemoveDefaultValue(template.context.setting.key);
1473
const defaultValue: Record<string, unknown> = typeof template.context.defaultValue === 'object'
1474
? template.context.defaultValue ?? {}
1475
: {};
1476
1477
const scopeValue: Record<string, unknown> = typeof template.context.scopeValue === 'object'
1478
? template.context.scopeValue ?? {}
1479
: {};
1480
1481
const newValue: Record<string, unknown> = { ...template.context.scopeValue }; // Initialize with scoped values as removed default values are not rendered
1482
const newItems: IObjectDataItem[] = [];
1483
1484
widget.items.forEach((item, idx) => {
1485
// Item was updated
1486
if ((e.type === 'change' || e.type === 'move') && e.targetIndex === idx) {
1487
// If the key of the default value is changed, remove the default value
1488
if (e.originalItem.key.data !== e.newItem.key.data && settingSupportsRemoveDefault && e.originalItem.key.data in defaultValue) {
1489
newValue[e.originalItem.key.data] = null;
1490
} else {
1491
delete newValue[e.originalItem.key.data];
1492
}
1493
newValue[e.newItem.key.data] = e.newItem.value.data;
1494
newItems.push(e.newItem);
1495
}
1496
// All remaining items, but skip the one that we just updated
1497
else if ((e.type !== 'change' && e.type !== 'move') || e.newItem.key.data !== item.key.data) {
1498
newValue[item.key.data] = item.value.data;
1499
newItems.push(item);
1500
}
1501
});
1502
1503
// Item was deleted
1504
if (e.type === 'remove' || e.type === 'reset') {
1505
const objectKey = e.originalItem.key.data;
1506
const removingDefaultValue = e.type === 'remove' && settingSupportsRemoveDefault && defaultValue[objectKey] === e.originalItem.value.data;
1507
if (removingDefaultValue) {
1508
newValue[objectKey] = null;
1509
} else {
1510
delete newValue[objectKey];
1511
}
1512
1513
const itemToDelete = newItems.findIndex(item => item.key.data === objectKey);
1514
const defaultItemValue = defaultValue[objectKey] as string | boolean;
1515
1516
// Item does not have a default or default is bing removed
1517
if (removingDefaultValue || isUndefinedOrNull(defaultValue[objectKey]) && itemToDelete > -1) {
1518
newItems.splice(itemToDelete, 1);
1519
} else if (!removingDefaultValue && itemToDelete > -1) {
1520
newItems[itemToDelete].value.data = defaultItemValue;
1521
}
1522
}
1523
// New item was added
1524
else if (e.type === 'add') {
1525
newValue[e.newItem.key.data] = e.newItem.value.data;
1526
newItems.push(e.newItem);
1527
}
1528
1529
Object.entries(newValue).forEach(([key, value]) => {
1530
// value from the scope has changed back to the default
1531
if (scopeValue[key] !== value && defaultValue[key] === value && !(settingSupportsRemoveDefault && value === null)) {
1532
delete newValue[key];
1533
}
1534
});
1535
1536
const newObject = Object.keys(newValue).length === 0 ? undefined : newValue;
1537
template.objectDropdownWidget!.setValue(newItems);
1538
template.onChange?.(newObject);
1539
}
1540
}
1541
1542
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingObjectItemTemplate, onChange: (value: Record<string, unknown> | undefined) => void): void {
1543
const items = getObjectDisplayValue(dataElement);
1544
const { key, objectProperties, objectPatternProperties, objectAdditionalProperties, propertyNames } = dataElement.setting;
1545
1546
template.objectDropdownWidget!.setValue(items, {
1547
settingKey: key,
1548
showAddButton: objectAdditionalProperties === false
1549
? (
1550
!areAllPropertiesDefined(Object.keys(objectProperties ?? {}), items) ||
1551
isDefined(objectPatternProperties)
1552
)
1553
: true,
1554
keySuggester: createObjectKeySuggester(dataElement),
1555
valueSuggester: createObjectValueSuggester(dataElement),
1556
propertyNames
1557
});
1558
1559
template.context = dataElement;
1560
1561
template.elementDisposables.add(toDisposable(() => {
1562
template.objectDropdownWidget!.cancelEdit();
1563
}));
1564
1565
template.onChange = (v: Record<string, unknown> | undefined) => {
1566
if (v && !renderArrayValidations(dataElement, template, v, false)) {
1567
const parsedRecord = parseNumericObjectValues(dataElement, v);
1568
onChange(parsedRecord);
1569
} else {
1570
// Save the setting unparsed and containing the errors.
1571
// renderArrayValidations will render relevant error messages.
1572
onChange(v);
1573
}
1574
};
1575
renderArrayValidations(dataElement, template, dataElement.value, true);
1576
}
1577
}
1578
1579
class SettingBoolObjectRenderer extends AbstractSettingObjectRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingObjectItemTemplate> {
1580
override templateId = SETTINGS_BOOL_OBJECT_TEMPLATE_ID;
1581
1582
renderTemplate(container: HTMLElement): ISettingObjectItemTemplate {
1583
const common = this.renderCommonTemplate(null, container, 'list');
1584
const widget = this._instantiationService.createInstance(ObjectSettingCheckboxWidget, common.controlElement);
1585
const template = this.renderTemplateWithWidget(common, widget);
1586
common.toDispose.add(widget.onDidChangeList(e => {
1587
this.onDidChangeObject(template, e);
1588
}));
1589
return template;
1590
}
1591
1592
protected onDidChangeObject(template: ISettingObjectItemTemplate, e: SettingListEvent<IBoolObjectDataItem>): void {
1593
if (template.context) {
1594
const widget = template.objectCheckboxWidget!;
1595
const defaultValue: Record<string, unknown> = typeof template.context.defaultValue === 'object'
1596
? template.context.defaultValue ?? {}
1597
: {};
1598
1599
const scopeValue: Record<string, unknown> = typeof template.context.scopeValue === 'object'
1600
? template.context.scopeValue ?? {}
1601
: {};
1602
1603
const newValue: Record<string, unknown> = { ...template.context.scopeValue }; // Initialize with scoped values as removed default values are not rendered
1604
const newItems: IBoolObjectDataItem[] = [];
1605
1606
if (e.type !== 'change') {
1607
console.warn('Unexpected event type', e.type, 'for bool object setting', template.context.setting.key);
1608
return;
1609
}
1610
1611
widget.items.forEach((item, idx) => {
1612
// Item was updated
1613
if (e.targetIndex === idx) {
1614
newValue[e.newItem.key.data] = e.newItem.value.data;
1615
newItems.push(e.newItem);
1616
}
1617
// All remaining items, but skip the one that we just updated
1618
else if (e.newItem.key.data !== item.key.data) {
1619
newValue[item.key.data] = item.value.data;
1620
newItems.push(item);
1621
}
1622
});
1623
1624
Object.entries(newValue).forEach(([key, value]) => {
1625
// value from the scope has changed back to the default
1626
if (scopeValue[key] !== value && defaultValue[key] === value) {
1627
delete newValue[key];
1628
}
1629
});
1630
1631
const newObject = Object.keys(newValue).length === 0 ? undefined : newValue;
1632
template.objectCheckboxWidget!.setValue(newItems);
1633
template.onChange?.(newObject);
1634
1635
// Focus this setting explicitly, in case we were previously
1636
// focused on another setting and clicked a checkbox/value container
1637
// for this setting.
1638
this._onDidFocusSetting.fire(template.context);
1639
}
1640
}
1641
1642
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingObjectItemTemplate, onChange: (value: Record<string, unknown> | undefined) => void): void {
1643
const items = getBoolObjectDisplayValue(dataElement);
1644
const { key } = dataElement.setting;
1645
1646
template.objectCheckboxWidget!.setValue(items, {
1647
settingKey: key
1648
});
1649
1650
template.context = dataElement;
1651
template.onChange = (v: Record<string, unknown> | undefined) => {
1652
onChange(v);
1653
};
1654
}
1655
}
1656
1657
abstract class SettingIncludeExcludeRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingIncludeExcludeItemTemplate> {
1658
1659
protected abstract isExclude(): boolean;
1660
1661
renderTemplate(container: HTMLElement): ISettingIncludeExcludeItemTemplate {
1662
const common = this.renderCommonTemplate(null, container, 'list');
1663
1664
const includeExcludeWidget = this._instantiationService.createInstance(this.isExclude() ? ExcludeSettingWidget : IncludeSettingWidget, common.controlElement);
1665
includeExcludeWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS);
1666
common.toDispose.add(includeExcludeWidget);
1667
1668
const template: ISettingIncludeExcludeItemTemplate = {
1669
...common,
1670
includeExcludeWidget
1671
};
1672
1673
this.addSettingElementFocusHandler(template);
1674
1675
common.toDispose.add(includeExcludeWidget.onDidChangeList(e => this.onDidChangeIncludeExclude(template, e)));
1676
1677
return template;
1678
}
1679
1680
private onDidChangeIncludeExclude(template: ISettingIncludeExcludeItemTemplate, e: SettingListEvent<IListDataItem>): void {
1681
if (template.context) {
1682
const newValue = { ...template.context.scopeValue };
1683
1684
// first delete the existing entry, if present
1685
if (e.type !== 'add') {
1686
if (e.originalItem.value.data.toString() in template.context.defaultValue) {
1687
// delete a default by overriding it
1688
newValue[e.originalItem.value.data.toString()] = false;
1689
} else {
1690
delete newValue[e.originalItem.value.data.toString()];
1691
}
1692
}
1693
1694
// then add the new or updated entry, if present
1695
if (e.type === 'change' || e.type === 'add' || e.type === 'move') {
1696
if (e.newItem.value.data.toString() in template.context.defaultValue && !e.newItem.sibling) {
1697
// add a default by deleting its override
1698
delete newValue[e.newItem.value.data.toString()];
1699
} else {
1700
newValue[e.newItem.value.data.toString()] = e.newItem.sibling ? { when: e.newItem.sibling } : true;
1701
}
1702
}
1703
1704
function sortKeys<T extends object>(obj: T) {
1705
const sortedKeys = Object.keys(obj)
1706
.sort((a, b) => a.localeCompare(b)) as Array<keyof T>;
1707
1708
const retVal: Partial<T> = {};
1709
for (const key of sortedKeys) {
1710
retVal[key] = obj[key];
1711
}
1712
return retVal;
1713
}
1714
1715
this._onDidChangeSetting.fire({
1716
key: template.context.setting.key,
1717
value: Object.keys(newValue).length === 0 ? undefined : sortKeys(newValue),
1718
type: template.context.valueType,
1719
manualReset: false,
1720
scope: template.context.setting.scope
1721
});
1722
}
1723
}
1724
1725
renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingIncludeExcludeItemTemplate): void {
1726
super.renderSettingElement(element, index, templateData);
1727
}
1728
1729
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingIncludeExcludeItemTemplate, onChange: (value: string) => void): void {
1730
const value = getIncludeExcludeDisplayValue(dataElement);
1731
template.includeExcludeWidget.setValue(value, { isReadOnly: dataElement.hasPolicyValue });
1732
template.context = dataElement;
1733
template.elementDisposables.add(toDisposable(() => {
1734
template.includeExcludeWidget.cancelEdit();
1735
}));
1736
}
1737
}
1738
1739
class SettingExcludeRenderer extends SettingIncludeExcludeRenderer {
1740
templateId = SETTINGS_EXCLUDE_TEMPLATE_ID;
1741
1742
protected override isExclude(): boolean {
1743
return true;
1744
}
1745
}
1746
1747
class SettingIncludeRenderer extends SettingIncludeExcludeRenderer {
1748
templateId = SETTINGS_INCLUDE_TEMPLATE_ID;
1749
1750
protected override isExclude(): boolean {
1751
return false;
1752
}
1753
}
1754
1755
const settingsInputBoxStyles = getInputBoxStyle({
1756
inputBackground: settingsTextInputBackground,
1757
inputForeground: settingsTextInputForeground,
1758
inputBorder: settingsTextInputBorder
1759
});
1760
1761
abstract class AbstractSettingTextRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingTextItemTemplate> {
1762
private readonly MULTILINE_MAX_HEIGHT = 150;
1763
1764
renderTemplate(_container: HTMLElement, useMultiline?: boolean): ISettingTextItemTemplate {
1765
const common = this.renderCommonTemplate(null, _container, 'text');
1766
const validationErrorMessageElement = DOM.append(common.containerElement, $('.setting-item-validation-message'));
1767
1768
const inputBoxOptions: IInputOptions = {
1769
flexibleHeight: useMultiline,
1770
flexibleWidth: false,
1771
flexibleMaxHeight: this.MULTILINE_MAX_HEIGHT,
1772
inputBoxStyles: settingsInputBoxStyles
1773
};
1774
const inputBox = new InputBox(common.controlElement, this._contextViewService, inputBoxOptions);
1775
common.toDispose.add(inputBox);
1776
common.toDispose.add(
1777
inputBox.onDidChange(e => {
1778
template.onChange?.(e);
1779
}));
1780
common.toDispose.add(inputBox);
1781
inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS);
1782
inputBox.inputElement.tabIndex = 0;
1783
1784
const template: ISettingTextItemTemplate = {
1785
...common,
1786
inputBox,
1787
validationErrorMessageElement
1788
};
1789
1790
this.addSettingElementFocusHandler(template);
1791
1792
return template;
1793
}
1794
1795
renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingTextItemTemplate): void {
1796
super.renderSettingElement(element, index, templateData);
1797
}
1798
1799
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, onChange: (value: string) => void): void {
1800
template.onChange = undefined;
1801
template.inputBox.value = dataElement.value;
1802
template.inputBox.setEnabled(!dataElement.hasPolicyValue);
1803
template.inputBox.setAriaLabel(dataElement.setting.key);
1804
template.onChange = value => {
1805
if (!renderValidations(dataElement, template, false)) {
1806
onChange(value);
1807
}
1808
};
1809
1810
renderValidations(dataElement, template, true);
1811
}
1812
}
1813
1814
class SettingTextRenderer extends AbstractSettingTextRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingTextItemTemplate> {
1815
templateId = SETTINGS_TEXT_TEMPLATE_ID;
1816
1817
override renderTemplate(_container: HTMLElement): ISettingTextItemTemplate {
1818
const template = super.renderTemplate(_container, false);
1819
1820
// TODO@9at8: listWidget filters out all key events from input boxes, so we need to come up with a better way
1821
// Disable ArrowUp and ArrowDown behaviour in favor of list navigation
1822
template.toDispose.add(DOM.addStandardDisposableListener(template.inputBox.inputElement, DOM.EventType.KEY_DOWN, e => {
1823
if (e.equals(KeyCode.UpArrow) || e.equals(KeyCode.DownArrow)) {
1824
e.preventDefault();
1825
}
1826
}));
1827
1828
return template;
1829
}
1830
}
1831
1832
class SettingMultilineTextRenderer extends AbstractSettingTextRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingTextItemTemplate> {
1833
templateId = SETTINGS_MULTILINE_TEXT_TEMPLATE_ID;
1834
1835
override renderTemplate(_container: HTMLElement): ISettingTextItemTemplate {
1836
return super.renderTemplate(_container, true);
1837
}
1838
1839
protected override renderValue(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, onChange: (value: string) => void) {
1840
const onChangeOverride = (value: string) => {
1841
// Ensure the model is up to date since a different value will be rendered as different height when probing the height.
1842
dataElement.value = value;
1843
onChange(value);
1844
};
1845
super.renderValue(dataElement, template, onChangeOverride);
1846
template.elementDisposables.add(
1847
template.inputBox.onDidHeightChange(e => {
1848
const height = template.containerElement.clientHeight;
1849
// Don't fire event if height is reported as 0,
1850
// which sometimes happens when clicking onto a new setting.
1851
if (height) {
1852
this._onDidChangeSettingHeight.fire({
1853
element: dataElement,
1854
height: template.containerElement.clientHeight
1855
});
1856
}
1857
})
1858
);
1859
template.inputBox.layout();
1860
}
1861
}
1862
1863
class SettingEnumRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingEnumItemTemplate> {
1864
templateId = SETTINGS_ENUM_TEMPLATE_ID;
1865
1866
renderTemplate(container: HTMLElement): ISettingEnumItemTemplate {
1867
const common = this.renderCommonTemplate(null, container, 'enum');
1868
1869
const styles = getSelectBoxStyles({
1870
selectBackground: settingsSelectBackground,
1871
selectForeground: settingsSelectForeground,
1872
selectBorder: settingsSelectBorder,
1873
selectListBorder: settingsSelectListBorder
1874
});
1875
1876
const selectBox = new SelectBox([], 0, this._contextViewService, styles, {
1877
useCustomDrawn: !hasNativeContextMenu(this._configService) || !(isIOS && BrowserFeatures.pointerEvents)
1878
});
1879
1880
common.toDispose.add(selectBox);
1881
selectBox.render(common.controlElement);
1882
// eslint-disable-next-line no-restricted-syntax
1883
const selectElement = common.controlElement.querySelector('select');
1884
if (selectElement) {
1885
selectElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS);
1886
selectElement.tabIndex = 0;
1887
}
1888
1889
common.toDispose.add(
1890
selectBox.onDidSelect(e => {
1891
template.onChange?.(e.index);
1892
}));
1893
1894
const enumDescriptionElement = common.containerElement.insertBefore($('.setting-item-enumDescription'), common.descriptionElement.nextSibling);
1895
1896
const template: ISettingEnumItemTemplate = {
1897
...common,
1898
selectBox,
1899
selectElement,
1900
enumDescriptionElement
1901
};
1902
1903
this.addSettingElementFocusHandler(template);
1904
1905
return template;
1906
}
1907
1908
renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingEnumItemTemplate): void {
1909
super.renderSettingElement(element, index, templateData);
1910
}
1911
1912
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingEnumItemTemplate, onChange: (value: string) => void): void {
1913
// Make shallow copies here so that we don't modify the actual dataElement later
1914
const enumItemLabels = dataElement.setting.enumItemLabels ? [...dataElement.setting.enumItemLabels] : [];
1915
const enumDescriptions = dataElement.setting.enumDescriptions ? [...dataElement.setting.enumDescriptions] : [];
1916
const settingEnum = [...dataElement.setting.enum!];
1917
const enumDescriptionsAreMarkdown = dataElement.setting.enumDescriptionsAreMarkdown;
1918
1919
const disposables = new DisposableStore();
1920
template.elementDisposables.add(disposables);
1921
1922
let createdDefault = false;
1923
if (!settingEnum.includes(dataElement.defaultValue)) {
1924
// Add a new potentially blank default setting
1925
settingEnum.unshift(dataElement.defaultValue);
1926
enumDescriptions.unshift('');
1927
enumItemLabels.unshift('');
1928
createdDefault = true;
1929
}
1930
1931
// Use String constructor in case of null or undefined values
1932
const stringifiedDefaultValue = escapeInvisibleChars(String(dataElement.defaultValue));
1933
const displayOptions: ISelectOptionItem[] = settingEnum
1934
.map(String)
1935
.map(escapeInvisibleChars)
1936
.map((data, index) => {
1937
const description = (enumDescriptions[index] && (enumDescriptionsAreMarkdown ? fixSettingLinks(enumDescriptions[index], false) : enumDescriptions[index]));
1938
return {
1939
text: enumItemLabels[index] ? enumItemLabels[index] : data,
1940
detail: enumItemLabels[index] ? data : '',
1941
description,
1942
descriptionIsMarkdown: enumDescriptionsAreMarkdown,
1943
descriptionMarkdownActionHandler: (content) => {
1944
this._openerService.open(content).catch(onUnexpectedError);
1945
},
1946
decoratorRight: (((data === stringifiedDefaultValue) || (createdDefault && index === 0)) ? localize('settings.Default', "default") : '')
1947
} satisfies ISelectOptionItem;
1948
});
1949
1950
template.selectBox.setOptions(displayOptions);
1951
template.selectBox.setAriaLabel(dataElement.setting.key);
1952
template.selectBox.setEnabled(!dataElement.hasPolicyValue);
1953
1954
let idx = settingEnum.indexOf(dataElement.value);
1955
if (idx === -1) {
1956
idx = 0;
1957
}
1958
1959
template.onChange = undefined;
1960
template.selectBox.select(idx);
1961
template.onChange = (idx) => {
1962
if (createdDefault && idx === 0) {
1963
onChange(dataElement.defaultValue);
1964
} else {
1965
onChange(settingEnum[idx]);
1966
}
1967
};
1968
1969
template.enumDescriptionElement.innerText = '';
1970
}
1971
}
1972
1973
const settingsNumberInputBoxStyles = getInputBoxStyle({
1974
inputBackground: settingsNumberInputBackground,
1975
inputForeground: settingsNumberInputForeground,
1976
inputBorder: settingsNumberInputBorder
1977
});
1978
1979
class SettingNumberRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingNumberItemTemplate> {
1980
templateId = SETTINGS_NUMBER_TEMPLATE_ID;
1981
1982
renderTemplate(_container: HTMLElement): ISettingNumberItemTemplate {
1983
const common = super.renderCommonTemplate(null, _container, 'number');
1984
const validationErrorMessageElement = DOM.append(common.containerElement, $('.setting-item-validation-message'));
1985
1986
const inputBox = new InputBox(common.controlElement, this._contextViewService, { type: 'number', inputBoxStyles: settingsNumberInputBoxStyles });
1987
common.toDispose.add(inputBox);
1988
common.toDispose.add(
1989
inputBox.onDidChange(e => {
1990
template.onChange?.(e);
1991
}));
1992
common.toDispose.add(inputBox);
1993
inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS);
1994
inputBox.inputElement.tabIndex = 0;
1995
1996
const template: ISettingNumberItemTemplate = {
1997
...common,
1998
inputBox,
1999
validationErrorMessageElement
2000
};
2001
2002
this.addSettingElementFocusHandler(template);
2003
2004
return template;
2005
}
2006
2007
renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingNumberItemTemplate): void {
2008
super.renderSettingElement(element, index, templateData);
2009
}
2010
2011
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingNumberItemTemplate, onChange: (value: number | null) => void): void {
2012
const numParseFn = (dataElement.valueType === 'integer' || dataElement.valueType === 'nullable-integer')
2013
? parseInt : parseFloat;
2014
2015
const nullNumParseFn = (dataElement.valueType === 'nullable-integer' || dataElement.valueType === 'nullable-number')
2016
? ((v: string) => v === '' ? null : numParseFn(v)) : numParseFn;
2017
2018
template.onChange = undefined;
2019
template.inputBox.value = typeof dataElement.value === 'number' ?
2020
dataElement.value.toString() : '';
2021
template.inputBox.step = dataElement.valueType.includes('integer') ? '1' : 'any';
2022
template.inputBox.setAriaLabel(dataElement.setting.key);
2023
template.inputBox.setEnabled(!dataElement.hasPolicyValue);
2024
template.onChange = value => {
2025
if (!renderValidations(dataElement, template, false)) {
2026
onChange(nullNumParseFn(value));
2027
}
2028
};
2029
2030
renderValidations(dataElement, template, true);
2031
}
2032
}
2033
2034
class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingBoolItemTemplate> {
2035
templateId = SETTINGS_BOOL_TEMPLATE_ID;
2036
2037
renderTemplate(_container: HTMLElement): ISettingBoolItemTemplate {
2038
_container.classList.add('setting-item');
2039
_container.classList.add('setting-item-bool');
2040
2041
const toDispose = new DisposableStore();
2042
2043
const container = DOM.append(_container, $(AbstractSettingRenderer.CONTENTS_SELECTOR));
2044
container.classList.add('settings-row-inner-container');
2045
2046
const titleElement = DOM.append(container, $('.setting-item-title'));
2047
const categoryElement = DOM.append(titleElement, $('span.setting-item-category'));
2048
const labelElementContainer = DOM.append(titleElement, $('span.setting-item-label'));
2049
const labelElement = toDispose.add(new SimpleIconLabel(labelElementContainer));
2050
const indicatorsLabel = toDispose.add(this._instantiationService.createInstance(SettingsTreeIndicatorsLabel, titleElement));
2051
2052
const descriptionAndValueElement = DOM.append(container, $('.setting-item-value-description'));
2053
const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control'));
2054
const descriptionElement = DOM.append(descriptionAndValueElement, $('.setting-item-description'));
2055
const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator'));
2056
toDispose.add(this._hoverService.setupDelayedHover(modifiedIndicatorElement, {
2057
content: localize('modified', "The setting has been configured in the current scope.")
2058
}));
2059
2060
const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message'));
2061
2062
const checkbox = new Toggle({ icon: Codicon.check, actionClassName: 'setting-value-checkbox', isChecked: true, title: '', ...unthemedToggleStyles });
2063
controlElement.appendChild(checkbox.domNode);
2064
toDispose.add(checkbox);
2065
toDispose.add(checkbox.onChange(() => {
2066
template.onChange!(checkbox.checked);
2067
}));
2068
2069
checkbox.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS);
2070
const toolbarContainer = DOM.append(container, $('.setting-toolbar-container'));
2071
const toolbar = this.renderSettingToolbar(toolbarContainer);
2072
toDispose.add(toolbar);
2073
2074
const template: ISettingBoolItemTemplate = {
2075
toDispose,
2076
elementDisposables: toDispose.add(new DisposableStore()),
2077
2078
containerElement: container,
2079
categoryElement,
2080
labelElement,
2081
controlElement,
2082
checkbox,
2083
descriptionElement,
2084
deprecationWarningElement,
2085
indicatorsLabel,
2086
toolbar
2087
};
2088
2089
this.addSettingElementFocusHandler(template);
2090
2091
// Prevent clicks from being handled by list
2092
toDispose.add(DOM.addDisposableListener(controlElement, 'mousedown', (e: IMouseEvent) => e.stopPropagation()));
2093
toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_ENTER, e => container.classList.add('mouseover')));
2094
toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover')));
2095
2096
return template;
2097
}
2098
2099
renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingBoolItemTemplate): void {
2100
super.renderSettingElement(element, index, templateData);
2101
}
2102
2103
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingBoolItemTemplate, onChange: (value: boolean) => void): void {
2104
template.onChange = undefined;
2105
template.checkbox.checked = dataElement.value;
2106
if (dataElement.hasPolicyValue) {
2107
template.checkbox.disable();
2108
template.descriptionElement.classList.add('disabled');
2109
} else {
2110
template.checkbox.enable();
2111
template.descriptionElement.classList.remove('disabled');
2112
2113
// Need to listen for mouse clicks on description and toggle checkbox - use target ID for safety
2114
// Also have to ignore embedded links - too buried to stop propagation
2115
template.elementDisposables.add(DOM.addDisposableListener(template.descriptionElement, DOM.EventType.MOUSE_DOWN, (e) => {
2116
const targetElement = <HTMLElement>e.target;
2117
2118
// Toggle target checkbox
2119
if (targetElement.tagName.toLowerCase() !== 'a') {
2120
template.checkbox.checked = !template.checkbox.checked;
2121
template.onChange!(template.checkbox.checked);
2122
}
2123
DOM.EventHelper.stop(e);
2124
}));
2125
}
2126
template.checkbox.setTitle(dataElement.setting.key);
2127
template.onChange = onChange;
2128
}
2129
}
2130
2131
type ManageExtensionClickTelemetryClassification = {
2132
extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension the user went to manage.' };
2133
owner: 'rzhao271';
2134
comment: 'Event used to gain insights into when users interact with an extension management setting';
2135
};
2136
2137
class SettingsExtensionToggleRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingExtensionToggleItemTemplate> {
2138
templateId = SETTINGS_EXTENSION_TOGGLE_TEMPLATE_ID;
2139
2140
private readonly _onDidDismissExtensionSetting = this._register(new Emitter<string>());
2141
readonly onDidDismissExtensionSetting = this._onDidDismissExtensionSetting.event;
2142
2143
renderTemplate(_container: HTMLElement): ISettingExtensionToggleItemTemplate {
2144
const common = super.renderCommonTemplate(null, _container, 'extension-toggle');
2145
2146
const actionButton = new Button(common.containerElement, {
2147
title: false,
2148
...defaultButtonStyles
2149
});
2150
actionButton.element.classList.add('setting-item-extension-toggle-button');
2151
actionButton.label = localize('showExtension', "Show Extension");
2152
2153
const dismissButton = new Button(common.containerElement, {
2154
title: false,
2155
secondary: true,
2156
...defaultButtonStyles
2157
});
2158
dismissButton.element.classList.add('setting-item-extension-dismiss-button');
2159
dismissButton.label = localize('dismiss', "Dismiss");
2160
2161
const template: ISettingExtensionToggleItemTemplate = {
2162
...common,
2163
actionButton,
2164
dismissButton
2165
};
2166
2167
this.addSettingElementFocusHandler(template);
2168
2169
return template;
2170
}
2171
2172
renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingExtensionToggleItemTemplate): void {
2173
super.renderSettingElement(element, index, templateData);
2174
}
2175
2176
protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingExtensionToggleItemTemplate, onChange: (_: undefined) => void): void {
2177
template.elementDisposables.clear();
2178
2179
const extensionId = dataElement.setting.displayExtensionId!;
2180
template.elementDisposables.add(template.actionButton.onDidClick(async () => {
2181
this._telemetryService.publicLog2<{ extensionId: String }, ManageExtensionClickTelemetryClassification>('ManageExtensionClick', { extensionId });
2182
this._commandService.executeCommand('extension.open', extensionId);
2183
}));
2184
2185
template.elementDisposables.add(template.dismissButton.onDidClick(async () => {
2186
this._telemetryService.publicLog2<{ extensionId: String }, ManageExtensionClickTelemetryClassification>('DismissExtensionClick', { extensionId });
2187
this._onDidDismissExtensionSetting.fire(extensionId);
2188
}));
2189
}
2190
}
2191
2192
export class SettingTreeRenderers extends Disposable {
2193
readonly onDidClickOverrideElement: Event<ISettingOverrideClickEvent>;
2194
2195
private readonly _onDidChangeSetting = this._register(new Emitter<ISettingChangeEvent>());
2196
readonly onDidChangeSetting: Event<ISettingChangeEvent>;
2197
2198
readonly onDidDismissExtensionSetting: Event<string>;
2199
2200
readonly onDidOpenSettings: Event<string>;
2201
2202
readonly onDidClickSettingLink: Event<ISettingLinkClickEvent>;
2203
2204
readonly onDidFocusSetting: Event<SettingsTreeSettingElement>;
2205
2206
readonly onDidChangeSettingHeight: Event<HeightChangeParams>;
2207
2208
readonly onApplyFilter: Event<string>;
2209
2210
readonly allRenderers: ITreeRenderer<SettingsTreeElement, never, any>[];
2211
2212
private readonly settingActions: IAction[];
2213
2214
constructor(
2215
@IInstantiationService private readonly _instantiationService: IInstantiationService,
2216
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
2217
@IContextViewService private readonly _contextViewService: IContextViewService,
2218
@IUserDataSyncEnablementService private readonly _userDataSyncEnablementService: IUserDataSyncEnablementService,
2219
) {
2220
super();
2221
this.settingActions = [
2222
new Action('settings.resetSetting', localize('resetSettingLabel', "Reset Setting"), undefined, undefined, async context => {
2223
if (context instanceof SettingsTreeSettingElement) {
2224
if (!context.isUntrusted) {
2225
this._onDidChangeSetting.fire({
2226
key: context.setting.key,
2227
value: undefined,
2228
type: context.setting.type as SettingValueType,
2229
manualReset: true,
2230
scope: context.setting.scope
2231
});
2232
}
2233
}
2234
}),
2235
new Separator(),
2236
this._instantiationService.createInstance(CopySettingIdAction),
2237
this._instantiationService.createInstance(CopySettingAsJSONAction),
2238
this._instantiationService.createInstance(CopySettingAsURLAction),
2239
];
2240
2241
const actionFactory = (setting: ISetting, settingTarget: SettingsTarget) => this.getActionsForSetting(setting, settingTarget);
2242
const emptyActionFactory = (_: ISetting) => [];
2243
const extensionRenderer = this._instantiationService.createInstance(SettingsExtensionToggleRenderer, [], emptyActionFactory);
2244
const settingRenderers = [
2245
this._instantiationService.createInstance(SettingBoolRenderer, this.settingActions, actionFactory),
2246
this._instantiationService.createInstance(SettingNumberRenderer, this.settingActions, actionFactory),
2247
this._instantiationService.createInstance(SettingArrayRenderer, this.settingActions, actionFactory),
2248
this._instantiationService.createInstance(SettingComplexRenderer, this.settingActions, actionFactory),
2249
this._instantiationService.createInstance(SettingComplexObjectRenderer, this.settingActions, actionFactory),
2250
this._instantiationService.createInstance(SettingTextRenderer, this.settingActions, actionFactory),
2251
this._instantiationService.createInstance(SettingMultilineTextRenderer, this.settingActions, actionFactory),
2252
this._instantiationService.createInstance(SettingExcludeRenderer, this.settingActions, actionFactory),
2253
this._instantiationService.createInstance(SettingIncludeRenderer, this.settingActions, actionFactory),
2254
this._instantiationService.createInstance(SettingEnumRenderer, this.settingActions, actionFactory),
2255
this._instantiationService.createInstance(SettingObjectRenderer, this.settingActions, actionFactory),
2256
this._instantiationService.createInstance(SettingBoolObjectRenderer, this.settingActions, actionFactory),
2257
extensionRenderer
2258
];
2259
2260
this.onDidClickOverrideElement = Event.any(...settingRenderers.map(r => r.onDidClickOverrideElement));
2261
this.onDidChangeSetting = Event.any(
2262
...settingRenderers.map(r => r.onDidChangeSetting),
2263
this._onDidChangeSetting.event
2264
);
2265
this.onDidDismissExtensionSetting = extensionRenderer.onDidDismissExtensionSetting;
2266
this.onDidOpenSettings = Event.any(...settingRenderers.map(r => r.onDidOpenSettings));
2267
this.onDidClickSettingLink = Event.any(...settingRenderers.map(r => r.onDidClickSettingLink));
2268
this.onDidFocusSetting = Event.any(...settingRenderers.map(r => r.onDidFocusSetting));
2269
this.onDidChangeSettingHeight = Event.any(...settingRenderers.map(r => r.onDidChangeSettingHeight));
2270
this.onApplyFilter = Event.any(...settingRenderers.map(r => r.onApplyFilter));
2271
2272
this.allRenderers = [
2273
...settingRenderers,
2274
this._instantiationService.createInstance(SettingGroupRenderer),
2275
this._instantiationService.createInstance(SettingNewExtensionsRenderer),
2276
];
2277
}
2278
2279
private getActionsForSetting(setting: ISetting, settingTarget: SettingsTarget): IAction[] {
2280
const actions: IAction[] = [];
2281
if (!(setting.scope && APPLICATION_SCOPES.includes(setting.scope)) && settingTarget === ConfigurationTarget.USER_LOCAL) {
2282
actions.push(this._instantiationService.createInstance(ApplySettingToAllProfilesAction, setting));
2283
}
2284
if (this._userDataSyncEnablementService.isEnabled() && !setting.disallowSyncIgnore) {
2285
actions.push(this._instantiationService.createInstance(SyncSettingAction, setting));
2286
}
2287
if (actions.length) {
2288
actions.splice(0, 0, new Separator());
2289
}
2290
return actions;
2291
}
2292
2293
cancelSuggesters() {
2294
this._contextViewService.hideContextView();
2295
}
2296
2297
showContextMenu(element: SettingsTreeSettingElement, settingDOMElement: HTMLElement): void {
2298
// eslint-disable-next-line no-restricted-syntax
2299
const toolbarElement = settingDOMElement.querySelector('.monaco-toolbar');
2300
if (toolbarElement) {
2301
this._contextMenuService.showContextMenu({
2302
getActions: () => this.settingActions,
2303
getAnchor: () => <HTMLElement>toolbarElement,
2304
getActionsContext: () => element
2305
});
2306
}
2307
}
2308
2309
getSettingDOMElementForDOMElement(domElement: HTMLElement): HTMLElement | null {
2310
const parent = DOM.findParentWithClass(domElement, AbstractSettingRenderer.CONTENTS_CLASS);
2311
if (parent) {
2312
return parent;
2313
}
2314
2315
return null;
2316
}
2317
2318
getDOMElementsForSettingKey(treeContainer: HTMLElement, key: string): NodeListOf<HTMLElement> {
2319
// eslint-disable-next-line no-restricted-syntax
2320
return treeContainer.querySelectorAll(`[${AbstractSettingRenderer.SETTING_KEY_ATTR}="${key}"]`);
2321
}
2322
2323
getKeyForDOMElementInSetting(element: HTMLElement): string | null {
2324
const settingElement = this.getSettingDOMElementForDOMElement(element);
2325
return settingElement && settingElement.getAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR);
2326
}
2327
2328
getIdForDOMElementInSetting(element: HTMLElement): string | null {
2329
const settingElement = this.getSettingDOMElementForDOMElement(element);
2330
return settingElement && settingElement.getAttribute(AbstractSettingRenderer.SETTING_ID_ATTR);
2331
}
2332
2333
override dispose(): void {
2334
super.dispose();
2335
this.settingActions.forEach(action => {
2336
if (isDisposable(action)) {
2337
action.dispose();
2338
}
2339
});
2340
this.allRenderers.forEach(renderer => {
2341
if (isDisposable(renderer)) {
2342
renderer.dispose();
2343
}
2344
});
2345
}
2346
}
2347
2348
/**
2349
* Validate and render any error message. Returns true if the value is invalid.
2350
*/
2351
function renderValidations(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, calledOnStartup: boolean): boolean {
2352
if (dataElement.setting.validator) {
2353
const errMsg = dataElement.setting.validator(template.inputBox.value);
2354
if (errMsg) {
2355
template.containerElement.classList.add('invalid-input');
2356
template.validationErrorMessageElement.innerText = errMsg;
2357
const validationError = localize('validationError', "Validation Error.");
2358
template.inputBox.inputElement.parentElement!.setAttribute('aria-label', [validationError, errMsg].join(' '));
2359
if (!calledOnStartup) { aria.status(validationError + ' ' + errMsg); }
2360
return true;
2361
} else {
2362
template.inputBox.inputElement.parentElement!.removeAttribute('aria-label');
2363
}
2364
}
2365
template.containerElement.classList.remove('invalid-input');
2366
return false;
2367
}
2368
2369
/**
2370
* Validate and render any error message for arrays. Returns true if the value is invalid.
2371
*/
2372
function renderArrayValidations(
2373
dataElement: SettingsTreeSettingElement,
2374
template: ISettingListItemTemplate | ISettingObjectItemTemplate,
2375
value: string[] | Record<string, unknown> | undefined,
2376
calledOnStartup: boolean
2377
): boolean {
2378
template.containerElement.classList.add('invalid-input');
2379
if (dataElement.setting.validator) {
2380
const errMsg = dataElement.setting.validator(value);
2381
if (errMsg && errMsg !== '') {
2382
template.containerElement.classList.add('invalid-input');
2383
template.validationErrorMessageElement.innerText = errMsg;
2384
const validationError = localize('validationError', "Validation Error.");
2385
template.containerElement.setAttribute('aria-label', [dataElement.setting.key, validationError, errMsg].join(' '));
2386
if (!calledOnStartup) { aria.status(validationError + ' ' + errMsg); }
2387
return true;
2388
} else {
2389
template.containerElement.setAttribute('aria-label', dataElement.setting.key);
2390
template.containerElement.classList.remove('invalid-input');
2391
}
2392
}
2393
return false;
2394
}
2395
2396
function cleanRenderedMarkdown(element: Node): void {
2397
for (let i = 0; i < element.childNodes.length; i++) {
2398
const child = element.childNodes.item(i);
2399
2400
const tagName = (<Element>child).tagName && (<Element>child).tagName.toLowerCase();
2401
if (tagName === 'img') {
2402
child.remove();
2403
} else {
2404
cleanRenderedMarkdown(child);
2405
}
2406
}
2407
}
2408
2409
function fixSettingLinks(text: string, linkify = true): string {
2410
return text.replace(/`#([^#\s`]+)#`|'#([^#\s']+)#'/g, (match, backticksGroup, quotesGroup) => {
2411
const settingKey: string = backticksGroup ?? quotesGroup;
2412
const targetDisplayFormat = settingKeyToDisplayFormat(settingKey);
2413
const targetName = `${targetDisplayFormat.category}: ${targetDisplayFormat.label}`;
2414
return linkify ?
2415
`[${targetName}](#${settingKey} "${settingKey}")` :
2416
`"${targetName}"`;
2417
});
2418
}
2419
2420
function escapeInvisibleChars(enumValue: string): string {
2421
return enumValue && enumValue
2422
.replace(/\n/g, '\\n')
2423
.replace(/\r/g, '\\r');
2424
}
2425
2426
2427
export class SettingsTreeFilter implements ITreeFilter<SettingsTreeElement> {
2428
constructor(
2429
private viewState: ISettingsEditorViewState,
2430
private isFilteringGroups: boolean,
2431
@IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService,
2432
) { }
2433
2434
filter(element: SettingsTreeElement, parentVisibility: TreeVisibility): TreeFilterResult<void> {
2435
// Filter during search
2436
if (this.viewState.categoryFilter && element instanceof SettingsTreeSettingElement) {
2437
if (!this.settingContainedInGroup(element.setting, this.viewState.categoryFilter)) {
2438
return false;
2439
}
2440
}
2441
2442
// Non-user scope selected
2443
if (element instanceof SettingsTreeSettingElement && this.viewState.settingsTarget !== ConfigurationTarget.USER_LOCAL) {
2444
const isRemote = !!this.environmentService.remoteAuthority;
2445
if (!element.matchesScope(this.viewState.settingsTarget, isRemote)) {
2446
return false;
2447
}
2448
}
2449
2450
// Group with no visible children
2451
if (element instanceof SettingsTreeGroupElement) {
2452
// When filtering to a specific category, only show that category and its descendants
2453
if (this.isFilteringGroups && this.viewState.categoryFilter) {
2454
if (!this.groupIsRelatedToCategory(element, this.viewState.categoryFilter)) {
2455
return false;
2456
}
2457
// For groups related to the category, skip the count check and recurse
2458
// to let child settings be filtered
2459
return TreeVisibility.Recurse;
2460
}
2461
2462
if (typeof element.count === 'number') {
2463
return element.count > 0;
2464
}
2465
2466
return TreeVisibility.Recurse;
2467
}
2468
2469
// Filtered "new extensions" button
2470
if (element instanceof SettingsTreeNewExtensionsElement) {
2471
if (this.viewState.tagFilters?.size || this.viewState.categoryFilter) {
2472
return false;
2473
}
2474
}
2475
2476
return true;
2477
}
2478
2479
private settingContainedInGroup(setting: ISetting, group: SettingsTreeGroupElement): boolean {
2480
return group.children.some(child => {
2481
if (child instanceof SettingsTreeGroupElement) {
2482
return this.settingContainedInGroup(setting, child);
2483
} else if (child instanceof SettingsTreeSettingElement) {
2484
return child.setting.key === setting.key;
2485
} else {
2486
return false;
2487
}
2488
});
2489
}
2490
2491
/**
2492
* Checks if a group is related to the filtered category.
2493
* A group is related if it's the category itself, a descendant of it, or an ancestor of it.
2494
*/
2495
private groupIsRelatedToCategory(group: SettingsTreeGroupElement, category: SettingsTreeGroupElement): boolean {
2496
// Check if this group is the category itself
2497
if (group.id === category.id) {
2498
return true;
2499
}
2500
2501
// Check if this group is a descendant of the category
2502
let parent = group.parent;
2503
while (parent) {
2504
if (parent.id === category.id) {
2505
return true;
2506
}
2507
parent = parent.parent;
2508
}
2509
2510
// Check if this group is an ancestor of the category
2511
let categoryParent = category.parent;
2512
while (categoryParent) {
2513
if (categoryParent.id === group.id) {
2514
return true;
2515
}
2516
categoryParent = categoryParent.parent;
2517
}
2518
2519
return false;
2520
}
2521
}
2522
2523
class SettingsTreeDelegate extends CachedListVirtualDelegate<SettingsTreeGroupChild> {
2524
2525
getTemplateId(element: SettingsTreeGroupElement | SettingsTreeSettingElement | SettingsTreeNewExtensionsElement): string {
2526
if (element instanceof SettingsTreeGroupElement) {
2527
return SETTINGS_ELEMENT_TEMPLATE_ID;
2528
}
2529
2530
if (element instanceof SettingsTreeSettingElement) {
2531
if (element.valueType === SettingValueType.ExtensionToggle) {
2532
return SETTINGS_EXTENSION_TOGGLE_TEMPLATE_ID;
2533
}
2534
2535
const invalidTypeError = element.isConfigured && getInvalidTypeError(element.value, element.setting.type);
2536
if (invalidTypeError) {
2537
return SETTINGS_COMPLEX_TEMPLATE_ID;
2538
}
2539
2540
if (element.valueType === SettingValueType.Boolean) {
2541
return SETTINGS_BOOL_TEMPLATE_ID;
2542
}
2543
2544
if (element.valueType === SettingValueType.Integer ||
2545
element.valueType === SettingValueType.Number ||
2546
element.valueType === SettingValueType.NullableInteger ||
2547
element.valueType === SettingValueType.NullableNumber) {
2548
return SETTINGS_NUMBER_TEMPLATE_ID;
2549
}
2550
2551
if (element.valueType === SettingValueType.MultilineString) {
2552
return SETTINGS_MULTILINE_TEXT_TEMPLATE_ID;
2553
}
2554
2555
if (element.valueType === SettingValueType.String) {
2556
return SETTINGS_TEXT_TEMPLATE_ID;
2557
}
2558
2559
if (element.valueType === SettingValueType.Enum) {
2560
return SETTINGS_ENUM_TEMPLATE_ID;
2561
}
2562
2563
if (element.valueType === SettingValueType.Array) {
2564
return SETTINGS_ARRAY_TEMPLATE_ID;
2565
}
2566
2567
if (element.valueType === SettingValueType.Exclude) {
2568
return SETTINGS_EXCLUDE_TEMPLATE_ID;
2569
}
2570
2571
if (element.valueType === SettingValueType.Include) {
2572
return SETTINGS_INCLUDE_TEMPLATE_ID;
2573
}
2574
2575
if (element.valueType === SettingValueType.Object) {
2576
return SETTINGS_OBJECT_TEMPLATE_ID;
2577
}
2578
2579
if (element.valueType === SettingValueType.BooleanObject) {
2580
return SETTINGS_BOOL_OBJECT_TEMPLATE_ID;
2581
}
2582
2583
if (element.valueType === SettingValueType.ComplexObject) {
2584
return SETTINGS_COMPLEX_OBJECT_TEMPLATE_ID;
2585
}
2586
2587
if (element.valueType === SettingValueType.LanguageTag) {
2588
return SETTINGS_COMPLEX_TEMPLATE_ID;
2589
}
2590
2591
return SETTINGS_COMPLEX_TEMPLATE_ID;
2592
}
2593
2594
if (element instanceof SettingsTreeNewExtensionsElement) {
2595
return SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID;
2596
}
2597
2598
throw new Error('unknown element type: ' + element);
2599
}
2600
2601
hasDynamicHeight(element: SettingsTreeGroupElement | SettingsTreeSettingElement | SettingsTreeNewExtensionsElement): boolean {
2602
return !(element instanceof SettingsTreeGroupElement);
2603
}
2604
2605
protected estimateHeight(element: SettingsTreeGroupChild): number {
2606
if (element instanceof SettingsTreeGroupElement) {
2607
return 42;
2608
}
2609
2610
return element instanceof SettingsTreeSettingElement && element.valueType === SettingValueType.Boolean ? 78 : 104;
2611
}
2612
}
2613
2614
export class NonCollapsibleObjectTreeModel<T> extends ObjectTreeModel<T> {
2615
override isCollapsible(element: T): boolean {
2616
return false;
2617
}
2618
2619
override setCollapsed(element: T, collapsed?: boolean, recursive?: boolean): boolean {
2620
return false;
2621
}
2622
}
2623
2624
class SettingsTreeAccessibilityProvider implements IListAccessibilityProvider<SettingsTreeElement> {
2625
constructor(private readonly configurationService: IWorkbenchConfigurationService, private readonly languageService: ILanguageService, private readonly userDataProfilesService: IUserDataProfilesService) {
2626
}
2627
2628
getAriaLabel(element: SettingsTreeElement) {
2629
if (element instanceof SettingsTreeSettingElement) {
2630
const ariaLabelSections: string[] = [];
2631
ariaLabelSections.push(`${element.displayCategory} ${element.displayLabel}.`);
2632
2633
if (element.isConfigured) {
2634
const modifiedText = localize('settings.Modified', 'Modified.');
2635
ariaLabelSections.push(modifiedText);
2636
}
2637
2638
const indicatorsLabelAriaLabel = getIndicatorsLabelAriaLabel(element, this.configurationService, this.userDataProfilesService, this.languageService);
2639
if (indicatorsLabelAriaLabel.length) {
2640
ariaLabelSections.push(`${indicatorsLabelAriaLabel}.`);
2641
}
2642
2643
const descriptionWithoutSettingLinks = renderAsPlaintext({ value: fixSettingLinks(element.description, false) });
2644
if (descriptionWithoutSettingLinks.length) {
2645
ariaLabelSections.push(descriptionWithoutSettingLinks);
2646
}
2647
return ariaLabelSections.join(' ');
2648
} else if (element instanceof SettingsTreeGroupElement) {
2649
return element.label;
2650
} else {
2651
return element.id;
2652
}
2653
}
2654
2655
getWidgetAriaLabel() {
2656
return localize('settings', "Settings");
2657
}
2658
}
2659
2660
export class SettingsTree extends WorkbenchObjectTree<SettingsTreeElement> {
2661
constructor(
2662
container: HTMLElement,
2663
viewState: ISettingsEditorViewState,
2664
renderers: ITreeRenderer<any, void, any>[],
2665
@IContextKeyService contextKeyService: IContextKeyService,
2666
@IListService listService: IListService,
2667
@IWorkbenchConfigurationService configurationService: IWorkbenchConfigurationService,
2668
@IInstantiationService instantiationService: IInstantiationService,
2669
@ILanguageService languageService: ILanguageService,
2670
@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService
2671
) {
2672
super('SettingsTree', container,
2673
new SettingsTreeDelegate(),
2674
renderers,
2675
{
2676
horizontalScrolling: false,
2677
supportDynamicHeights: true,
2678
scrollToActiveElement: true,
2679
identityProvider: {
2680
getId(e) {
2681
return e.id;
2682
}
2683
},
2684
accessibilityProvider: new SettingsTreeAccessibilityProvider(configurationService, languageService, userDataProfilesService),
2685
styleController: id => new DefaultStyleController(domStylesheetsJs.createStyleSheet(container), id),
2686
filter: instantiationService.createInstance(SettingsTreeFilter, viewState, true),
2687
smoothScrolling: configurationService.getValue<boolean>('workbench.list.smoothScrolling'),
2688
multipleSelectionSupport: false,
2689
findWidgetEnabled: false,
2690
renderIndentGuides: RenderIndentGuides.None,
2691
transformOptimization: false // Disable transform optimization #177470
2692
},
2693
instantiationService,
2694
contextKeyService,
2695
listService,
2696
configurationService,
2697
);
2698
2699
this.getHTMLElement().classList.add('settings-editor-tree');
2700
2701
this.style(getListStyles({
2702
listBackground: editorBackground,
2703
listActiveSelectionBackground: editorBackground,
2704
listActiveSelectionForeground: foreground,
2705
listFocusAndSelectionBackground: editorBackground,
2706
listFocusAndSelectionForeground: foreground,
2707
listFocusBackground: editorBackground,
2708
listFocusForeground: foreground,
2709
listHoverForeground: foreground,
2710
listHoverBackground: editorBackground,
2711
listHoverOutline: editorBackground,
2712
listFocusOutline: editorBackground,
2713
listInactiveSelectionBackground: editorBackground,
2714
listInactiveSelectionForeground: foreground,
2715
listInactiveFocusBackground: editorBackground,
2716
listInactiveFocusOutline: editorBackground,
2717
treeIndentGuidesStroke: undefined,
2718
treeInactiveIndentGuidesStroke: undefined,
2719
}));
2720
2721
this.disposables.add(configurationService.onDidChangeConfiguration(e => {
2722
if (e.affectsConfiguration('workbench.list.smoothScrolling')) {
2723
this.updateOptions({
2724
smoothScrolling: configurationService.getValue<boolean>('workbench.list.smoothScrolling')
2725
});
2726
}
2727
}));
2728
}
2729
2730
protected override createModel(user: string, options: IObjectTreeOptions<SettingsTreeElement | null, void>): ITreeModel<SettingsTreeGroupChild | null, void, SettingsTreeGroupChild | null> {
2731
return new NonCollapsibleObjectTreeModel<SettingsTreeGroupChild>(user, options);
2732
}
2733
}
2734
2735
class CopySettingIdAction extends Action {
2736
static readonly ID = 'settings.copySettingId';
2737
static readonly LABEL = localize('copySettingIdLabel', "Copy Setting ID");
2738
2739
constructor(
2740
@IClipboardService private readonly clipboardService: IClipboardService
2741
) {
2742
super(CopySettingIdAction.ID, CopySettingIdAction.LABEL);
2743
}
2744
2745
override async run(context: SettingsTreeSettingElement): Promise<void> {
2746
if (context) {
2747
await this.clipboardService.writeText(context.setting.key);
2748
}
2749
2750
return Promise.resolve(undefined);
2751
}
2752
}
2753
2754
class CopySettingAsJSONAction extends Action {
2755
static readonly ID = 'settings.copySettingAsJSON';
2756
static readonly LABEL = localize('copySettingAsJSONLabel', "Copy Setting as JSON");
2757
2758
constructor(
2759
@IClipboardService private readonly clipboardService: IClipboardService
2760
) {
2761
super(CopySettingAsJSONAction.ID, CopySettingAsJSONAction.LABEL);
2762
}
2763
2764
override async run(context: SettingsTreeSettingElement): Promise<void> {
2765
if (context) {
2766
const jsonResult = `"${context.setting.key}": ${JSON.stringify(context.value, undefined, ' ')}`;
2767
await this.clipboardService.writeText(jsonResult);
2768
}
2769
2770
return Promise.resolve(undefined);
2771
}
2772
}
2773
2774
class CopySettingAsURLAction extends Action {
2775
static readonly ID = 'settings.copySettingAsURL';
2776
static readonly LABEL = localize('copySettingAsURLLabel', "Copy Setting as URL");
2777
2778
constructor(
2779
@IClipboardService private readonly clipboardService: IClipboardService,
2780
@IProductService private readonly productService: IProductService,
2781
) {
2782
super(CopySettingAsURLAction.ID, CopySettingAsURLAction.LABEL);
2783
}
2784
2785
override async run(context: SettingsTreeSettingElement): Promise<void> {
2786
if (context) {
2787
const settingKey = context.setting.key;
2788
const product = this.productService.urlProtocol;
2789
const uri = URI.from({ scheme: product, authority: SETTINGS_AUTHORITY, path: `/${settingKey}` }, true);
2790
await this.clipboardService.writeText(uri.toString());
2791
}
2792
2793
return Promise.resolve(undefined);
2794
}
2795
}
2796
2797
class SyncSettingAction extends Action {
2798
static readonly ID = 'settings.stopSyncingSetting';
2799
static readonly LABEL = localize('stopSyncingSetting', "Sync This Setting");
2800
2801
constructor(
2802
private readonly setting: ISetting,
2803
@IConfigurationService private readonly configService: IConfigurationService,
2804
) {
2805
super(SyncSettingAction.ID, SyncSettingAction.LABEL);
2806
this._register(Event.filter(configService.onDidChangeConfiguration, e => e.affectsConfiguration('settingsSync.ignoredSettings'))(() => this.update()));
2807
this.update();
2808
}
2809
2810
async update() {
2811
const ignoredSettings = getIgnoredSettings(getDefaultIgnoredSettings(), this.configService);
2812
this.checked = !ignoredSettings.includes(this.setting.key);
2813
}
2814
2815
override async run(): Promise<void> {
2816
// first remove the current setting completely from ignored settings
2817
let currentValue = [...this.configService.getValue<string[]>('settingsSync.ignoredSettings')];
2818
currentValue = currentValue.filter(v => v !== this.setting.key && v !== `-${this.setting.key}`);
2819
2820
const defaultIgnoredSettings = getDefaultIgnoredSettings();
2821
const isDefaultIgnored = defaultIgnoredSettings.includes(this.setting.key);
2822
const askedToSync = !this.checked;
2823
2824
// If asked to sync, then add only if it is ignored by default
2825
if (askedToSync && isDefaultIgnored) {
2826
currentValue.push(`-${this.setting.key}`);
2827
}
2828
2829
// If asked not to sync, then add only if it is not ignored by default
2830
if (!askedToSync && !isDefaultIgnored) {
2831
currentValue.push(this.setting.key);
2832
}
2833
2834
this.configService.updateValue('settingsSync.ignoredSettings', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER);
2835
2836
return Promise.resolve(undefined);
2837
}
2838
2839
}
2840
2841
class ApplySettingToAllProfilesAction extends Action {
2842
static readonly ID = 'settings.applyToAllProfiles';
2843
static readonly LABEL = localize('applyToAllProfiles', "Apply Setting to all Profiles");
2844
2845
constructor(
2846
private readonly setting: ISetting,
2847
@IWorkbenchConfigurationService private readonly configService: IWorkbenchConfigurationService,
2848
) {
2849
super(ApplySettingToAllProfilesAction.ID, ApplySettingToAllProfilesAction.LABEL);
2850
this._register(Event.filter(configService.onDidChangeConfiguration, e => e.affectsConfiguration(APPLY_ALL_PROFILES_SETTING))(() => this.update()));
2851
this.update();
2852
}
2853
2854
update() {
2855
const allProfilesSettings = this.configService.getValue<string[]>(APPLY_ALL_PROFILES_SETTING);
2856
this.checked = allProfilesSettings.includes(this.setting.key);
2857
}
2858
2859
override async run(): Promise<void> {
2860
// first remove the current setting completely from ignored settings
2861
const value = this.configService.getValue<string[]>(APPLY_ALL_PROFILES_SETTING) ?? [];
2862
2863
if (this.checked) {
2864
value.splice(value.indexOf(this.setting.key), 1);
2865
} else {
2866
value.push(this.setting.key);
2867
}
2868
2869
const newValue = distinct(value);
2870
if (this.checked) {
2871
await this.configService.updateValue(this.setting.key, this.configService.inspect(this.setting.key).application?.value, ConfigurationTarget.USER_LOCAL);
2872
await this.configService.updateValue(APPLY_ALL_PROFILES_SETTING, newValue.length ? newValue : undefined, ConfigurationTarget.USER_LOCAL);
2873
} else {
2874
await this.configService.updateValue(APPLY_ALL_PROFILES_SETTING, newValue.length ? newValue : undefined, ConfigurationTarget.USER_LOCAL);
2875
await this.configService.updateValue(this.setting.key, this.configService.inspect(this.setting.key).userLocal?.value, ConfigurationTarget.USER_LOCAL);
2876
}
2877
}
2878
2879
}
2880
2881