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
3296 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
filterToCategory?: 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
// Check that the filter tags are a subset of this setting's tags
406
return !!this.tags?.size &&
407
Array.from(tagFilters).every(tag => this.tags!.has(tag));
408
}
409
410
matchesScope(scope: SettingsTarget, isRemote: boolean): boolean {
411
const configTarget = URI.isUri(scope) ? ConfigurationTarget.WORKSPACE_FOLDER : scope;
412
413
if (!this.setting.scope) {
414
return true;
415
}
416
417
if (configTarget === ConfigurationTarget.APPLICATION) {
418
return APPLICATION_SCOPES.includes(this.setting.scope);
419
}
420
421
if (configTarget === ConfigurationTarget.WORKSPACE_FOLDER) {
422
return FOLDER_SCOPES.includes(this.setting.scope);
423
}
424
425
if (configTarget === ConfigurationTarget.WORKSPACE) {
426
return WORKSPACE_SCOPES.includes(this.setting.scope);
427
}
428
429
if (configTarget === ConfigurationTarget.USER_REMOTE) {
430
return REMOTE_MACHINE_SCOPES.includes(this.setting.scope) || USER_LOCAL_AND_REMOTE_SETTINGS.includes(this.setting.key);
431
}
432
433
if (configTarget === ConfigurationTarget.USER_LOCAL) {
434
if (isRemote) {
435
return LOCAL_MACHINE_SCOPES.includes(this.setting.scope) || USER_LOCAL_AND_REMOTE_SETTINGS.includes(this.setting.key);
436
}
437
}
438
439
return true;
440
}
441
442
matchesAnyExtension(extensionFilters?: Set<string>): boolean {
443
if (!extensionFilters || !extensionFilters.size) {
444
return true;
445
}
446
447
if (!this.setting.extensionInfo) {
448
return false;
449
}
450
451
return Array.from(extensionFilters).some(extensionId => extensionId.toLowerCase() === this.setting.extensionInfo!.id.toLowerCase());
452
}
453
454
matchesAnyFeature(featureFilters?: Set<string>): boolean {
455
if (!featureFilters || !featureFilters.size) {
456
return true;
457
}
458
459
const features = tocData.children!.find(child => child.id === 'features');
460
461
return Array.from(featureFilters).some(filter => {
462
if (features && features.children) {
463
const feature = features.children.find(feature => 'features/' + filter === feature.id);
464
if (feature) {
465
const patterns = feature.settings?.map(setting => createSettingMatchRegExp(setting));
466
return patterns && !this.setting.extensionInfo && patterns.some(pattern => pattern.test(this.setting.key.toLowerCase()));
467
} else {
468
return false;
469
}
470
} else {
471
return false;
472
}
473
});
474
}
475
476
matchesAnyId(idFilters?: Set<string>): boolean {
477
if (!idFilters || !idFilters.size) {
478
return true;
479
}
480
return idFilters.has(this.setting.key);
481
}
482
483
matchesAllLanguages(languageFilter?: string): boolean {
484
if (!languageFilter) {
485
// We're not filtering by language.
486
return true;
487
}
488
489
if (!this.languageService.isRegisteredLanguageId(languageFilter)) {
490
// We're trying to filter by an invalid language.
491
return false;
492
}
493
494
// We have a language filter in the search widget at this point.
495
// We decide to show all language overridable settings to make the
496
// lang filter act more like a scope filter,
497
// rather than adding on an implicit @modified as well.
498
if (this.setting.scope === ConfigurationScope.LANGUAGE_OVERRIDABLE) {
499
return true;
500
}
501
502
return false;
503
}
504
}
505
506
507
function createSettingMatchRegExp(pattern: string): RegExp {
508
pattern = escapeRegExpCharacters(pattern)
509
.replace(/\\\*/g, '.*');
510
511
return new RegExp(`^${pattern}$`, 'i');
512
}
513
514
export class SettingsTreeModel implements IDisposable {
515
protected _root!: SettingsTreeGroupElement;
516
private _tocRoot!: ITOCEntry<ISetting>;
517
private readonly _treeElementsBySettingName = new Map<string, SettingsTreeSettingElement[]>();
518
519
constructor(
520
protected readonly _viewState: ISettingsEditorViewState,
521
private _isWorkspaceTrusted: boolean,
522
@IWorkbenchConfigurationService private readonly _configurationService: IWorkbenchConfigurationService,
523
@ILanguageService private readonly _languageService: ILanguageService,
524
@IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService,
525
@IProductService private readonly _productService: IProductService
526
) {
527
}
528
529
get root(): SettingsTreeGroupElement {
530
return this._root;
531
}
532
533
update(newTocRoot = this._tocRoot): void {
534
this._treeElementsBySettingName.clear();
535
536
const newRoot = this.createSettingsTreeGroupElement(newTocRoot);
537
if (newRoot.children[0] instanceof SettingsTreeGroupElement) {
538
(<SettingsTreeGroupElement>newRoot.children[0]).isFirstGroup = true;
539
}
540
541
if (this._root) {
542
this.disposeChildren(this._root.children);
543
this._root.children = newRoot.children;
544
newRoot.dispose();
545
} else {
546
this._root = newRoot;
547
}
548
}
549
550
updateWorkspaceTrust(workspaceTrusted: boolean): void {
551
this._isWorkspaceTrusted = workspaceTrusted;
552
this.updateRequireTrustedTargetElements();
553
}
554
555
private disposeChildren(children: SettingsTreeGroupChild[]) {
556
for (const child of children) {
557
this.disposeChildAndRecurse(child);
558
}
559
}
560
561
private disposeChildAndRecurse(element: SettingsTreeElement) {
562
if (element instanceof SettingsTreeGroupElement) {
563
this.disposeChildren(element.children);
564
}
565
566
element.dispose();
567
}
568
569
getElementsByName(name: string): SettingsTreeSettingElement[] | null {
570
return this._treeElementsBySettingName.get(name) ?? null;
571
}
572
573
updateElementsByName(name: string): void {
574
if (!this._treeElementsBySettingName.has(name)) {
575
return;
576
}
577
578
this.reinspectSettings(this._treeElementsBySettingName.get(name)!);
579
}
580
581
private updateRequireTrustedTargetElements(): void {
582
this.reinspectSettings([...this._treeElementsBySettingName.values()].flat().filter(s => s.isUntrusted));
583
}
584
585
private reinspectSettings(settings: SettingsTreeSettingElement[]): void {
586
for (const element of settings) {
587
element.inspectSelf();
588
}
589
}
590
591
private createSettingsTreeGroupElement(tocEntry: ITOCEntry<ISetting>, parent?: SettingsTreeGroupElement): SettingsTreeGroupElement {
592
const depth = parent ? this.getDepth(parent) + 1 : 0;
593
const element = new SettingsTreeGroupElement(tocEntry.id, undefined, tocEntry.label, depth, false);
594
element.parent = parent;
595
596
const children: SettingsTreeGroupChild[] = [];
597
if (tocEntry.settings) {
598
const settingChildren = tocEntry.settings.map(s => this.createSettingsTreeSettingElement(s, element));
599
for (const child of settingChildren) {
600
if (!child.setting.deprecationMessage) {
601
children.push(child);
602
} else {
603
child.inspectSelf();
604
if (child.isConfigured) {
605
children.push(child);
606
} else {
607
child.dispose();
608
}
609
}
610
}
611
}
612
613
if (tocEntry.children) {
614
const groupChildren = tocEntry.children.map(child => this.createSettingsTreeGroupElement(child, element));
615
children.push(...groupChildren);
616
}
617
618
element.children = children;
619
620
return element;
621
}
622
623
private getDepth(element: SettingsTreeElement): number {
624
if (element.parent) {
625
return 1 + this.getDepth(element.parent);
626
} else {
627
return 0;
628
}
629
}
630
631
private createSettingsTreeSettingElement(setting: ISetting, parent: SettingsTreeGroupElement): SettingsTreeSettingElement {
632
const element = new SettingsTreeSettingElement(
633
setting,
634
parent,
635
this._viewState.settingsTarget,
636
this._isWorkspaceTrusted,
637
this._viewState.languageFilter,
638
this._languageService,
639
this._productService,
640
this._userDataProfileService,
641
this._configurationService);
642
643
const nameElements = this._treeElementsBySettingName.get(setting.key) ?? [];
644
nameElements.push(element);
645
this._treeElementsBySettingName.set(setting.key, nameElements);
646
return element;
647
}
648
649
dispose() {
650
this._treeElementsBySettingName.clear();
651
this.disposeChildAndRecurse(this._root);
652
}
653
}
654
655
interface IInspectResult {
656
isConfigured: boolean;
657
inspected: IConfigurationValue<unknown>;
658
targetSelector: 'applicationValue' | 'userLocalValue' | 'userRemoteValue' | 'workspaceValue' | 'workspaceFolderValue';
659
inspectedLanguageOverrides: Map<string, IConfigurationValue<unknown>>;
660
languageSelector: string | undefined;
661
}
662
663
export function inspectSetting(key: string, target: SettingsTarget, languageFilter: string | undefined, configurationService: IWorkbenchConfigurationService): IInspectResult {
664
const inspectOverrides = URI.isUri(target) ? { resource: target } : undefined;
665
const inspected = configurationService.inspect(key, inspectOverrides);
666
const targetSelector = target === ConfigurationTarget.APPLICATION ? 'applicationValue' :
667
target === ConfigurationTarget.USER_LOCAL ? 'userLocalValue' :
668
target === ConfigurationTarget.USER_REMOTE ? 'userRemoteValue' :
669
target === ConfigurationTarget.WORKSPACE ? 'workspaceValue' :
670
'workspaceFolderValue';
671
const targetOverrideSelector = target === ConfigurationTarget.APPLICATION ? 'application' :
672
target === ConfigurationTarget.USER_LOCAL ? 'userLocal' :
673
target === ConfigurationTarget.USER_REMOTE ? 'userRemote' :
674
target === ConfigurationTarget.WORKSPACE ? 'workspace' :
675
'workspaceFolder';
676
let isConfigured = typeof inspected[targetSelector] !== 'undefined';
677
678
const overrideIdentifiers = inspected.overrideIdentifiers;
679
const inspectedLanguageOverrides = new Map<string, IConfigurationValue<unknown>>();
680
681
// We must reset isConfigured to be false if languageFilter is set, and manually
682
// determine whether it can be set to true later.
683
if (languageFilter) {
684
isConfigured = false;
685
}
686
if (overrideIdentifiers) {
687
// The setting we're looking at has language overrides.
688
for (const overrideIdentifier of overrideIdentifiers) {
689
inspectedLanguageOverrides.set(overrideIdentifier, configurationService.inspect(key, { overrideIdentifier }));
690
}
691
692
// For all language filters, see if there's an override for that filter.
693
if (languageFilter) {
694
if (inspectedLanguageOverrides.has(languageFilter)) {
695
const overrideValue = inspectedLanguageOverrides.get(languageFilter)![targetOverrideSelector]?.override;
696
if (typeof overrideValue !== 'undefined') {
697
isConfigured = true;
698
}
699
}
700
}
701
}
702
703
return { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector: languageFilter };
704
}
705
706
function sanitizeId(id: string): string {
707
return id.replace(/[\.\/]/, '_');
708
}
709
710
export function settingKeyToDisplayFormat(key: string, groupId: string = '', isLanguageTagSetting: boolean = false): { category: string; label: string } {
711
const lastDotIdx = key.lastIndexOf('.');
712
let category = '';
713
if (lastDotIdx >= 0) {
714
category = key.substring(0, lastDotIdx);
715
key = key.substring(lastDotIdx + 1);
716
}
717
718
groupId = groupId.replace(/\//g, '.');
719
category = trimCategoryForGroup(category, groupId);
720
category = wordifyKey(category);
721
722
if (isLanguageTagSetting) {
723
key = getLanguageTagSettingPlainKey(key);
724
key = '$(bracket) ' + key;
725
}
726
727
const label = wordifyKey(key);
728
return { category, label };
729
}
730
731
/**
732
* Removes redundant sections of the category label.
733
* A redundant section is a section already reflected in the groupId.
734
*
735
* @param category The category of the specific setting.
736
* @param groupId The author + extension ID.
737
* @returns The new category label to use.
738
*/
739
function trimCategoryForGroup(category: string, groupId: string): string {
740
const doTrim = (forward: boolean) => {
741
// Remove the Insiders portion if the category doesn't use it.
742
if (!/insiders$/i.test(category)) {
743
groupId = groupId.replace(/-?insiders$/i, '');
744
}
745
const parts = groupId.split('.')
746
.map(part => {
747
// Remove hyphens, but only if that results in a match with the category.
748
if (part.replace(/-/g, '').toLowerCase() === category.toLowerCase()) {
749
return part.replace(/-/g, '');
750
} else {
751
return part;
752
}
753
});
754
while (parts.length) {
755
const reg = new RegExp(`^${parts.join('\\.')}(\\.|$)`, 'i');
756
if (reg.test(category)) {
757
return category.replace(reg, '');
758
}
759
760
if (forward) {
761
parts.pop();
762
} else {
763
parts.shift();
764
}
765
}
766
767
return null;
768
};
769
770
let trimmed = doTrim(true);
771
if (trimmed === null) {
772
trimmed = doTrim(false);
773
}
774
775
if (trimmed === null) {
776
trimmed = category;
777
}
778
779
return trimmed;
780
}
781
782
function isExtensionToggleSetting(setting: ISetting, productService: IProductService): boolean {
783
return ENABLE_EXTENSION_TOGGLE_SETTINGS &&
784
!!productService.extensionRecommendations &&
785
!!setting.displayExtensionId;
786
}
787
788
function isExcludeSetting(setting: ISetting): boolean {
789
return setting.key === 'files.exclude' ||
790
setting.key === 'search.exclude' ||
791
setting.key === 'workbench.localHistory.exclude' ||
792
setting.key === 'explorer.autoRevealExclude' ||
793
setting.key === 'files.readonlyExclude' ||
794
setting.key === 'files.watcherExclude';
795
}
796
797
function isIncludeSetting(setting: ISetting): boolean {
798
return setting.key === 'files.readonlyInclude';
799
}
800
801
// The values of the following settings when a default values has been removed
802
export function objectSettingSupportsRemoveDefaultValue(key: string): boolean {
803
return key === 'workbench.editor.customLabels.patterns';
804
}
805
806
function isSimpleType(type: string | undefined): boolean {
807
return type === 'string' || type === 'boolean' || type === 'integer' || type === 'number';
808
}
809
810
function getObjectRenderableSchemaType(schema: IJSONSchema, key: string): 'simple' | 'complex' | false {
811
const { type } = schema;
812
813
if (Array.isArray(type)) {
814
if (objectSettingSupportsRemoveDefaultValue(key) && type.length === 2) {
815
if (type.includes('null') && (type.includes('string') || type.includes('boolean') || type.includes('integer') || type.includes('number'))) {
816
return 'simple';
817
}
818
}
819
820
for (const t of type) {
821
if (!isSimpleType(t)) {
822
return false;
823
}
824
}
825
return 'complex';
826
}
827
828
if (isSimpleType(type)) {
829
return 'simple';
830
}
831
832
if (type === 'array') {
833
if (schema.items) {
834
const itemSchemas = Array.isArray(schema.items) ? schema.items : [schema.items];
835
for (const { type } of itemSchemas) {
836
if (Array.isArray(type)) {
837
for (const t of type) {
838
if (!isSimpleType(t)) {
839
return false;
840
}
841
}
842
return 'complex';
843
}
844
if (!isSimpleType(type)) {
845
return false;
846
}
847
return 'complex';
848
}
849
}
850
return false;
851
}
852
853
return false;
854
}
855
856
function getObjectSettingSchemaType({
857
key,
858
type,
859
objectProperties,
860
objectPatternProperties,
861
objectAdditionalProperties
862
}: ISetting): 'simple' | 'complex' | false {
863
if (type !== 'object') {
864
return false;
865
}
866
867
// object can have any shape
868
if (
869
isUndefinedOrNull(objectProperties) &&
870
isUndefinedOrNull(objectPatternProperties) &&
871
isUndefinedOrNull(objectAdditionalProperties)
872
) {
873
return false;
874
}
875
876
// objectAdditionalProperties allow the setting to have any shape,
877
// but if there's a pattern property that handles everything, then every
878
// property will match that patternProperty, so we don't need to look at
879
// the value of objectAdditionalProperties in that case.
880
if ((objectAdditionalProperties === true || objectAdditionalProperties === undefined)
881
&& !Object.keys(objectPatternProperties ?? {}).includes('.*')) {
882
return false;
883
}
884
885
const schemas = [...Object.values(objectProperties ?? {}), ...Object.values(objectPatternProperties ?? {})];
886
887
if (objectAdditionalProperties && typeof objectAdditionalProperties === 'object') {
888
schemas.push(objectAdditionalProperties);
889
}
890
891
let schemaType: 'simple' | 'complex' | false = 'simple';
892
for (const schema of schemas) {
893
for (const subSchema of Array.isArray(schema.anyOf) ? schema.anyOf : [schema]) {
894
const subSchemaType = getObjectRenderableSchemaType(subSchema, key);
895
if (subSchemaType === false) {
896
return false;
897
}
898
if (subSchemaType === 'complex') {
899
schemaType = 'complex';
900
}
901
}
902
}
903
904
return schemaType;
905
}
906
907
function settingTypeEnumRenderable(_type: string | string[]) {
908
const enumRenderableSettingTypes = ['string', 'boolean', 'null', 'integer', 'number'];
909
const type = Array.isArray(_type) ? _type : [_type];
910
return type.every(type => enumRenderableSettingTypes.includes(type));
911
}
912
913
export const enum SearchResultIdx {
914
Local = 0,
915
Remote = 1,
916
NewExtensions = 2,
917
Embeddings = 3,
918
AiSelected = 4
919
}
920
921
export class SearchResultModel extends SettingsTreeModel {
922
private rawSearchResults: ISearchResult[] | null = null;
923
private cachedUniqueSearchResults: Map<boolean, ISearchResult | null>;
924
private newExtensionSearchResults: ISearchResult | null = null;
925
private searchResultCount: number | null = null;
926
private settingsOrderByTocIndex: Map<string, number> | null;
927
private aiFilterEnabled: boolean = false;
928
929
readonly id = 'searchResultModel';
930
931
constructor(
932
viewState: ISettingsEditorViewState,
933
settingsOrderByTocIndex: Map<string, number> | null,
934
isWorkspaceTrusted: boolean,
935
@IWorkbenchConfigurationService configurationService: IWorkbenchConfigurationService,
936
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
937
@ILanguageService languageService: ILanguageService,
938
@IUserDataProfileService userDataProfileService: IUserDataProfileService,
939
@IProductService productService: IProductService
940
) {
941
super(viewState, isWorkspaceTrusted, configurationService, languageService, userDataProfileService, productService);
942
this.settingsOrderByTocIndex = settingsOrderByTocIndex;
943
this.cachedUniqueSearchResults = new Map();
944
this.update({ id: 'searchResultModel', label: '' });
945
}
946
947
set showAiResults(show: boolean) {
948
this.aiFilterEnabled = show;
949
this.updateChildren();
950
}
951
952
private sortResults(filterMatches: ISettingMatch[]): ISettingMatch[] {
953
if (this.settingsOrderByTocIndex) {
954
for (const match of filterMatches) {
955
match.setting.internalOrder = this.settingsOrderByTocIndex.get(match.setting.key);
956
}
957
}
958
959
// The search only has filters, so we can sort by the order in the TOC.
960
if (!this._viewState.query) {
961
return filterMatches.sort((a, b) => compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder));
962
}
963
964
// Sort the settings according to their relevancy.
965
// https://github.com/microsoft/vscode/issues/197773
966
filterMatches.sort((a, b) => {
967
if (a.matchType !== b.matchType) {
968
// Sort by match type if the match types are not the same.
969
// The priority of the match type is given by the SettingMatchType enum.
970
return b.matchType - a.matchType;
971
} else if ((a.matchType & SettingMatchType.NonContiguousWordsInSettingsLabel) || (a.matchType & SettingMatchType.ContiguousWordsInSettingsLabel)) {
972
// The match types of a and b are the same and can be sorted by their number of matched words.
973
// If those numbers are the same, sort by the order in the table of contents.
974
return (b.keyMatchScore - a.keyMatchScore) || compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder);
975
} else if (a.matchType === SettingMatchType.RemoteMatch) {
976
// The match types are the same and are RemoteMatch.
977
// Sort by score.
978
return b.score - a.score;
979
} else {
980
// The match types are the same but are not RemoteMatch.
981
// Sort by their order in the table of contents.
982
return compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder);
983
}
984
});
985
986
// Remove duplicates, which sometimes occur with settings
987
// such as the experimental toggle setting.
988
return arrays.distinct(filterMatches, (match) => match.setting.key);
989
}
990
991
getUniqueSearchResults(): ISearchResult | null {
992
const cachedResults = this.cachedUniqueSearchResults.get(this.aiFilterEnabled);
993
if (cachedResults) {
994
return cachedResults;
995
}
996
997
if (!this.rawSearchResults) {
998
return null;
999
}
1000
1001
let combinedFilterMatches: ISettingMatch[] = [];
1002
1003
if (this.aiFilterEnabled) {
1004
const aiSelectedKeys = new Set<string>();
1005
const aiSelectedResult = this.rawSearchResults[SearchResultIdx.AiSelected];
1006
if (aiSelectedResult) {
1007
aiSelectedResult.filterMatches.forEach(m => aiSelectedKeys.add(m.setting.key));
1008
combinedFilterMatches = aiSelectedResult.filterMatches;
1009
}
1010
1011
const embeddingsResult = this.rawSearchResults[SearchResultIdx.Embeddings];
1012
if (embeddingsResult) {
1013
embeddingsResult.filterMatches = embeddingsResult.filterMatches.filter(m => !aiSelectedKeys.has(m.setting.key));
1014
combinedFilterMatches = combinedFilterMatches.concat(embeddingsResult.filterMatches);
1015
}
1016
const result = {
1017
filterMatches: combinedFilterMatches,
1018
exactMatch: false
1019
};
1020
this.cachedUniqueSearchResults.set(true, result);
1021
return result;
1022
}
1023
1024
const localMatchKeys = new Set<string>();
1025
const localResult = this.rawSearchResults[SearchResultIdx.Local];
1026
if (localResult) {
1027
localResult.filterMatches.forEach(m => localMatchKeys.add(m.setting.key));
1028
combinedFilterMatches = localResult.filterMatches;
1029
}
1030
1031
const remoteResult = this.rawSearchResults[SearchResultIdx.Remote];
1032
if (remoteResult) {
1033
remoteResult.filterMatches = remoteResult.filterMatches.filter(m => !localMatchKeys.has(m.setting.key));
1034
combinedFilterMatches = combinedFilterMatches.concat(remoteResult.filterMatches);
1035
1036
this.newExtensionSearchResults = this.rawSearchResults[SearchResultIdx.NewExtensions];
1037
}
1038
combinedFilterMatches = this.sortResults(combinedFilterMatches);
1039
const result = {
1040
filterMatches: combinedFilterMatches,
1041
exactMatch: localResult.exactMatch // remote results should never have an exact match
1042
};
1043
this.cachedUniqueSearchResults.set(false, result);
1044
return result;
1045
}
1046
1047
getRawResults(): ISearchResult[] {
1048
return this.rawSearchResults ?? [];
1049
}
1050
1051
private getUniqueSearchResultSettings(): ISetting[] {
1052
return this.getUniqueSearchResults()?.filterMatches.map(m => m.setting) ?? [];
1053
}
1054
1055
updateChildren(): void {
1056
this.update({
1057
id: 'searchResultModel',
1058
label: 'searchResultModel',
1059
settings: this.getUniqueSearchResultSettings()
1060
});
1061
1062
// Save time by filtering children in the search model instead of relying on the tree filter, which still requires heights to be calculated.
1063
const isRemote = !!this.environmentService.remoteAuthority;
1064
1065
const newChildren = [];
1066
for (const child of this.root.children) {
1067
if (child instanceof SettingsTreeSettingElement
1068
&& child.matchesAllTags(this._viewState.tagFilters)
1069
&& child.matchesScope(this._viewState.settingsTarget, isRemote)
1070
&& child.matchesAnyExtension(this._viewState.extensionFilters)
1071
&& child.matchesAnyId(this._viewState.idFilters)
1072
&& child.matchesAnyFeature(this._viewState.featureFilters)
1073
&& child.matchesAllLanguages(this._viewState.languageFilter)) {
1074
newChildren.push(child);
1075
} else {
1076
child.dispose();
1077
}
1078
}
1079
this.root.children = newChildren;
1080
this.searchResultCount = this.root.children.length;
1081
1082
if (this.newExtensionSearchResults?.filterMatches.length) {
1083
let resultExtensionIds = this.newExtensionSearchResults.filterMatches
1084
.map(result => (<IExtensionSetting>result.setting))
1085
.filter(setting => setting.extensionName && setting.extensionPublisher)
1086
.map(setting => `${setting.extensionPublisher}.${setting.extensionName}`);
1087
resultExtensionIds = arrays.distinct(resultExtensionIds);
1088
1089
if (resultExtensionIds.length) {
1090
const newExtElement = new SettingsTreeNewExtensionsElement('newExtensions', resultExtensionIds);
1091
newExtElement.parent = this._root;
1092
this._root.children.push(newExtElement);
1093
}
1094
}
1095
}
1096
1097
setResult(order: SearchResultIdx, result: ISearchResult | null): void {
1098
this.cachedUniqueSearchResults.clear();
1099
this.newExtensionSearchResults = null;
1100
1101
if (this.rawSearchResults && order === SearchResultIdx.Local) {
1102
// To prevent the Settings editor from showing
1103
// stale remote results mid-search.
1104
delete this.rawSearchResults[SearchResultIdx.Remote];
1105
}
1106
1107
this.rawSearchResults ??= [];
1108
if (!result) {
1109
delete this.rawSearchResults[order];
1110
return;
1111
}
1112
1113
this.rawSearchResults[order] = result;
1114
this.updateChildren();
1115
}
1116
1117
getUniqueResultsCount(): number {
1118
return this.searchResultCount ?? 0;
1119
}
1120
}
1121
1122
export interface IParsedQuery {
1123
tags: string[];
1124
query: string;
1125
extensionFilters: string[];
1126
idFilters: string[];
1127
featureFilters: string[];
1128
languageFilter: string | undefined;
1129
}
1130
1131
const tagRegex = /(^|\s)@tag:("([^"]*)"|[^"]\S*)/g;
1132
const extensionRegex = /(^|\s)@ext:("([^"]*)"|[^"]\S*)?/g;
1133
const featureRegex = /(^|\s)@feature:("([^"]*)"|[^"]\S*)?/g;
1134
const idRegex = /(^|\s)@id:("([^"]*)"|[^"]\S*)?/g;
1135
const languageRegex = /(^|\s)@lang:("([^"]*)"|[^"]\S*)?/g;
1136
1137
export function parseQuery(query: string): IParsedQuery {
1138
/**
1139
* A helper function to parse the query on one type of regex.
1140
*
1141
* @param query The search query
1142
* @param filterRegex The regex to use on the query
1143
* @param parsedParts The parts that the regex parses out will be appended to the array passed in here.
1144
* @returns The query with the parsed parts removed
1145
*/
1146
function getTagsForType(query: string, filterRegex: RegExp, parsedParts: string[]): string {
1147
return query.replace(filterRegex, (_, __, quotedParsedElement, unquotedParsedElement) => {
1148
const parsedElement: string = unquotedParsedElement || quotedParsedElement;
1149
if (parsedElement) {
1150
parsedParts.push(...parsedElement.split(',').map(s => s.trim()).filter(s => !isFalsyOrWhitespace(s)));
1151
}
1152
return '';
1153
});
1154
}
1155
1156
const tags: string[] = [];
1157
query = query.replace(tagRegex, (_, __, quotedTag, tag) => {
1158
tags.push(tag || quotedTag);
1159
return '';
1160
});
1161
1162
query = query.replace(`@${MODIFIED_SETTING_TAG}`, () => {
1163
tags.push(MODIFIED_SETTING_TAG);
1164
return '';
1165
});
1166
1167
query = query.replace(`@${POLICY_SETTING_TAG}`, () => {
1168
tags.push(POLICY_SETTING_TAG);
1169
return '';
1170
});
1171
1172
const extensions: string[] = [];
1173
const features: string[] = [];
1174
const ids: string[] = [];
1175
const langs: string[] = [];
1176
query = getTagsForType(query, extensionRegex, extensions);
1177
query = getTagsForType(query, featureRegex, features);
1178
query = getTagsForType(query, idRegex, ids);
1179
1180
if (ENABLE_LANGUAGE_FILTER) {
1181
query = getTagsForType(query, languageRegex, langs);
1182
}
1183
1184
query = query.trim();
1185
1186
// For now, only return the first found language filter
1187
return {
1188
tags,
1189
extensionFilters: extensions,
1190
featureFilters: features,
1191
idFilters: ids,
1192
languageFilter: langs.length ? langs[0] : undefined,
1193
query,
1194
};
1195
}
1196
1197