Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts
5255 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 * as arrays from '../../../../base/common/arrays.js';
7
import { Emitter } from '../../../../base/common/event.js';
8
import { IJSONSchema } from '../../../../base/common/jsonSchema.js';
9
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
10
import { escapeRegExpCharacters, isFalsyOrWhitespace } from '../../../../base/common/strings.js';
11
import { isUndefinedOrNull } from '../../../../base/common/types.js';
12
import { URI } from '../../../../base/common/uri.js';
13
import { ILanguageService } from '../../../../editor/common/languages/language.js';
14
import { ConfigurationTarget, getLanguageTagSettingPlainKey, IConfigurationValue } from '../../../../platform/configuration/common/configuration.js';
15
import { ConfigurationDefaultValueSource, ConfigurationScope, EditPresentationTypes, Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
16
import { IProductService } from '../../../../platform/product/common/productService.js';
17
import { Registry } from '../../../../platform/registry/common/platform.js';
18
import { USER_LOCAL_AND_REMOTE_SETTINGS } from '../../../../platform/request/common/request.js';
19
import { APPLICATION_SCOPES, FOLDER_SCOPES, IWorkbenchConfigurationService, LOCAL_MACHINE_SCOPES, REMOTE_MACHINE_SCOPES, WORKSPACE_SCOPES } from '../../../services/configuration/common/configuration.js';
20
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
21
import { IExtensionSetting, ISearchResult, ISetting, ISettingMatch, SettingMatchType, SettingValueType } from '../../../services/preferences/common/preferences.js';
22
import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';
23
import { ENABLE_EXTENSION_TOGGLE_SETTINGS, ENABLE_LANGUAGE_FILTER, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, compareTwoNullableNumbers, wordifyKey } from '../common/preferences.js';
24
import { SettingsTarget } from './preferencesWidgets.js';
25
import { ITOCEntry, tocData } from './settingsLayout.js';
26
27
export const ONLINE_SERVICES_SETTING_TAG = 'usesOnlineServices';
28
29
export interface ISettingsEditorViewState {
30
settingsTarget: SettingsTarget;
31
query?: string; // used to keep track of loading from setInput vs loading from cache
32
tagFilters?: Set<string>;
33
extensionFilters?: Set<string>;
34
featureFilters?: Set<string>;
35
idFilters?: Set<string>;
36
languageFilter?: string;
37
categoryFilter?: SettingsTreeGroupElement;
38
}
39
40
export abstract class SettingsTreeElement extends Disposable {
41
id: string;
42
parent?: SettingsTreeGroupElement;
43
44
private _tabbable = false;
45
46
private readonly _onDidChangeTabbable = this._register(new Emitter<void>());
47
get onDidChangeTabbable() { return this._onDidChangeTabbable.event; }
48
49
constructor(_id: string) {
50
super();
51
this.id = _id;
52
}
53
54
get tabbable(): boolean {
55
return this._tabbable;
56
}
57
58
set tabbable(value: boolean) {
59
this._tabbable = value;
60
this._onDidChangeTabbable.fire();
61
}
62
}
63
64
export type SettingsTreeGroupChild = (SettingsTreeGroupElement | SettingsTreeSettingElement | SettingsTreeNewExtensionsElement);
65
66
export class SettingsTreeGroupElement extends SettingsTreeElement {
67
count?: number;
68
label: string;
69
level: number;
70
isFirstGroup: boolean;
71
72
private _childSettingKeys: Set<string> = new Set();
73
private _children: SettingsTreeGroupChild[] = [];
74
75
get children(): SettingsTreeGroupChild[] {
76
return this._children;
77
}
78
79
set children(newChildren: SettingsTreeGroupChild[]) {
80
this._children = newChildren;
81
82
this._childSettingKeys = new Set();
83
this._children.forEach(child => {
84
if (child instanceof SettingsTreeSettingElement) {
85
this._childSettingKeys.add(child.setting.key);
86
}
87
});
88
}
89
90
constructor(_id: string, count: number | undefined, label: string, level: number, isFirstGroup: boolean) {
91
super(_id);
92
93
this.count = count;
94
this.label = label;
95
this.level = level;
96
this.isFirstGroup = isFirstGroup;
97
}
98
99
/**
100
* Returns whether this group contains the given child key (to a depth of 1 only)
101
*/
102
containsSetting(key: string): boolean {
103
return this._childSettingKeys.has(key);
104
}
105
}
106
107
export class SettingsTreeNewExtensionsElement extends SettingsTreeElement {
108
constructor(_id: string, public readonly extensionIds: string[]) {
109
super(_id);
110
}
111
}
112
113
export class SettingsTreeSettingElement extends SettingsTreeElement {
114
private static readonly MAX_DESC_LINES = 20;
115
116
setting: ISetting;
117
118
private _displayCategory: string | null = null;
119
private _displayLabel: string | null = null;
120
121
/**
122
* scopeValue || defaultValue, for rendering convenience.
123
*/
124
value: any;
125
126
/**
127
* The value in the current settings scope.
128
*/
129
scopeValue: any;
130
131
/**
132
* The default value
133
*/
134
defaultValue?: any;
135
136
/**
137
* The source of the default value to display.
138
* This value also accounts for extension-contributed language-specific default value overrides.
139
*/
140
defaultValueSource: ConfigurationDefaultValueSource | undefined;
141
142
/**
143
* Whether the setting is configured in the selected scope.
144
*/
145
isConfigured = false;
146
147
/**
148
* Whether the setting requires trusted target
149
*/
150
isUntrusted = false;
151
152
/**
153
* Whether the setting is under a policy that blocks all changes.
154
*/
155
hasPolicyValue = false;
156
157
tags?: Set<string>;
158
overriddenScopeList: string[] = [];
159
overriddenDefaultsLanguageList: string[] = [];
160
161
/**
162
* For each language that contributes setting values or default overrides, we can see those values here.
163
*/
164
languageOverrideValues: Map<string, IConfigurationValue<unknown>> = new Map<string, IConfigurationValue<unknown>>();
165
166
description!: string;
167
valueType!: SettingValueType;
168
169
constructor(
170
setting: ISetting,
171
parent: SettingsTreeGroupElement,
172
readonly settingsTarget: SettingsTarget,
173
private readonly isWorkspaceTrusted: boolean,
174
private readonly languageFilter: string | undefined,
175
private readonly languageService: ILanguageService,
176
private readonly productService: IProductService,
177
private readonly userDataProfileService: IUserDataProfileService,
178
private readonly configurationService: IWorkbenchConfigurationService,
179
) {
180
super(sanitizeId(parent.id + '_' + setting.key));
181
this.setting = setting;
182
this.parent = parent;
183
184
// Make sure description and valueType are initialized
185
this.initSettingDescription();
186
this.initSettingValueType();
187
}
188
189
get displayCategory(): string {
190
if (!this._displayCategory) {
191
this.initLabels();
192
}
193
194
return this._displayCategory!;
195
}
196
197
get displayLabel(): string {
198
if (!this._displayLabel) {
199
this.initLabels();
200
}
201
202
return this._displayLabel!;
203
}
204
205
private initLabels(): void {
206
if (this.setting.title) {
207
this._displayLabel = this.setting.title;
208
this._displayCategory = this.setting.categoryLabel ?? null;
209
return;
210
}
211
const displayKeyFormat = settingKeyToDisplayFormat(this.setting.key, this.parent!.id, this.setting.isLanguageTagSetting);
212
this._displayLabel = displayKeyFormat.label;
213
this._displayCategory = displayKeyFormat.category;
214
}
215
216
private initSettingDescription() {
217
if (this.setting.description.length > SettingsTreeSettingElement.MAX_DESC_LINES) {
218
const truncatedDescLines = this.setting.description.slice(0, SettingsTreeSettingElement.MAX_DESC_LINES);
219
truncatedDescLines.push('[...]');
220
this.description = truncatedDescLines.join('\n');
221
} else {
222
this.description = this.setting.description.join('\n');
223
}
224
}
225
226
private initSettingValueType() {
227
if (isExtensionToggleSetting(this.setting, this.productService)) {
228
this.valueType = SettingValueType.ExtensionToggle;
229
} else if (this.setting.enum && (!this.setting.type || settingTypeEnumRenderable(this.setting.type))) {
230
this.valueType = SettingValueType.Enum;
231
} else if (this.setting.type === 'string') {
232
if (this.setting.editPresentation === EditPresentationTypes.Multiline) {
233
this.valueType = SettingValueType.MultilineString;
234
} else {
235
this.valueType = SettingValueType.String;
236
}
237
} else if (isExcludeSetting(this.setting)) {
238
this.valueType = SettingValueType.Exclude;
239
} else if (isIncludeSetting(this.setting)) {
240
this.valueType = SettingValueType.Include;
241
} else if (this.setting.type === 'integer') {
242
this.valueType = SettingValueType.Integer;
243
} else if (this.setting.type === 'number') {
244
this.valueType = SettingValueType.Number;
245
} else if (this.setting.type === 'boolean') {
246
this.valueType = SettingValueType.Boolean;
247
} else if (this.setting.type === 'array' && this.setting.arrayItemType &&
248
['string', 'enum', 'number', 'integer'].includes(this.setting.arrayItemType)) {
249
this.valueType = SettingValueType.Array;
250
} else if (Array.isArray(this.setting.type) && this.setting.type.includes(SettingValueType.Null) && this.setting.type.length === 2) {
251
if (this.setting.type.includes(SettingValueType.Integer)) {
252
this.valueType = SettingValueType.NullableInteger;
253
} else if (this.setting.type.includes(SettingValueType.Number)) {
254
this.valueType = SettingValueType.NullableNumber;
255
} else {
256
this.valueType = SettingValueType.Complex;
257
}
258
} else {
259
const schemaType = getObjectSettingSchemaType(this.setting);
260
if (schemaType) {
261
if (this.setting.allKeysAreBoolean) {
262
this.valueType = SettingValueType.BooleanObject;
263
} else if (schemaType === 'simple') {
264
this.valueType = SettingValueType.Object;
265
} else {
266
this.valueType = SettingValueType.ComplexObject;
267
}
268
} else if (this.setting.isLanguageTagSetting) {
269
this.valueType = SettingValueType.LanguageTag;
270
} else {
271
this.valueType = SettingValueType.Complex;
272
}
273
}
274
}
275
276
inspectSelf() {
277
const targetToInspect = this.getTargetToInspect(this.setting);
278
const inspectResult = inspectSetting(this.setting.key, targetToInspect, this.languageFilter, this.configurationService);
279
this.update(inspectResult, this.isWorkspaceTrusted);
280
}
281
282
private getTargetToInspect(setting: ISetting): SettingsTarget {
283
if (!this.userDataProfileService.currentProfile.isDefault && !this.userDataProfileService.currentProfile.useDefaultFlags?.settings) {
284
if (setting.scope === ConfigurationScope.APPLICATION) {
285
return ConfigurationTarget.APPLICATION;
286
}
287
if (this.configurationService.isSettingAppliedForAllProfiles(setting.key) && this.settingsTarget === ConfigurationTarget.USER_LOCAL) {
288
return ConfigurationTarget.APPLICATION;
289
}
290
}
291
return this.settingsTarget;
292
}
293
294
private update(inspectResult: IInspectResult, isWorkspaceTrusted: boolean): void {
295
let { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector } = inspectResult;
296
297
switch (targetSelector) {
298
case 'workspaceFolderValue':
299
case 'workspaceValue':
300
this.isUntrusted = !!this.setting.restricted && !isWorkspaceTrusted;
301
break;
302
}
303
304
let displayValue = isConfigured ? inspected[targetSelector] : inspected.defaultValue;
305
const overriddenScopeList: string[] = [];
306
const overriddenDefaultsLanguageList: string[] = [];
307
if ((languageSelector || targetSelector !== 'workspaceValue') && typeof inspected.workspaceValue !== 'undefined') {
308
overriddenScopeList.push('workspace:');
309
}
310
if ((languageSelector || targetSelector !== 'userRemoteValue') && typeof inspected.userRemoteValue !== 'undefined') {
311
overriddenScopeList.push('remote:');
312
}
313
if ((languageSelector || targetSelector !== 'userLocalValue') && typeof inspected.userLocalValue !== 'undefined') {
314
overriddenScopeList.push('user:');
315
}
316
317
if (inspected.overrideIdentifiers) {
318
for (const overrideIdentifier of inspected.overrideIdentifiers) {
319
const inspectedOverride = inspectedLanguageOverrides.get(overrideIdentifier);
320
if (inspectedOverride) {
321
if (this.languageService.isRegisteredLanguageId(overrideIdentifier)) {
322
if (languageSelector !== overrideIdentifier && typeof inspectedOverride.default?.override !== 'undefined') {
323
overriddenDefaultsLanguageList.push(overrideIdentifier);
324
}
325
if ((languageSelector !== overrideIdentifier || targetSelector !== 'workspaceValue') && typeof inspectedOverride.workspace?.override !== 'undefined') {
326
overriddenScopeList.push(`workspace:${overrideIdentifier}`);
327
}
328
if ((languageSelector !== overrideIdentifier || targetSelector !== 'userRemoteValue') && typeof inspectedOverride.userRemote?.override !== 'undefined') {
329
overriddenScopeList.push(`remote:${overrideIdentifier}`);
330
}
331
if ((languageSelector !== overrideIdentifier || targetSelector !== 'userLocalValue') && typeof inspectedOverride.userLocal?.override !== 'undefined') {
332
overriddenScopeList.push(`user:${overrideIdentifier}`);
333
}
334
}
335
this.languageOverrideValues.set(overrideIdentifier, inspectedOverride);
336
}
337
}
338
}
339
this.overriddenScopeList = overriddenScopeList;
340
this.overriddenDefaultsLanguageList = overriddenDefaultsLanguageList;
341
342
// The user might have added, removed, or modified a language filter,
343
// so we reset the default value source to the non-language-specific default value source for now.
344
this.defaultValueSource = this.setting.nonLanguageSpecificDefaultValueSource;
345
346
if (inspected.policyValue !== undefined) {
347
this.hasPolicyValue = true;
348
isConfigured = false; // The user did not manually configure the setting themselves.
349
displayValue = inspected.policyValue;
350
this.scopeValue = inspected.policyValue;
351
this.defaultValue = inspected.defaultValue;
352
} else if (languageSelector && this.languageOverrideValues.has(languageSelector)) {
353
const overrideValues = this.languageOverrideValues.get(languageSelector)!;
354
// In the worst case, go back to using the previous display value.
355
// Also, sometimes the override is in the form of a default value override, so consider that second.
356
displayValue = (isConfigured ? overrideValues[targetSelector] : overrideValues.defaultValue) ?? displayValue;
357
this.scopeValue = isConfigured && overrideValues[targetSelector];
358
this.defaultValue = overrideValues.defaultValue ?? inspected.defaultValue;
359
360
const registryValues = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationDefaultsOverrides();
361
const source = registryValues.get(`[${languageSelector}]`)?.source;
362
const overrideValueSource = source instanceof Map ? source.get(this.setting.key) : undefined;
363
if (overrideValueSource) {
364
this.defaultValueSource = overrideValueSource;
365
}
366
} else {
367
this.scopeValue = isConfigured && inspected[targetSelector];
368
this.defaultValue = inspected.defaultValue;
369
}
370
371
this.value = displayValue;
372
this.isConfigured = isConfigured;
373
if (isConfigured || this.setting.tags || this.tags || this.setting.restricted || this.hasPolicyValue) {
374
// Don't create an empty Set for all 1000 settings, only if needed
375
this.tags = new Set<string>();
376
if (isConfigured) {
377
this.tags.add(MODIFIED_SETTING_TAG);
378
}
379
380
this.setting.tags?.forEach(tag => this.tags!.add(tag));
381
382
if (this.setting.restricted) {
383
this.tags.add(REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG);
384
}
385
386
if (this.hasPolicyValue) {
387
this.tags.add(POLICY_SETTING_TAG);
388
}
389
}
390
}
391
392
matchesAllTags(tagFilters?: Set<string>): boolean {
393
if (!tagFilters?.size) {
394
// This setting, which may have tags,
395
// matches against a query with no tags.
396
return true;
397
}
398
399
if (!this.tags) {
400
// The setting must inspect itself to get tag information
401
// including for the hasPolicy tag.
402
this.inspectSelf();
403
}
404
405
// Handle the special 'stable' tag filter
406
if (tagFilters.has('stable')) {
407
// For stable filter, exclude preview and experimental settings
408
if (this.tags?.has('preview') || this.tags?.has('experimental')) {
409
return false;
410
}
411
// Check other filters (excluding 'stable' itself)
412
const otherFilters = new Set(Array.from(tagFilters).filter(tag => tag !== 'stable'));
413
if (otherFilters.size === 0) {
414
return true;
415
}
416
return !!this.tags?.size &&
417
Array.from(otherFilters).every(tag => this.tags!.has(tag));
418
}
419
420
// Check that the filter tags are a subset of this setting's tags
421
return !!this.tags?.size &&
422
Array.from(tagFilters).every(tag => this.tags!.has(tag));
423
}
424
425
matchesScope(scope: SettingsTarget, isRemote: boolean): boolean {
426
const configTarget = URI.isUri(scope) ? ConfigurationTarget.WORKSPACE_FOLDER : scope;
427
428
if (!this.setting.scope) {
429
return true;
430
}
431
432
if (configTarget === ConfigurationTarget.APPLICATION) {
433
return APPLICATION_SCOPES.includes(this.setting.scope);
434
}
435
436
if (configTarget === ConfigurationTarget.WORKSPACE_FOLDER) {
437
return FOLDER_SCOPES.includes(this.setting.scope);
438
}
439
440
if (configTarget === ConfigurationTarget.WORKSPACE) {
441
return WORKSPACE_SCOPES.includes(this.setting.scope);
442
}
443
444
if (configTarget === ConfigurationTarget.USER_REMOTE) {
445
return REMOTE_MACHINE_SCOPES.includes(this.setting.scope) || USER_LOCAL_AND_REMOTE_SETTINGS.includes(this.setting.key);
446
}
447
448
if (configTarget === ConfigurationTarget.USER_LOCAL) {
449
if (isRemote) {
450
return LOCAL_MACHINE_SCOPES.includes(this.setting.scope) || USER_LOCAL_AND_REMOTE_SETTINGS.includes(this.setting.key);
451
}
452
}
453
454
return true;
455
}
456
457
matchesAnyExtension(extensionFilters?: Set<string>): boolean {
458
if (!extensionFilters || !extensionFilters.size) {
459
return true;
460
}
461
462
if (!this.setting.extensionInfo) {
463
return false;
464
}
465
466
return Array.from(extensionFilters).some(extensionId => extensionId.toLowerCase() === this.setting.extensionInfo!.id.toLowerCase());
467
}
468
469
matchesAnyFeature(featureFilters?: Set<string>): boolean {
470
if (!featureFilters || !featureFilters.size) {
471
return true;
472
}
473
474
const features = tocData.children!.find(child => child.id === 'features');
475
476
return Array.from(featureFilters).some(filter => {
477
if (features && features.children) {
478
const feature = features.children.find(feature => 'features/' + filter === feature.id);
479
if (feature) {
480
const patterns = feature.settings?.map(setting => createSettingMatchRegExp(setting));
481
return patterns && !this.setting.extensionInfo && patterns.some(pattern => pattern.test(this.setting.key.toLowerCase()));
482
} else {
483
return false;
484
}
485
} else {
486
return false;
487
}
488
});
489
}
490
491
matchesAnyId(idFilters?: Set<string>): boolean {
492
if (!idFilters || !idFilters.size) {
493
return true;
494
}
495
496
// Check for exact match first
497
if (idFilters.has(this.setting.key)) {
498
return true;
499
}
500
501
// Check for wildcard patterns (ending with .*)
502
for (const filter of idFilters) {
503
if (filter.endsWith('*')) {
504
const prefix = filter.slice(0, -1); // Remove '*' suffix
505
if (this.setting.key.startsWith(prefix)) {
506
return true;
507
}
508
}
509
}
510
511
return false;
512
}
513
514
matchesAllLanguages(languageFilter?: string): boolean {
515
if (!languageFilter) {
516
// We're not filtering by language.
517
return true;
518
}
519
520
if (!this.languageService.isRegisteredLanguageId(languageFilter)) {
521
// We're trying to filter by an invalid language.
522
return false;
523
}
524
525
// We have a language filter in the search widget at this point.
526
// We decide to show all language overridable settings to make the
527
// lang filter act more like a scope filter,
528
// rather than adding on an implicit @modified as well.
529
if (this.setting.scope === ConfigurationScope.LANGUAGE_OVERRIDABLE) {
530
return true;
531
}
532
533
return false;
534
}
535
}
536
537
538
function createSettingMatchRegExp(pattern: string): RegExp {
539
pattern = escapeRegExpCharacters(pattern)
540
.replace(/\\\*/g, '.*');
541
542
return new RegExp(`^${pattern}$`, 'i');
543
}
544
545
export class SettingsTreeModel implements IDisposable {
546
protected _root!: SettingsTreeGroupElement;
547
private _tocRoot!: ITOCEntry<ISetting>;
548
private readonly _treeElementsBySettingName = new Map<string, SettingsTreeSettingElement[]>();
549
550
constructor(
551
protected readonly _viewState: ISettingsEditorViewState,
552
private _isWorkspaceTrusted: boolean,
553
@IWorkbenchConfigurationService private readonly _configurationService: IWorkbenchConfigurationService,
554
@ILanguageService private readonly _languageService: ILanguageService,
555
@IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService,
556
@IProductService private readonly _productService: IProductService
557
) {
558
}
559
560
get root(): SettingsTreeGroupElement {
561
return this._root;
562
}
563
564
update(newTocRoot = this._tocRoot): void {
565
this._treeElementsBySettingName.clear();
566
567
const newRoot = this.createSettingsTreeGroupElement(newTocRoot);
568
if (newRoot.children[0] instanceof SettingsTreeGroupElement) {
569
(<SettingsTreeGroupElement>newRoot.children[0]).isFirstGroup = true;
570
}
571
572
if (this._root) {
573
this.disposeChildren(this._root.children);
574
this._root.children = newRoot.children;
575
newRoot.dispose();
576
} else {
577
this._root = newRoot;
578
}
579
}
580
581
updateWorkspaceTrust(workspaceTrusted: boolean): void {
582
this._isWorkspaceTrusted = workspaceTrusted;
583
this.updateRequireTrustedTargetElements();
584
}
585
586
private disposeChildren(children: SettingsTreeGroupChild[]) {
587
for (const child of children) {
588
this.disposeChildAndRecurse(child);
589
}
590
}
591
592
private disposeChildAndRecurse(element: SettingsTreeElement) {
593
if (element instanceof SettingsTreeGroupElement) {
594
this.disposeChildren(element.children);
595
}
596
597
element.dispose();
598
}
599
600
getElementsByName(name: string): SettingsTreeSettingElement[] | null {
601
return this._treeElementsBySettingName.get(name) ?? null;
602
}
603
604
updateElementsByName(name: string): void {
605
if (!this._treeElementsBySettingName.has(name)) {
606
return;
607
}
608
609
this.reinspectSettings(this._treeElementsBySettingName.get(name)!);
610
}
611
612
private updateRequireTrustedTargetElements(): void {
613
this.reinspectSettings([...this._treeElementsBySettingName.values()].flat().filter(s => s.isUntrusted));
614
}
615
616
private reinspectSettings(settings: SettingsTreeSettingElement[]): void {
617
for (const element of settings) {
618
element.inspectSelf();
619
}
620
}
621
622
private createSettingsTreeGroupElement(tocEntry: ITOCEntry<ISetting>, parent?: SettingsTreeGroupElement): SettingsTreeGroupElement {
623
const depth = parent ? this.getDepth(parent) + 1 : 0;
624
const element = new SettingsTreeGroupElement(tocEntry.id, undefined, tocEntry.label, depth, false);
625
element.parent = parent;
626
627
const children: SettingsTreeGroupChild[] = [];
628
if (tocEntry.settings) {
629
const settingChildren = tocEntry.settings.map(s => this.createSettingsTreeSettingElement(s, element));
630
for (const child of settingChildren) {
631
if (!child.setting.deprecationMessage) {
632
children.push(child);
633
} else {
634
child.inspectSelf();
635
if (child.isConfigured) {
636
children.push(child);
637
} else {
638
child.dispose();
639
}
640
}
641
}
642
}
643
644
if (tocEntry.children) {
645
const groupChildren = tocEntry.children.map(child => this.createSettingsTreeGroupElement(child, element));
646
children.push(...groupChildren);
647
}
648
649
element.children = children;
650
651
return element;
652
}
653
654
private getDepth(element: SettingsTreeElement): number {
655
if (element.parent) {
656
return 1 + this.getDepth(element.parent);
657
} else {
658
return 0;
659
}
660
}
661
662
private createSettingsTreeSettingElement(setting: ISetting, parent: SettingsTreeGroupElement): SettingsTreeSettingElement {
663
const element = new SettingsTreeSettingElement(
664
setting,
665
parent,
666
this._viewState.settingsTarget,
667
this._isWorkspaceTrusted,
668
this._viewState.languageFilter,
669
this._languageService,
670
this._productService,
671
this._userDataProfileService,
672
this._configurationService);
673
674
const nameElements = this._treeElementsBySettingName.get(setting.key) ?? [];
675
nameElements.push(element);
676
this._treeElementsBySettingName.set(setting.key, nameElements);
677
return element;
678
}
679
680
dispose() {
681
this._treeElementsBySettingName.clear();
682
this.disposeChildAndRecurse(this._root);
683
}
684
}
685
686
interface IInspectResult {
687
isConfigured: boolean;
688
inspected: IConfigurationValue<unknown>;
689
targetSelector: 'applicationValue' | 'userLocalValue' | 'userRemoteValue' | 'workspaceValue' | 'workspaceFolderValue';
690
inspectedLanguageOverrides: Map<string, IConfigurationValue<unknown>>;
691
languageSelector: string | undefined;
692
}
693
694
export function inspectSetting(key: string, target: SettingsTarget, languageFilter: string | undefined, configurationService: IWorkbenchConfigurationService): IInspectResult {
695
const inspectOverrides = URI.isUri(target) ? { resource: target } : undefined;
696
const inspected = configurationService.inspect(key, inspectOverrides);
697
const targetSelector = target === ConfigurationTarget.APPLICATION ? 'applicationValue' :
698
target === ConfigurationTarget.USER_LOCAL ? 'userLocalValue' :
699
target === ConfigurationTarget.USER_REMOTE ? 'userRemoteValue' :
700
target === ConfigurationTarget.WORKSPACE ? 'workspaceValue' :
701
'workspaceFolderValue';
702
const targetOverrideSelector = target === ConfigurationTarget.APPLICATION ? 'application' :
703
target === ConfigurationTarget.USER_LOCAL ? 'userLocal' :
704
target === ConfigurationTarget.USER_REMOTE ? 'userRemote' :
705
target === ConfigurationTarget.WORKSPACE ? 'workspace' :
706
'workspaceFolder';
707
let isConfigured = typeof inspected[targetSelector] !== 'undefined';
708
709
const overrideIdentifiers = inspected.overrideIdentifiers;
710
const inspectedLanguageOverrides = new Map<string, IConfigurationValue<unknown>>();
711
712
// We must reset isConfigured to be false if languageFilter is set, and manually
713
// determine whether it can be set to true later.
714
if (languageFilter) {
715
isConfigured = false;
716
}
717
if (overrideIdentifiers) {
718
// The setting we're looking at has language overrides.
719
for (const overrideIdentifier of overrideIdentifiers) {
720
inspectedLanguageOverrides.set(overrideIdentifier, configurationService.inspect(key, { overrideIdentifier }));
721
}
722
723
// For all language filters, see if there's an override for that filter.
724
if (languageFilter) {
725
if (inspectedLanguageOverrides.has(languageFilter)) {
726
const overrideValue = inspectedLanguageOverrides.get(languageFilter)![targetOverrideSelector]?.override;
727
if (typeof overrideValue !== 'undefined') {
728
isConfigured = true;
729
}
730
}
731
}
732
}
733
734
return { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector: languageFilter };
735
}
736
737
function sanitizeId(id: string): string {
738
return id.replace(/[\.\/]/, '_');
739
}
740
741
export function settingKeyToDisplayFormat(key: string, groupId: string = '', isLanguageTagSetting: boolean = false): { category: string; label: string } {
742
const lastDotIdx = key.lastIndexOf('.');
743
let category = '';
744
if (lastDotIdx >= 0) {
745
category = key.substring(0, lastDotIdx);
746
key = key.substring(lastDotIdx + 1);
747
}
748
749
groupId = groupId.replace(/\//g, '.');
750
category = trimCategoryForGroup(category, groupId);
751
category = wordifyKey(category);
752
753
if (isLanguageTagSetting) {
754
key = getLanguageTagSettingPlainKey(key);
755
key = '$(bracket) ' + key;
756
}
757
758
const label = wordifyKey(key);
759
return { category, label };
760
}
761
762
/**
763
* Removes redundant sections of the category label.
764
* A redundant section is a section already reflected in the groupId.
765
*
766
* @param category The category of the specific setting.
767
* @param groupId The author + extension ID.
768
* @returns The new category label to use.
769
*/
770
function trimCategoryForGroup(category: string, groupId: string): string {
771
const doTrim = (forward: boolean) => {
772
// Remove the Insiders portion if the category doesn't use it.
773
if (!/insiders$/i.test(category)) {
774
groupId = groupId.replace(/-?insiders$/i, '');
775
}
776
const parts = groupId.split('.')
777
.map(part => {
778
// Remove hyphens, but only if that results in a match with the category.
779
if (part.replace(/-/g, '').toLowerCase() === category.toLowerCase()) {
780
return part.replace(/-/g, '');
781
} else {
782
return part;
783
}
784
});
785
while (parts.length) {
786
const reg = new RegExp(`^${parts.join('\\.')}(\\.|$)`, 'i');
787
if (reg.test(category)) {
788
return category.replace(reg, '');
789
}
790
791
if (forward) {
792
parts.pop();
793
} else {
794
parts.shift();
795
}
796
}
797
798
return null;
799
};
800
801
let trimmed = doTrim(true);
802
if (trimmed === null) {
803
trimmed = doTrim(false);
804
}
805
806
if (trimmed === null) {
807
trimmed = category;
808
}
809
810
return trimmed;
811
}
812
813
function isExtensionToggleSetting(setting: ISetting, productService: IProductService): boolean {
814
return ENABLE_EXTENSION_TOGGLE_SETTINGS &&
815
!!productService.extensionRecommendations &&
816
!!setting.displayExtensionId;
817
}
818
819
function isExcludeSetting(setting: ISetting): boolean {
820
return setting.key === 'files.exclude' ||
821
setting.key === 'search.exclude' ||
822
setting.key === 'workbench.localHistory.exclude' ||
823
setting.key === 'explorer.autoRevealExclude' ||
824
setting.key === 'files.readonlyExclude' ||
825
setting.key === 'files.watcherExclude';
826
}
827
828
function isIncludeSetting(setting: ISetting): boolean {
829
return setting.key === 'files.readonlyInclude';
830
}
831
832
// The values of the following settings when a default values has been removed
833
export function objectSettingSupportsRemoveDefaultValue(key: string): boolean {
834
return key === 'workbench.editor.customLabels.patterns';
835
}
836
837
function isSimpleType(type: string | undefined): boolean {
838
return type === 'string' || type === 'boolean' || type === 'integer' || type === 'number';
839
}
840
841
function getObjectRenderableSchemaType(schema: IJSONSchema, key: string): 'simple' | 'complex' | false {
842
const { type } = schema;
843
844
if (Array.isArray(type)) {
845
if (objectSettingSupportsRemoveDefaultValue(key) && type.length === 2) {
846
if (type.includes('null') && (type.includes('string') || type.includes('boolean') || type.includes('integer') || type.includes('number'))) {
847
return 'simple';
848
}
849
}
850
851
for (const t of type) {
852
if (!isSimpleType(t)) {
853
return false;
854
}
855
}
856
return 'complex';
857
}
858
859
if (isSimpleType(type)) {
860
return 'simple';
861
}
862
863
if (type === 'array') {
864
if (schema.items) {
865
const itemSchemas = Array.isArray(schema.items) ? schema.items : [schema.items];
866
for (const { type } of itemSchemas) {
867
if (Array.isArray(type)) {
868
for (const t of type) {
869
if (!isSimpleType(t)) {
870
return false;
871
}
872
}
873
return 'complex';
874
}
875
if (!isSimpleType(type)) {
876
return false;
877
}
878
return 'complex';
879
}
880
}
881
return false;
882
}
883
884
return false;
885
}
886
887
function getObjectSettingSchemaType({
888
key,
889
type,
890
objectProperties,
891
objectPatternProperties,
892
objectAdditionalProperties
893
}: ISetting): 'simple' | 'complex' | false {
894
if (type !== 'object') {
895
return false;
896
}
897
898
// object can have any shape
899
if (
900
isUndefinedOrNull(objectProperties) &&
901
isUndefinedOrNull(objectPatternProperties) &&
902
isUndefinedOrNull(objectAdditionalProperties)
903
) {
904
return false;
905
}
906
907
// objectAdditionalProperties allow the setting to have any shape,
908
// but if there's a pattern property that handles everything, then every
909
// property will match that patternProperty, so we don't need to look at
910
// the value of objectAdditionalProperties in that case.
911
if ((objectAdditionalProperties === true || objectAdditionalProperties === undefined)
912
&& !Object.keys(objectPatternProperties ?? {}).includes('.*')) {
913
return false;
914
}
915
916
const schemas = [...Object.values(objectProperties ?? {}), ...Object.values(objectPatternProperties ?? {})];
917
918
if (objectAdditionalProperties && typeof objectAdditionalProperties === 'object') {
919
schemas.push(objectAdditionalProperties);
920
}
921
922
let schemaType: 'simple' | 'complex' | false = 'simple';
923
for (const schema of schemas) {
924
for (const subSchema of Array.isArray(schema.anyOf) ? schema.anyOf : [schema]) {
925
const subSchemaType = getObjectRenderableSchemaType(subSchema, key);
926
if (subSchemaType === false) {
927
return false;
928
}
929
if (subSchemaType === 'complex') {
930
schemaType = 'complex';
931
}
932
}
933
}
934
935
return schemaType;
936
}
937
938
function settingTypeEnumRenderable(_type: string | string[]) {
939
const enumRenderableSettingTypes = ['string', 'boolean', 'null', 'integer', 'number'];
940
const type = Array.isArray(_type) ? _type : [_type];
941
return type.every(type => enumRenderableSettingTypes.includes(type));
942
}
943
944
export const enum SearchResultIdx {
945
Local = 0,
946
Remote = 1,
947
NewExtensions = 2,
948
Embeddings = 3,
949
AiSelected = 4
950
}
951
952
export class SearchResultModel extends SettingsTreeModel {
953
private rawSearchResults: ISearchResult[] | null = null;
954
private cachedUniqueSearchResults: Map<boolean, ISearchResult | null>;
955
private newExtensionSearchResults: ISearchResult | null = null;
956
private searchResultCount: number | null = null;
957
private settingsOrderByTocIndex: Map<string, number> | null;
958
private aiFilterEnabled: boolean = false;
959
960
readonly id = 'searchResultModel';
961
962
constructor(
963
viewState: ISettingsEditorViewState,
964
settingsOrderByTocIndex: Map<string, number> | null,
965
isWorkspaceTrusted: boolean,
966
@IWorkbenchConfigurationService configurationService: IWorkbenchConfigurationService,
967
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
968
@ILanguageService languageService: ILanguageService,
969
@IUserDataProfileService userDataProfileService: IUserDataProfileService,
970
@IProductService productService: IProductService
971
) {
972
super(viewState, isWorkspaceTrusted, configurationService, languageService, userDataProfileService, productService);
973
this.settingsOrderByTocIndex = settingsOrderByTocIndex;
974
this.cachedUniqueSearchResults = new Map();
975
this.update({ id: 'searchResultModel', label: '' });
976
}
977
978
set showAiResults(show: boolean) {
979
this.aiFilterEnabled = show;
980
this.updateChildren();
981
}
982
983
private sortResults(filterMatches: ISettingMatch[]): ISettingMatch[] {
984
if (this.settingsOrderByTocIndex) {
985
for (const match of filterMatches) {
986
match.setting.internalOrder = this.settingsOrderByTocIndex.get(match.setting.key);
987
}
988
}
989
990
// The search only has filters, so we can sort by the order in the TOC.
991
if (!this._viewState.query) {
992
return filterMatches.sort((a, b) => compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder));
993
}
994
995
// Sort the settings according to their relevancy.
996
// https://github.com/microsoft/vscode/issues/197773
997
filterMatches.sort((a, b) => {
998
if (a.matchType !== b.matchType) {
999
// Sort by match type if the match types are not the same.
1000
// The priority of the match type is given by the SettingMatchType enum.
1001
return b.matchType - a.matchType;
1002
} else if ((a.matchType & SettingMatchType.NonContiguousWordsInSettingsLabel) || (a.matchType & SettingMatchType.ContiguousWordsInSettingsLabel)) {
1003
// The match types of a and b are the same and can be sorted by their number of matched words.
1004
// If those numbers are the same, sort by the order in the table of contents.
1005
return (b.keyMatchScore - a.keyMatchScore) || compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder);
1006
} else if (a.matchType === SettingMatchType.RemoteMatch) {
1007
// The match types are the same and are RemoteMatch.
1008
// Sort by score.
1009
return b.score - a.score;
1010
} else {
1011
// The match types are the same but are not RemoteMatch.
1012
// Sort by their order in the table of contents.
1013
return compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder);
1014
}
1015
});
1016
1017
// Remove duplicates, which sometimes occur with settings
1018
// such as the experimental toggle setting.
1019
return arrays.distinct(filterMatches, (match) => match.setting.key);
1020
}
1021
1022
getUniqueSearchResults(): ISearchResult | null {
1023
const cachedResults = this.cachedUniqueSearchResults.get(this.aiFilterEnabled);
1024
if (cachedResults) {
1025
return cachedResults;
1026
}
1027
1028
if (!this.rawSearchResults) {
1029
return null;
1030
}
1031
1032
let combinedFilterMatches: ISettingMatch[] = [];
1033
1034
if (this.aiFilterEnabled) {
1035
const aiSelectedKeys = new Set<string>();
1036
const aiSelectedResult = this.rawSearchResults[SearchResultIdx.AiSelected];
1037
if (aiSelectedResult) {
1038
aiSelectedResult.filterMatches.forEach(m => aiSelectedKeys.add(m.setting.key));
1039
combinedFilterMatches = aiSelectedResult.filterMatches;
1040
}
1041
1042
const embeddingsResult = this.rawSearchResults[SearchResultIdx.Embeddings];
1043
if (embeddingsResult) {
1044
embeddingsResult.filterMatches = embeddingsResult.filterMatches.filter(m => !aiSelectedKeys.has(m.setting.key));
1045
combinedFilterMatches = combinedFilterMatches.concat(embeddingsResult.filterMatches);
1046
}
1047
const result = {
1048
filterMatches: combinedFilterMatches,
1049
exactMatch: false
1050
};
1051
this.cachedUniqueSearchResults.set(true, result);
1052
return result;
1053
}
1054
1055
const localMatchKeys = new Set<string>();
1056
const localResult = this.rawSearchResults[SearchResultIdx.Local];
1057
if (localResult) {
1058
localResult.filterMatches.forEach(m => localMatchKeys.add(m.setting.key));
1059
combinedFilterMatches = localResult.filterMatches;
1060
}
1061
1062
const remoteResult = this.rawSearchResults[SearchResultIdx.Remote];
1063
if (remoteResult) {
1064
remoteResult.filterMatches = remoteResult.filterMatches.filter(m => !localMatchKeys.has(m.setting.key));
1065
combinedFilterMatches = combinedFilterMatches.concat(remoteResult.filterMatches);
1066
1067
this.newExtensionSearchResults = this.rawSearchResults[SearchResultIdx.NewExtensions];
1068
}
1069
combinedFilterMatches = this.sortResults(combinedFilterMatches);
1070
const result = {
1071
filterMatches: combinedFilterMatches,
1072
exactMatch: localResult.exactMatch // remote results should never have an exact match
1073
};
1074
this.cachedUniqueSearchResults.set(false, result);
1075
return result;
1076
}
1077
1078
getRawResults(): ISearchResult[] {
1079
return this.rawSearchResults ?? [];
1080
}
1081
1082
private getUniqueSearchResultSettings(): ISetting[] {
1083
return this.getUniqueSearchResults()?.filterMatches.map(m => m.setting) ?? [];
1084
}
1085
1086
updateChildren(): void {
1087
this.update({
1088
id: 'searchResultModel',
1089
label: 'searchResultModel',
1090
settings: this.getUniqueSearchResultSettings()
1091
});
1092
1093
// Save time by filtering children in the search model instead of relying on the tree filter, which still requires heights to be calculated.
1094
const isRemote = !!this.environmentService.remoteAuthority;
1095
1096
const newChildren = [];
1097
for (const child of this.root.children) {
1098
if (child instanceof SettingsTreeSettingElement
1099
&& child.matchesAllTags(this._viewState.tagFilters)
1100
&& child.matchesScope(this._viewState.settingsTarget, isRemote)
1101
&& child.matchesAnyExtension(this._viewState.extensionFilters)
1102
&& child.matchesAnyId(this._viewState.idFilters)
1103
&& child.matchesAnyFeature(this._viewState.featureFilters)
1104
&& child.matchesAllLanguages(this._viewState.languageFilter)) {
1105
newChildren.push(child);
1106
} else {
1107
child.dispose();
1108
}
1109
}
1110
this.root.children = newChildren;
1111
this.searchResultCount = this.root.children.length;
1112
1113
if (this.newExtensionSearchResults?.filterMatches.length) {
1114
let resultExtensionIds = this.newExtensionSearchResults.filterMatches
1115
.map(result => (<IExtensionSetting>result.setting))
1116
.filter(setting => setting.extensionName && setting.extensionPublisher)
1117
.map(setting => `${setting.extensionPublisher}.${setting.extensionName}`);
1118
resultExtensionIds = arrays.distinct(resultExtensionIds);
1119
1120
if (resultExtensionIds.length) {
1121
const newExtElement = new SettingsTreeNewExtensionsElement('newExtensions', resultExtensionIds);
1122
newExtElement.parent = this._root;
1123
this._root.children.push(newExtElement);
1124
}
1125
}
1126
}
1127
1128
setResult(order: SearchResultIdx, result: ISearchResult | null): void {
1129
this.cachedUniqueSearchResults.clear();
1130
this.newExtensionSearchResults = null;
1131
1132
if (this.rawSearchResults && order === SearchResultIdx.Local) {
1133
// To prevent the Settings editor from showing
1134
// stale remote results mid-search.
1135
delete this.rawSearchResults[SearchResultIdx.Remote];
1136
}
1137
1138
this.rawSearchResults ??= [];
1139
if (!result) {
1140
delete this.rawSearchResults[order];
1141
return;
1142
}
1143
1144
this.rawSearchResults[order] = result;
1145
this.updateChildren();
1146
}
1147
1148
getUniqueResultsCount(): number {
1149
return this.searchResultCount ?? 0;
1150
}
1151
}
1152
1153
export interface IParsedQuery {
1154
tags: string[];
1155
query: string;
1156
extensionFilters: string[];
1157
idFilters: string[];
1158
featureFilters: string[];
1159
languageFilter: string | undefined;
1160
}
1161
1162
const tagRegex = /(^|\s)@tag:("([^"]*)"|[^"]\S*)/g;
1163
const extensionRegex = /(^|\s)@ext:("([^"]*)"|[^"]\S*)?/g;
1164
const featureRegex = /(^|\s)@feature:("([^"]*)"|[^"]\S*)?/g;
1165
const idRegex = /(^|\s)@id:("([^"]*)"|[^"]\S*)?/g;
1166
const languageRegex = /(^|\s)@lang:("([^"]*)"|[^"]\S*)?/g;
1167
1168
export function parseQuery(query: string): IParsedQuery {
1169
/**
1170
* A helper function to parse the query on one type of regex.
1171
*
1172
* @param query The search query
1173
* @param filterRegex The regex to use on the query
1174
* @param parsedParts The parts that the regex parses out will be appended to the array passed in here.
1175
* @returns The query with the parsed parts removed
1176
*/
1177
function getTagsForType(query: string, filterRegex: RegExp, parsedParts: string[]): string {
1178
return query.replace(filterRegex, (_, __, quotedParsedElement, unquotedParsedElement) => {
1179
const parsedElement: string = unquotedParsedElement || quotedParsedElement;
1180
if (parsedElement) {
1181
parsedParts.push(...parsedElement.split(',').map(s => s.trim()).filter(s => !isFalsyOrWhitespace(s)));
1182
}
1183
return '';
1184
});
1185
}
1186
1187
const tags: string[] = [];
1188
query = query.replace(tagRegex, (_, __, quotedTag, tag) => {
1189
tags.push(tag || quotedTag);
1190
return '';
1191
});
1192
1193
query = query.replace(`@${MODIFIED_SETTING_TAG}`, () => {
1194
tags.push(MODIFIED_SETTING_TAG);
1195
return '';
1196
});
1197
1198
query = query.replace(`@${POLICY_SETTING_TAG}`, () => {
1199
tags.push(POLICY_SETTING_TAG);
1200
return '';
1201
});
1202
1203
// Handle @stable by excluding preview and experimental tags
1204
query = query.replace(/@stable/g, () => {
1205
tags.push('stable');
1206
return '';
1207
});
1208
1209
const extensions: string[] = [];
1210
const features: string[] = [];
1211
const ids: string[] = [];
1212
const langs: string[] = [];
1213
query = getTagsForType(query, extensionRegex, extensions);
1214
query = getTagsForType(query, featureRegex, features);
1215
query = getTagsForType(query, idRegex, ids);
1216
1217
if (ENABLE_LANGUAGE_FILTER) {
1218
query = getTagsForType(query, languageRegex, langs);
1219
}
1220
1221
query = query.trim();
1222
1223
// For now, only return the first found language filter
1224
return {
1225
tags,
1226
extensionFilters: extensions,
1227
featureFilters: features,
1228
idFilters: ids,
1229
languageFilter: langs.length ? langs[0] : undefined,
1230
query,
1231
};
1232
}
1233
1234