Path: blob/main/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts
5255 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as arrays from '../../../../base/common/arrays.js';6import { Emitter } from '../../../../base/common/event.js';7import { IJSONSchema } from '../../../../base/common/jsonSchema.js';8import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';9import { escapeRegExpCharacters, isFalsyOrWhitespace } from '../../../../base/common/strings.js';10import { isUndefinedOrNull } from '../../../../base/common/types.js';11import { URI } from '../../../../base/common/uri.js';12import { ILanguageService } from '../../../../editor/common/languages/language.js';13import { ConfigurationTarget, getLanguageTagSettingPlainKey, IConfigurationValue } from '../../../../platform/configuration/common/configuration.js';14import { ConfigurationDefaultValueSource, ConfigurationScope, EditPresentationTypes, Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';15import { IProductService } from '../../../../platform/product/common/productService.js';16import { Registry } from '../../../../platform/registry/common/platform.js';17import { USER_LOCAL_AND_REMOTE_SETTINGS } from '../../../../platform/request/common/request.js';18import { APPLICATION_SCOPES, FOLDER_SCOPES, IWorkbenchConfigurationService, LOCAL_MACHINE_SCOPES, REMOTE_MACHINE_SCOPES, WORKSPACE_SCOPES } from '../../../services/configuration/common/configuration.js';19import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';20import { IExtensionSetting, ISearchResult, ISetting, ISettingMatch, SettingMatchType, SettingValueType } from '../../../services/preferences/common/preferences.js';21import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js';22import { ENABLE_EXTENSION_TOGGLE_SETTINGS, ENABLE_LANGUAGE_FILTER, MODIFIED_SETTING_TAG, POLICY_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, compareTwoNullableNumbers, wordifyKey } from '../common/preferences.js';23import { SettingsTarget } from './preferencesWidgets.js';24import { ITOCEntry, tocData } from './settingsLayout.js';2526export const ONLINE_SERVICES_SETTING_TAG = 'usesOnlineServices';2728export interface ISettingsEditorViewState {29settingsTarget: SettingsTarget;30query?: string; // used to keep track of loading from setInput vs loading from cache31tagFilters?: Set<string>;32extensionFilters?: Set<string>;33featureFilters?: Set<string>;34idFilters?: Set<string>;35languageFilter?: string;36categoryFilter?: SettingsTreeGroupElement;37}3839export abstract class SettingsTreeElement extends Disposable {40id: string;41parent?: SettingsTreeGroupElement;4243private _tabbable = false;4445private readonly _onDidChangeTabbable = this._register(new Emitter<void>());46get onDidChangeTabbable() { return this._onDidChangeTabbable.event; }4748constructor(_id: string) {49super();50this.id = _id;51}5253get tabbable(): boolean {54return this._tabbable;55}5657set tabbable(value: boolean) {58this._tabbable = value;59this._onDidChangeTabbable.fire();60}61}6263export type SettingsTreeGroupChild = (SettingsTreeGroupElement | SettingsTreeSettingElement | SettingsTreeNewExtensionsElement);6465export class SettingsTreeGroupElement extends SettingsTreeElement {66count?: number;67label: string;68level: number;69isFirstGroup: boolean;7071private _childSettingKeys: Set<string> = new Set();72private _children: SettingsTreeGroupChild[] = [];7374get children(): SettingsTreeGroupChild[] {75return this._children;76}7778set children(newChildren: SettingsTreeGroupChild[]) {79this._children = newChildren;8081this._childSettingKeys = new Set();82this._children.forEach(child => {83if (child instanceof SettingsTreeSettingElement) {84this._childSettingKeys.add(child.setting.key);85}86});87}8889constructor(_id: string, count: number | undefined, label: string, level: number, isFirstGroup: boolean) {90super(_id);9192this.count = count;93this.label = label;94this.level = level;95this.isFirstGroup = isFirstGroup;96}9798/**99* Returns whether this group contains the given child key (to a depth of 1 only)100*/101containsSetting(key: string): boolean {102return this._childSettingKeys.has(key);103}104}105106export class SettingsTreeNewExtensionsElement extends SettingsTreeElement {107constructor(_id: string, public readonly extensionIds: string[]) {108super(_id);109}110}111112export class SettingsTreeSettingElement extends SettingsTreeElement {113private static readonly MAX_DESC_LINES = 20;114115setting: ISetting;116117private _displayCategory: string | null = null;118private _displayLabel: string | null = null;119120/**121* scopeValue || defaultValue, for rendering convenience.122*/123value: any;124125/**126* The value in the current settings scope.127*/128scopeValue: any;129130/**131* The default value132*/133defaultValue?: any;134135/**136* The source of the default value to display.137* This value also accounts for extension-contributed language-specific default value overrides.138*/139defaultValueSource: ConfigurationDefaultValueSource | undefined;140141/**142* Whether the setting is configured in the selected scope.143*/144isConfigured = false;145146/**147* Whether the setting requires trusted target148*/149isUntrusted = false;150151/**152* Whether the setting is under a policy that blocks all changes.153*/154hasPolicyValue = false;155156tags?: Set<string>;157overriddenScopeList: string[] = [];158overriddenDefaultsLanguageList: string[] = [];159160/**161* For each language that contributes setting values or default overrides, we can see those values here.162*/163languageOverrideValues: Map<string, IConfigurationValue<unknown>> = new Map<string, IConfigurationValue<unknown>>();164165description!: string;166valueType!: SettingValueType;167168constructor(169setting: ISetting,170parent: SettingsTreeGroupElement,171readonly settingsTarget: SettingsTarget,172private readonly isWorkspaceTrusted: boolean,173private readonly languageFilter: string | undefined,174private readonly languageService: ILanguageService,175private readonly productService: IProductService,176private readonly userDataProfileService: IUserDataProfileService,177private readonly configurationService: IWorkbenchConfigurationService,178) {179super(sanitizeId(parent.id + '_' + setting.key));180this.setting = setting;181this.parent = parent;182183// Make sure description and valueType are initialized184this.initSettingDescription();185this.initSettingValueType();186}187188get displayCategory(): string {189if (!this._displayCategory) {190this.initLabels();191}192193return this._displayCategory!;194}195196get displayLabel(): string {197if (!this._displayLabel) {198this.initLabels();199}200201return this._displayLabel!;202}203204private initLabels(): void {205if (this.setting.title) {206this._displayLabel = this.setting.title;207this._displayCategory = this.setting.categoryLabel ?? null;208return;209}210const displayKeyFormat = settingKeyToDisplayFormat(this.setting.key, this.parent!.id, this.setting.isLanguageTagSetting);211this._displayLabel = displayKeyFormat.label;212this._displayCategory = displayKeyFormat.category;213}214215private initSettingDescription() {216if (this.setting.description.length > SettingsTreeSettingElement.MAX_DESC_LINES) {217const truncatedDescLines = this.setting.description.slice(0, SettingsTreeSettingElement.MAX_DESC_LINES);218truncatedDescLines.push('[...]');219this.description = truncatedDescLines.join('\n');220} else {221this.description = this.setting.description.join('\n');222}223}224225private initSettingValueType() {226if (isExtensionToggleSetting(this.setting, this.productService)) {227this.valueType = SettingValueType.ExtensionToggle;228} else if (this.setting.enum && (!this.setting.type || settingTypeEnumRenderable(this.setting.type))) {229this.valueType = SettingValueType.Enum;230} else if (this.setting.type === 'string') {231if (this.setting.editPresentation === EditPresentationTypes.Multiline) {232this.valueType = SettingValueType.MultilineString;233} else {234this.valueType = SettingValueType.String;235}236} else if (isExcludeSetting(this.setting)) {237this.valueType = SettingValueType.Exclude;238} else if (isIncludeSetting(this.setting)) {239this.valueType = SettingValueType.Include;240} else if (this.setting.type === 'integer') {241this.valueType = SettingValueType.Integer;242} else if (this.setting.type === 'number') {243this.valueType = SettingValueType.Number;244} else if (this.setting.type === 'boolean') {245this.valueType = SettingValueType.Boolean;246} else if (this.setting.type === 'array' && this.setting.arrayItemType &&247['string', 'enum', 'number', 'integer'].includes(this.setting.arrayItemType)) {248this.valueType = SettingValueType.Array;249} else if (Array.isArray(this.setting.type) && this.setting.type.includes(SettingValueType.Null) && this.setting.type.length === 2) {250if (this.setting.type.includes(SettingValueType.Integer)) {251this.valueType = SettingValueType.NullableInteger;252} else if (this.setting.type.includes(SettingValueType.Number)) {253this.valueType = SettingValueType.NullableNumber;254} else {255this.valueType = SettingValueType.Complex;256}257} else {258const schemaType = getObjectSettingSchemaType(this.setting);259if (schemaType) {260if (this.setting.allKeysAreBoolean) {261this.valueType = SettingValueType.BooleanObject;262} else if (schemaType === 'simple') {263this.valueType = SettingValueType.Object;264} else {265this.valueType = SettingValueType.ComplexObject;266}267} else if (this.setting.isLanguageTagSetting) {268this.valueType = SettingValueType.LanguageTag;269} else {270this.valueType = SettingValueType.Complex;271}272}273}274275inspectSelf() {276const targetToInspect = this.getTargetToInspect(this.setting);277const inspectResult = inspectSetting(this.setting.key, targetToInspect, this.languageFilter, this.configurationService);278this.update(inspectResult, this.isWorkspaceTrusted);279}280281private getTargetToInspect(setting: ISetting): SettingsTarget {282if (!this.userDataProfileService.currentProfile.isDefault && !this.userDataProfileService.currentProfile.useDefaultFlags?.settings) {283if (setting.scope === ConfigurationScope.APPLICATION) {284return ConfigurationTarget.APPLICATION;285}286if (this.configurationService.isSettingAppliedForAllProfiles(setting.key) && this.settingsTarget === ConfigurationTarget.USER_LOCAL) {287return ConfigurationTarget.APPLICATION;288}289}290return this.settingsTarget;291}292293private update(inspectResult: IInspectResult, isWorkspaceTrusted: boolean): void {294let { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector } = inspectResult;295296switch (targetSelector) {297case 'workspaceFolderValue':298case 'workspaceValue':299this.isUntrusted = !!this.setting.restricted && !isWorkspaceTrusted;300break;301}302303let displayValue = isConfigured ? inspected[targetSelector] : inspected.defaultValue;304const overriddenScopeList: string[] = [];305const overriddenDefaultsLanguageList: string[] = [];306if ((languageSelector || targetSelector !== 'workspaceValue') && typeof inspected.workspaceValue !== 'undefined') {307overriddenScopeList.push('workspace:');308}309if ((languageSelector || targetSelector !== 'userRemoteValue') && typeof inspected.userRemoteValue !== 'undefined') {310overriddenScopeList.push('remote:');311}312if ((languageSelector || targetSelector !== 'userLocalValue') && typeof inspected.userLocalValue !== 'undefined') {313overriddenScopeList.push('user:');314}315316if (inspected.overrideIdentifiers) {317for (const overrideIdentifier of inspected.overrideIdentifiers) {318const inspectedOverride = inspectedLanguageOverrides.get(overrideIdentifier);319if (inspectedOverride) {320if (this.languageService.isRegisteredLanguageId(overrideIdentifier)) {321if (languageSelector !== overrideIdentifier && typeof inspectedOverride.default?.override !== 'undefined') {322overriddenDefaultsLanguageList.push(overrideIdentifier);323}324if ((languageSelector !== overrideIdentifier || targetSelector !== 'workspaceValue') && typeof inspectedOverride.workspace?.override !== 'undefined') {325overriddenScopeList.push(`workspace:${overrideIdentifier}`);326}327if ((languageSelector !== overrideIdentifier || targetSelector !== 'userRemoteValue') && typeof inspectedOverride.userRemote?.override !== 'undefined') {328overriddenScopeList.push(`remote:${overrideIdentifier}`);329}330if ((languageSelector !== overrideIdentifier || targetSelector !== 'userLocalValue') && typeof inspectedOverride.userLocal?.override !== 'undefined') {331overriddenScopeList.push(`user:${overrideIdentifier}`);332}333}334this.languageOverrideValues.set(overrideIdentifier, inspectedOverride);335}336}337}338this.overriddenScopeList = overriddenScopeList;339this.overriddenDefaultsLanguageList = overriddenDefaultsLanguageList;340341// The user might have added, removed, or modified a language filter,342// so we reset the default value source to the non-language-specific default value source for now.343this.defaultValueSource = this.setting.nonLanguageSpecificDefaultValueSource;344345if (inspected.policyValue !== undefined) {346this.hasPolicyValue = true;347isConfigured = false; // The user did not manually configure the setting themselves.348displayValue = inspected.policyValue;349this.scopeValue = inspected.policyValue;350this.defaultValue = inspected.defaultValue;351} else if (languageSelector && this.languageOverrideValues.has(languageSelector)) {352const overrideValues = this.languageOverrideValues.get(languageSelector)!;353// In the worst case, go back to using the previous display value.354// Also, sometimes the override is in the form of a default value override, so consider that second.355displayValue = (isConfigured ? overrideValues[targetSelector] : overrideValues.defaultValue) ?? displayValue;356this.scopeValue = isConfigured && overrideValues[targetSelector];357this.defaultValue = overrideValues.defaultValue ?? inspected.defaultValue;358359const registryValues = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationDefaultsOverrides();360const source = registryValues.get(`[${languageSelector}]`)?.source;361const overrideValueSource = source instanceof Map ? source.get(this.setting.key) : undefined;362if (overrideValueSource) {363this.defaultValueSource = overrideValueSource;364}365} else {366this.scopeValue = isConfigured && inspected[targetSelector];367this.defaultValue = inspected.defaultValue;368}369370this.value = displayValue;371this.isConfigured = isConfigured;372if (isConfigured || this.setting.tags || this.tags || this.setting.restricted || this.hasPolicyValue) {373// Don't create an empty Set for all 1000 settings, only if needed374this.tags = new Set<string>();375if (isConfigured) {376this.tags.add(MODIFIED_SETTING_TAG);377}378379this.setting.tags?.forEach(tag => this.tags!.add(tag));380381if (this.setting.restricted) {382this.tags.add(REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG);383}384385if (this.hasPolicyValue) {386this.tags.add(POLICY_SETTING_TAG);387}388}389}390391matchesAllTags(tagFilters?: Set<string>): boolean {392if (!tagFilters?.size) {393// This setting, which may have tags,394// matches against a query with no tags.395return true;396}397398if (!this.tags) {399// The setting must inspect itself to get tag information400// including for the hasPolicy tag.401this.inspectSelf();402}403404// Handle the special 'stable' tag filter405if (tagFilters.has('stable')) {406// For stable filter, exclude preview and experimental settings407if (this.tags?.has('preview') || this.tags?.has('experimental')) {408return false;409}410// Check other filters (excluding 'stable' itself)411const otherFilters = new Set(Array.from(tagFilters).filter(tag => tag !== 'stable'));412if (otherFilters.size === 0) {413return true;414}415return !!this.tags?.size &&416Array.from(otherFilters).every(tag => this.tags!.has(tag));417}418419// Check that the filter tags are a subset of this setting's tags420return !!this.tags?.size &&421Array.from(tagFilters).every(tag => this.tags!.has(tag));422}423424matchesScope(scope: SettingsTarget, isRemote: boolean): boolean {425const configTarget = URI.isUri(scope) ? ConfigurationTarget.WORKSPACE_FOLDER : scope;426427if (!this.setting.scope) {428return true;429}430431if (configTarget === ConfigurationTarget.APPLICATION) {432return APPLICATION_SCOPES.includes(this.setting.scope);433}434435if (configTarget === ConfigurationTarget.WORKSPACE_FOLDER) {436return FOLDER_SCOPES.includes(this.setting.scope);437}438439if (configTarget === ConfigurationTarget.WORKSPACE) {440return WORKSPACE_SCOPES.includes(this.setting.scope);441}442443if (configTarget === ConfigurationTarget.USER_REMOTE) {444return REMOTE_MACHINE_SCOPES.includes(this.setting.scope) || USER_LOCAL_AND_REMOTE_SETTINGS.includes(this.setting.key);445}446447if (configTarget === ConfigurationTarget.USER_LOCAL) {448if (isRemote) {449return LOCAL_MACHINE_SCOPES.includes(this.setting.scope) || USER_LOCAL_AND_REMOTE_SETTINGS.includes(this.setting.key);450}451}452453return true;454}455456matchesAnyExtension(extensionFilters?: Set<string>): boolean {457if (!extensionFilters || !extensionFilters.size) {458return true;459}460461if (!this.setting.extensionInfo) {462return false;463}464465return Array.from(extensionFilters).some(extensionId => extensionId.toLowerCase() === this.setting.extensionInfo!.id.toLowerCase());466}467468matchesAnyFeature(featureFilters?: Set<string>): boolean {469if (!featureFilters || !featureFilters.size) {470return true;471}472473const features = tocData.children!.find(child => child.id === 'features');474475return Array.from(featureFilters).some(filter => {476if (features && features.children) {477const feature = features.children.find(feature => 'features/' + filter === feature.id);478if (feature) {479const patterns = feature.settings?.map(setting => createSettingMatchRegExp(setting));480return patterns && !this.setting.extensionInfo && patterns.some(pattern => pattern.test(this.setting.key.toLowerCase()));481} else {482return false;483}484} else {485return false;486}487});488}489490matchesAnyId(idFilters?: Set<string>): boolean {491if (!idFilters || !idFilters.size) {492return true;493}494495// Check for exact match first496if (idFilters.has(this.setting.key)) {497return true;498}499500// Check for wildcard patterns (ending with .*)501for (const filter of idFilters) {502if (filter.endsWith('*')) {503const prefix = filter.slice(0, -1); // Remove '*' suffix504if (this.setting.key.startsWith(prefix)) {505return true;506}507}508}509510return false;511}512513matchesAllLanguages(languageFilter?: string): boolean {514if (!languageFilter) {515// We're not filtering by language.516return true;517}518519if (!this.languageService.isRegisteredLanguageId(languageFilter)) {520// We're trying to filter by an invalid language.521return false;522}523524// We have a language filter in the search widget at this point.525// We decide to show all language overridable settings to make the526// lang filter act more like a scope filter,527// rather than adding on an implicit @modified as well.528if (this.setting.scope === ConfigurationScope.LANGUAGE_OVERRIDABLE) {529return true;530}531532return false;533}534}535536537function createSettingMatchRegExp(pattern: string): RegExp {538pattern = escapeRegExpCharacters(pattern)539.replace(/\\\*/g, '.*');540541return new RegExp(`^${pattern}$`, 'i');542}543544export class SettingsTreeModel implements IDisposable {545protected _root!: SettingsTreeGroupElement;546private _tocRoot!: ITOCEntry<ISetting>;547private readonly _treeElementsBySettingName = new Map<string, SettingsTreeSettingElement[]>();548549constructor(550protected readonly _viewState: ISettingsEditorViewState,551private _isWorkspaceTrusted: boolean,552@IWorkbenchConfigurationService private readonly _configurationService: IWorkbenchConfigurationService,553@ILanguageService private readonly _languageService: ILanguageService,554@IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService,555@IProductService private readonly _productService: IProductService556) {557}558559get root(): SettingsTreeGroupElement {560return this._root;561}562563update(newTocRoot = this._tocRoot): void {564this._treeElementsBySettingName.clear();565566const newRoot = this.createSettingsTreeGroupElement(newTocRoot);567if (newRoot.children[0] instanceof SettingsTreeGroupElement) {568(<SettingsTreeGroupElement>newRoot.children[0]).isFirstGroup = true;569}570571if (this._root) {572this.disposeChildren(this._root.children);573this._root.children = newRoot.children;574newRoot.dispose();575} else {576this._root = newRoot;577}578}579580updateWorkspaceTrust(workspaceTrusted: boolean): void {581this._isWorkspaceTrusted = workspaceTrusted;582this.updateRequireTrustedTargetElements();583}584585private disposeChildren(children: SettingsTreeGroupChild[]) {586for (const child of children) {587this.disposeChildAndRecurse(child);588}589}590591private disposeChildAndRecurse(element: SettingsTreeElement) {592if (element instanceof SettingsTreeGroupElement) {593this.disposeChildren(element.children);594}595596element.dispose();597}598599getElementsByName(name: string): SettingsTreeSettingElement[] | null {600return this._treeElementsBySettingName.get(name) ?? null;601}602603updateElementsByName(name: string): void {604if (!this._treeElementsBySettingName.has(name)) {605return;606}607608this.reinspectSettings(this._treeElementsBySettingName.get(name)!);609}610611private updateRequireTrustedTargetElements(): void {612this.reinspectSettings([...this._treeElementsBySettingName.values()].flat().filter(s => s.isUntrusted));613}614615private reinspectSettings(settings: SettingsTreeSettingElement[]): void {616for (const element of settings) {617element.inspectSelf();618}619}620621private createSettingsTreeGroupElement(tocEntry: ITOCEntry<ISetting>, parent?: SettingsTreeGroupElement): SettingsTreeGroupElement {622const depth = parent ? this.getDepth(parent) + 1 : 0;623const element = new SettingsTreeGroupElement(tocEntry.id, undefined, tocEntry.label, depth, false);624element.parent = parent;625626const children: SettingsTreeGroupChild[] = [];627if (tocEntry.settings) {628const settingChildren = tocEntry.settings.map(s => this.createSettingsTreeSettingElement(s, element));629for (const child of settingChildren) {630if (!child.setting.deprecationMessage) {631children.push(child);632} else {633child.inspectSelf();634if (child.isConfigured) {635children.push(child);636} else {637child.dispose();638}639}640}641}642643if (tocEntry.children) {644const groupChildren = tocEntry.children.map(child => this.createSettingsTreeGroupElement(child, element));645children.push(...groupChildren);646}647648element.children = children;649650return element;651}652653private getDepth(element: SettingsTreeElement): number {654if (element.parent) {655return 1 + this.getDepth(element.parent);656} else {657return 0;658}659}660661private createSettingsTreeSettingElement(setting: ISetting, parent: SettingsTreeGroupElement): SettingsTreeSettingElement {662const element = new SettingsTreeSettingElement(663setting,664parent,665this._viewState.settingsTarget,666this._isWorkspaceTrusted,667this._viewState.languageFilter,668this._languageService,669this._productService,670this._userDataProfileService,671this._configurationService);672673const nameElements = this._treeElementsBySettingName.get(setting.key) ?? [];674nameElements.push(element);675this._treeElementsBySettingName.set(setting.key, nameElements);676return element;677}678679dispose() {680this._treeElementsBySettingName.clear();681this.disposeChildAndRecurse(this._root);682}683}684685interface IInspectResult {686isConfigured: boolean;687inspected: IConfigurationValue<unknown>;688targetSelector: 'applicationValue' | 'userLocalValue' | 'userRemoteValue' | 'workspaceValue' | 'workspaceFolderValue';689inspectedLanguageOverrides: Map<string, IConfigurationValue<unknown>>;690languageSelector: string | undefined;691}692693export function inspectSetting(key: string, target: SettingsTarget, languageFilter: string | undefined, configurationService: IWorkbenchConfigurationService): IInspectResult {694const inspectOverrides = URI.isUri(target) ? { resource: target } : undefined;695const inspected = configurationService.inspect(key, inspectOverrides);696const targetSelector = target === ConfigurationTarget.APPLICATION ? 'applicationValue' :697target === ConfigurationTarget.USER_LOCAL ? 'userLocalValue' :698target === ConfigurationTarget.USER_REMOTE ? 'userRemoteValue' :699target === ConfigurationTarget.WORKSPACE ? 'workspaceValue' :700'workspaceFolderValue';701const targetOverrideSelector = target === ConfigurationTarget.APPLICATION ? 'application' :702target === ConfigurationTarget.USER_LOCAL ? 'userLocal' :703target === ConfigurationTarget.USER_REMOTE ? 'userRemote' :704target === ConfigurationTarget.WORKSPACE ? 'workspace' :705'workspaceFolder';706let isConfigured = typeof inspected[targetSelector] !== 'undefined';707708const overrideIdentifiers = inspected.overrideIdentifiers;709const inspectedLanguageOverrides = new Map<string, IConfigurationValue<unknown>>();710711// We must reset isConfigured to be false if languageFilter is set, and manually712// determine whether it can be set to true later.713if (languageFilter) {714isConfigured = false;715}716if (overrideIdentifiers) {717// The setting we're looking at has language overrides.718for (const overrideIdentifier of overrideIdentifiers) {719inspectedLanguageOverrides.set(overrideIdentifier, configurationService.inspect(key, { overrideIdentifier }));720}721722// For all language filters, see if there's an override for that filter.723if (languageFilter) {724if (inspectedLanguageOverrides.has(languageFilter)) {725const overrideValue = inspectedLanguageOverrides.get(languageFilter)![targetOverrideSelector]?.override;726if (typeof overrideValue !== 'undefined') {727isConfigured = true;728}729}730}731}732733return { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector: languageFilter };734}735736function sanitizeId(id: string): string {737return id.replace(/[\.\/]/, '_');738}739740export function settingKeyToDisplayFormat(key: string, groupId: string = '', isLanguageTagSetting: boolean = false): { category: string; label: string } {741const lastDotIdx = key.lastIndexOf('.');742let category = '';743if (lastDotIdx >= 0) {744category = key.substring(0, lastDotIdx);745key = key.substring(lastDotIdx + 1);746}747748groupId = groupId.replace(/\//g, '.');749category = trimCategoryForGroup(category, groupId);750category = wordifyKey(category);751752if (isLanguageTagSetting) {753key = getLanguageTagSettingPlainKey(key);754key = '$(bracket) ' + key;755}756757const label = wordifyKey(key);758return { category, label };759}760761/**762* Removes redundant sections of the category label.763* A redundant section is a section already reflected in the groupId.764*765* @param category The category of the specific setting.766* @param groupId The author + extension ID.767* @returns The new category label to use.768*/769function trimCategoryForGroup(category: string, groupId: string): string {770const doTrim = (forward: boolean) => {771// Remove the Insiders portion if the category doesn't use it.772if (!/insiders$/i.test(category)) {773groupId = groupId.replace(/-?insiders$/i, '');774}775const parts = groupId.split('.')776.map(part => {777// Remove hyphens, but only if that results in a match with the category.778if (part.replace(/-/g, '').toLowerCase() === category.toLowerCase()) {779return part.replace(/-/g, '');780} else {781return part;782}783});784while (parts.length) {785const reg = new RegExp(`^${parts.join('\\.')}(\\.|$)`, 'i');786if (reg.test(category)) {787return category.replace(reg, '');788}789790if (forward) {791parts.pop();792} else {793parts.shift();794}795}796797return null;798};799800let trimmed = doTrim(true);801if (trimmed === null) {802trimmed = doTrim(false);803}804805if (trimmed === null) {806trimmed = category;807}808809return trimmed;810}811812function isExtensionToggleSetting(setting: ISetting, productService: IProductService): boolean {813return ENABLE_EXTENSION_TOGGLE_SETTINGS &&814!!productService.extensionRecommendations &&815!!setting.displayExtensionId;816}817818function isExcludeSetting(setting: ISetting): boolean {819return setting.key === 'files.exclude' ||820setting.key === 'search.exclude' ||821setting.key === 'workbench.localHistory.exclude' ||822setting.key === 'explorer.autoRevealExclude' ||823setting.key === 'files.readonlyExclude' ||824setting.key === 'files.watcherExclude';825}826827function isIncludeSetting(setting: ISetting): boolean {828return setting.key === 'files.readonlyInclude';829}830831// The values of the following settings when a default values has been removed832export function objectSettingSupportsRemoveDefaultValue(key: string): boolean {833return key === 'workbench.editor.customLabels.patterns';834}835836function isSimpleType(type: string | undefined): boolean {837return type === 'string' || type === 'boolean' || type === 'integer' || type === 'number';838}839840function getObjectRenderableSchemaType(schema: IJSONSchema, key: string): 'simple' | 'complex' | false {841const { type } = schema;842843if (Array.isArray(type)) {844if (objectSettingSupportsRemoveDefaultValue(key) && type.length === 2) {845if (type.includes('null') && (type.includes('string') || type.includes('boolean') || type.includes('integer') || type.includes('number'))) {846return 'simple';847}848}849850for (const t of type) {851if (!isSimpleType(t)) {852return false;853}854}855return 'complex';856}857858if (isSimpleType(type)) {859return 'simple';860}861862if (type === 'array') {863if (schema.items) {864const itemSchemas = Array.isArray(schema.items) ? schema.items : [schema.items];865for (const { type } of itemSchemas) {866if (Array.isArray(type)) {867for (const t of type) {868if (!isSimpleType(t)) {869return false;870}871}872return 'complex';873}874if (!isSimpleType(type)) {875return false;876}877return 'complex';878}879}880return false;881}882883return false;884}885886function getObjectSettingSchemaType({887key,888type,889objectProperties,890objectPatternProperties,891objectAdditionalProperties892}: ISetting): 'simple' | 'complex' | false {893if (type !== 'object') {894return false;895}896897// object can have any shape898if (899isUndefinedOrNull(objectProperties) &&900isUndefinedOrNull(objectPatternProperties) &&901isUndefinedOrNull(objectAdditionalProperties)902) {903return false;904}905906// objectAdditionalProperties allow the setting to have any shape,907// but if there's a pattern property that handles everything, then every908// property will match that patternProperty, so we don't need to look at909// the value of objectAdditionalProperties in that case.910if ((objectAdditionalProperties === true || objectAdditionalProperties === undefined)911&& !Object.keys(objectPatternProperties ?? {}).includes('.*')) {912return false;913}914915const schemas = [...Object.values(objectProperties ?? {}), ...Object.values(objectPatternProperties ?? {})];916917if (objectAdditionalProperties && typeof objectAdditionalProperties === 'object') {918schemas.push(objectAdditionalProperties);919}920921let schemaType: 'simple' | 'complex' | false = 'simple';922for (const schema of schemas) {923for (const subSchema of Array.isArray(schema.anyOf) ? schema.anyOf : [schema]) {924const subSchemaType = getObjectRenderableSchemaType(subSchema, key);925if (subSchemaType === false) {926return false;927}928if (subSchemaType === 'complex') {929schemaType = 'complex';930}931}932}933934return schemaType;935}936937function settingTypeEnumRenderable(_type: string | string[]) {938const enumRenderableSettingTypes = ['string', 'boolean', 'null', 'integer', 'number'];939const type = Array.isArray(_type) ? _type : [_type];940return type.every(type => enumRenderableSettingTypes.includes(type));941}942943export const enum SearchResultIdx {944Local = 0,945Remote = 1,946NewExtensions = 2,947Embeddings = 3,948AiSelected = 4949}950951export class SearchResultModel extends SettingsTreeModel {952private rawSearchResults: ISearchResult[] | null = null;953private cachedUniqueSearchResults: Map<boolean, ISearchResult | null>;954private newExtensionSearchResults: ISearchResult | null = null;955private searchResultCount: number | null = null;956private settingsOrderByTocIndex: Map<string, number> | null;957private aiFilterEnabled: boolean = false;958959readonly id = 'searchResultModel';960961constructor(962viewState: ISettingsEditorViewState,963settingsOrderByTocIndex: Map<string, number> | null,964isWorkspaceTrusted: boolean,965@IWorkbenchConfigurationService configurationService: IWorkbenchConfigurationService,966@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,967@ILanguageService languageService: ILanguageService,968@IUserDataProfileService userDataProfileService: IUserDataProfileService,969@IProductService productService: IProductService970) {971super(viewState, isWorkspaceTrusted, configurationService, languageService, userDataProfileService, productService);972this.settingsOrderByTocIndex = settingsOrderByTocIndex;973this.cachedUniqueSearchResults = new Map();974this.update({ id: 'searchResultModel', label: '' });975}976977set showAiResults(show: boolean) {978this.aiFilterEnabled = show;979this.updateChildren();980}981982private sortResults(filterMatches: ISettingMatch[]): ISettingMatch[] {983if (this.settingsOrderByTocIndex) {984for (const match of filterMatches) {985match.setting.internalOrder = this.settingsOrderByTocIndex.get(match.setting.key);986}987}988989// The search only has filters, so we can sort by the order in the TOC.990if (!this._viewState.query) {991return filterMatches.sort((a, b) => compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder));992}993994// Sort the settings according to their relevancy.995// https://github.com/microsoft/vscode/issues/197773996filterMatches.sort((a, b) => {997if (a.matchType !== b.matchType) {998// Sort by match type if the match types are not the same.999// The priority of the match type is given by the SettingMatchType enum.1000return b.matchType - a.matchType;1001} else if ((a.matchType & SettingMatchType.NonContiguousWordsInSettingsLabel) || (a.matchType & SettingMatchType.ContiguousWordsInSettingsLabel)) {1002// The match types of a and b are the same and can be sorted by their number of matched words.1003// If those numbers are the same, sort by the order in the table of contents.1004return (b.keyMatchScore - a.keyMatchScore) || compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder);1005} else if (a.matchType === SettingMatchType.RemoteMatch) {1006// The match types are the same and are RemoteMatch.1007// Sort by score.1008return b.score - a.score;1009} else {1010// The match types are the same but are not RemoteMatch.1011// Sort by their order in the table of contents.1012return compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder);1013}1014});10151016// Remove duplicates, which sometimes occur with settings1017// such as the experimental toggle setting.1018return arrays.distinct(filterMatches, (match) => match.setting.key);1019}10201021getUniqueSearchResults(): ISearchResult | null {1022const cachedResults = this.cachedUniqueSearchResults.get(this.aiFilterEnabled);1023if (cachedResults) {1024return cachedResults;1025}10261027if (!this.rawSearchResults) {1028return null;1029}10301031let combinedFilterMatches: ISettingMatch[] = [];10321033if (this.aiFilterEnabled) {1034const aiSelectedKeys = new Set<string>();1035const aiSelectedResult = this.rawSearchResults[SearchResultIdx.AiSelected];1036if (aiSelectedResult) {1037aiSelectedResult.filterMatches.forEach(m => aiSelectedKeys.add(m.setting.key));1038combinedFilterMatches = aiSelectedResult.filterMatches;1039}10401041const embeddingsResult = this.rawSearchResults[SearchResultIdx.Embeddings];1042if (embeddingsResult) {1043embeddingsResult.filterMatches = embeddingsResult.filterMatches.filter(m => !aiSelectedKeys.has(m.setting.key));1044combinedFilterMatches = combinedFilterMatches.concat(embeddingsResult.filterMatches);1045}1046const result = {1047filterMatches: combinedFilterMatches,1048exactMatch: false1049};1050this.cachedUniqueSearchResults.set(true, result);1051return result;1052}10531054const localMatchKeys = new Set<string>();1055const localResult = this.rawSearchResults[SearchResultIdx.Local];1056if (localResult) {1057localResult.filterMatches.forEach(m => localMatchKeys.add(m.setting.key));1058combinedFilterMatches = localResult.filterMatches;1059}10601061const remoteResult = this.rawSearchResults[SearchResultIdx.Remote];1062if (remoteResult) {1063remoteResult.filterMatches = remoteResult.filterMatches.filter(m => !localMatchKeys.has(m.setting.key));1064combinedFilterMatches = combinedFilterMatches.concat(remoteResult.filterMatches);10651066this.newExtensionSearchResults = this.rawSearchResults[SearchResultIdx.NewExtensions];1067}1068combinedFilterMatches = this.sortResults(combinedFilterMatches);1069const result = {1070filterMatches: combinedFilterMatches,1071exactMatch: localResult.exactMatch // remote results should never have an exact match1072};1073this.cachedUniqueSearchResults.set(false, result);1074return result;1075}10761077getRawResults(): ISearchResult[] {1078return this.rawSearchResults ?? [];1079}10801081private getUniqueSearchResultSettings(): ISetting[] {1082return this.getUniqueSearchResults()?.filterMatches.map(m => m.setting) ?? [];1083}10841085updateChildren(): void {1086this.update({1087id: 'searchResultModel',1088label: 'searchResultModel',1089settings: this.getUniqueSearchResultSettings()1090});10911092// Save time by filtering children in the search model instead of relying on the tree filter, which still requires heights to be calculated.1093const isRemote = !!this.environmentService.remoteAuthority;10941095const newChildren = [];1096for (const child of this.root.children) {1097if (child instanceof SettingsTreeSettingElement1098&& child.matchesAllTags(this._viewState.tagFilters)1099&& child.matchesScope(this._viewState.settingsTarget, isRemote)1100&& child.matchesAnyExtension(this._viewState.extensionFilters)1101&& child.matchesAnyId(this._viewState.idFilters)1102&& child.matchesAnyFeature(this._viewState.featureFilters)1103&& child.matchesAllLanguages(this._viewState.languageFilter)) {1104newChildren.push(child);1105} else {1106child.dispose();1107}1108}1109this.root.children = newChildren;1110this.searchResultCount = this.root.children.length;11111112if (this.newExtensionSearchResults?.filterMatches.length) {1113let resultExtensionIds = this.newExtensionSearchResults.filterMatches1114.map(result => (<IExtensionSetting>result.setting))1115.filter(setting => setting.extensionName && setting.extensionPublisher)1116.map(setting => `${setting.extensionPublisher}.${setting.extensionName}`);1117resultExtensionIds = arrays.distinct(resultExtensionIds);11181119if (resultExtensionIds.length) {1120const newExtElement = new SettingsTreeNewExtensionsElement('newExtensions', resultExtensionIds);1121newExtElement.parent = this._root;1122this._root.children.push(newExtElement);1123}1124}1125}11261127setResult(order: SearchResultIdx, result: ISearchResult | null): void {1128this.cachedUniqueSearchResults.clear();1129this.newExtensionSearchResults = null;11301131if (this.rawSearchResults && order === SearchResultIdx.Local) {1132// To prevent the Settings editor from showing1133// stale remote results mid-search.1134delete this.rawSearchResults[SearchResultIdx.Remote];1135}11361137this.rawSearchResults ??= [];1138if (!result) {1139delete this.rawSearchResults[order];1140return;1141}11421143this.rawSearchResults[order] = result;1144this.updateChildren();1145}11461147getUniqueResultsCount(): number {1148return this.searchResultCount ?? 0;1149}1150}11511152export interface IParsedQuery {1153tags: string[];1154query: string;1155extensionFilters: string[];1156idFilters: string[];1157featureFilters: string[];1158languageFilter: string | undefined;1159}11601161const tagRegex = /(^|\s)@tag:("([^"]*)"|[^"]\S*)/g;1162const extensionRegex = /(^|\s)@ext:("([^"]*)"|[^"]\S*)?/g;1163const featureRegex = /(^|\s)@feature:("([^"]*)"|[^"]\S*)?/g;1164const idRegex = /(^|\s)@id:("([^"]*)"|[^"]\S*)?/g;1165const languageRegex = /(^|\s)@lang:("([^"]*)"|[^"]\S*)?/g;11661167export function parseQuery(query: string): IParsedQuery {1168/**1169* A helper function to parse the query on one type of regex.1170*1171* @param query The search query1172* @param filterRegex The regex to use on the query1173* @param parsedParts The parts that the regex parses out will be appended to the array passed in here.1174* @returns The query with the parsed parts removed1175*/1176function getTagsForType(query: string, filterRegex: RegExp, parsedParts: string[]): string {1177return query.replace(filterRegex, (_, __, quotedParsedElement, unquotedParsedElement) => {1178const parsedElement: string = unquotedParsedElement || quotedParsedElement;1179if (parsedElement) {1180parsedParts.push(...parsedElement.split(',').map(s => s.trim()).filter(s => !isFalsyOrWhitespace(s)));1181}1182return '';1183});1184}11851186const tags: string[] = [];1187query = query.replace(tagRegex, (_, __, quotedTag, tag) => {1188tags.push(tag || quotedTag);1189return '';1190});11911192query = query.replace(`@${MODIFIED_SETTING_TAG}`, () => {1193tags.push(MODIFIED_SETTING_TAG);1194return '';1195});11961197query = query.replace(`@${POLICY_SETTING_TAG}`, () => {1198tags.push(POLICY_SETTING_TAG);1199return '';1200});12011202// Handle @stable by excluding preview and experimental tags1203query = query.replace(/@stable/g, () => {1204tags.push('stable');1205return '';1206});12071208const extensions: string[] = [];1209const features: string[] = [];1210const ids: string[] = [];1211const langs: string[] = [];1212query = getTagsForType(query, extensionRegex, extensions);1213query = getTagsForType(query, featureRegex, features);1214query = getTagsForType(query, idRegex, ids);12151216if (ENABLE_LANGUAGE_FILTER) {1217query = getTagsForType(query, languageRegex, langs);1218}12191220query = query.trim();12211222// For now, only return the first found language filter1223return {1224tags,1225extensionFilters: extensions,1226featureFilters: features,1227idFilters: ids,1228languageFilter: langs.length ? langs[0] : undefined,1229query,1230};1231}123212331234