Path: blob/main/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts
3296 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;36filterToCategory?: 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// Check that the filter tags are a subset of this setting's tags405return !!this.tags?.size &&406Array.from(tagFilters).every(tag => this.tags!.has(tag));407}408409matchesScope(scope: SettingsTarget, isRemote: boolean): boolean {410const configTarget = URI.isUri(scope) ? ConfigurationTarget.WORKSPACE_FOLDER : scope;411412if (!this.setting.scope) {413return true;414}415416if (configTarget === ConfigurationTarget.APPLICATION) {417return APPLICATION_SCOPES.includes(this.setting.scope);418}419420if (configTarget === ConfigurationTarget.WORKSPACE_FOLDER) {421return FOLDER_SCOPES.includes(this.setting.scope);422}423424if (configTarget === ConfigurationTarget.WORKSPACE) {425return WORKSPACE_SCOPES.includes(this.setting.scope);426}427428if (configTarget === ConfigurationTarget.USER_REMOTE) {429return REMOTE_MACHINE_SCOPES.includes(this.setting.scope) || USER_LOCAL_AND_REMOTE_SETTINGS.includes(this.setting.key);430}431432if (configTarget === ConfigurationTarget.USER_LOCAL) {433if (isRemote) {434return LOCAL_MACHINE_SCOPES.includes(this.setting.scope) || USER_LOCAL_AND_REMOTE_SETTINGS.includes(this.setting.key);435}436}437438return true;439}440441matchesAnyExtension(extensionFilters?: Set<string>): boolean {442if (!extensionFilters || !extensionFilters.size) {443return true;444}445446if (!this.setting.extensionInfo) {447return false;448}449450return Array.from(extensionFilters).some(extensionId => extensionId.toLowerCase() === this.setting.extensionInfo!.id.toLowerCase());451}452453matchesAnyFeature(featureFilters?: Set<string>): boolean {454if (!featureFilters || !featureFilters.size) {455return true;456}457458const features = tocData.children!.find(child => child.id === 'features');459460return Array.from(featureFilters).some(filter => {461if (features && features.children) {462const feature = features.children.find(feature => 'features/' + filter === feature.id);463if (feature) {464const patterns = feature.settings?.map(setting => createSettingMatchRegExp(setting));465return patterns && !this.setting.extensionInfo && patterns.some(pattern => pattern.test(this.setting.key.toLowerCase()));466} else {467return false;468}469} else {470return false;471}472});473}474475matchesAnyId(idFilters?: Set<string>): boolean {476if (!idFilters || !idFilters.size) {477return true;478}479return idFilters.has(this.setting.key);480}481482matchesAllLanguages(languageFilter?: string): boolean {483if (!languageFilter) {484// We're not filtering by language.485return true;486}487488if (!this.languageService.isRegisteredLanguageId(languageFilter)) {489// We're trying to filter by an invalid language.490return false;491}492493// We have a language filter in the search widget at this point.494// We decide to show all language overridable settings to make the495// lang filter act more like a scope filter,496// rather than adding on an implicit @modified as well.497if (this.setting.scope === ConfigurationScope.LANGUAGE_OVERRIDABLE) {498return true;499}500501return false;502}503}504505506function createSettingMatchRegExp(pattern: string): RegExp {507pattern = escapeRegExpCharacters(pattern)508.replace(/\\\*/g, '.*');509510return new RegExp(`^${pattern}$`, 'i');511}512513export class SettingsTreeModel implements IDisposable {514protected _root!: SettingsTreeGroupElement;515private _tocRoot!: ITOCEntry<ISetting>;516private readonly _treeElementsBySettingName = new Map<string, SettingsTreeSettingElement[]>();517518constructor(519protected readonly _viewState: ISettingsEditorViewState,520private _isWorkspaceTrusted: boolean,521@IWorkbenchConfigurationService private readonly _configurationService: IWorkbenchConfigurationService,522@ILanguageService private readonly _languageService: ILanguageService,523@IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService,524@IProductService private readonly _productService: IProductService525) {526}527528get root(): SettingsTreeGroupElement {529return this._root;530}531532update(newTocRoot = this._tocRoot): void {533this._treeElementsBySettingName.clear();534535const newRoot = this.createSettingsTreeGroupElement(newTocRoot);536if (newRoot.children[0] instanceof SettingsTreeGroupElement) {537(<SettingsTreeGroupElement>newRoot.children[0]).isFirstGroup = true;538}539540if (this._root) {541this.disposeChildren(this._root.children);542this._root.children = newRoot.children;543newRoot.dispose();544} else {545this._root = newRoot;546}547}548549updateWorkspaceTrust(workspaceTrusted: boolean): void {550this._isWorkspaceTrusted = workspaceTrusted;551this.updateRequireTrustedTargetElements();552}553554private disposeChildren(children: SettingsTreeGroupChild[]) {555for (const child of children) {556this.disposeChildAndRecurse(child);557}558}559560private disposeChildAndRecurse(element: SettingsTreeElement) {561if (element instanceof SettingsTreeGroupElement) {562this.disposeChildren(element.children);563}564565element.dispose();566}567568getElementsByName(name: string): SettingsTreeSettingElement[] | null {569return this._treeElementsBySettingName.get(name) ?? null;570}571572updateElementsByName(name: string): void {573if (!this._treeElementsBySettingName.has(name)) {574return;575}576577this.reinspectSettings(this._treeElementsBySettingName.get(name)!);578}579580private updateRequireTrustedTargetElements(): void {581this.reinspectSettings([...this._treeElementsBySettingName.values()].flat().filter(s => s.isUntrusted));582}583584private reinspectSettings(settings: SettingsTreeSettingElement[]): void {585for (const element of settings) {586element.inspectSelf();587}588}589590private createSettingsTreeGroupElement(tocEntry: ITOCEntry<ISetting>, parent?: SettingsTreeGroupElement): SettingsTreeGroupElement {591const depth = parent ? this.getDepth(parent) + 1 : 0;592const element = new SettingsTreeGroupElement(tocEntry.id, undefined, tocEntry.label, depth, false);593element.parent = parent;594595const children: SettingsTreeGroupChild[] = [];596if (tocEntry.settings) {597const settingChildren = tocEntry.settings.map(s => this.createSettingsTreeSettingElement(s, element));598for (const child of settingChildren) {599if (!child.setting.deprecationMessage) {600children.push(child);601} else {602child.inspectSelf();603if (child.isConfigured) {604children.push(child);605} else {606child.dispose();607}608}609}610}611612if (tocEntry.children) {613const groupChildren = tocEntry.children.map(child => this.createSettingsTreeGroupElement(child, element));614children.push(...groupChildren);615}616617element.children = children;618619return element;620}621622private getDepth(element: SettingsTreeElement): number {623if (element.parent) {624return 1 + this.getDepth(element.parent);625} else {626return 0;627}628}629630private createSettingsTreeSettingElement(setting: ISetting, parent: SettingsTreeGroupElement): SettingsTreeSettingElement {631const element = new SettingsTreeSettingElement(632setting,633parent,634this._viewState.settingsTarget,635this._isWorkspaceTrusted,636this._viewState.languageFilter,637this._languageService,638this._productService,639this._userDataProfileService,640this._configurationService);641642const nameElements = this._treeElementsBySettingName.get(setting.key) ?? [];643nameElements.push(element);644this._treeElementsBySettingName.set(setting.key, nameElements);645return element;646}647648dispose() {649this._treeElementsBySettingName.clear();650this.disposeChildAndRecurse(this._root);651}652}653654interface IInspectResult {655isConfigured: boolean;656inspected: IConfigurationValue<unknown>;657targetSelector: 'applicationValue' | 'userLocalValue' | 'userRemoteValue' | 'workspaceValue' | 'workspaceFolderValue';658inspectedLanguageOverrides: Map<string, IConfigurationValue<unknown>>;659languageSelector: string | undefined;660}661662export function inspectSetting(key: string, target: SettingsTarget, languageFilter: string | undefined, configurationService: IWorkbenchConfigurationService): IInspectResult {663const inspectOverrides = URI.isUri(target) ? { resource: target } : undefined;664const inspected = configurationService.inspect(key, inspectOverrides);665const targetSelector = target === ConfigurationTarget.APPLICATION ? 'applicationValue' :666target === ConfigurationTarget.USER_LOCAL ? 'userLocalValue' :667target === ConfigurationTarget.USER_REMOTE ? 'userRemoteValue' :668target === ConfigurationTarget.WORKSPACE ? 'workspaceValue' :669'workspaceFolderValue';670const targetOverrideSelector = target === ConfigurationTarget.APPLICATION ? 'application' :671target === ConfigurationTarget.USER_LOCAL ? 'userLocal' :672target === ConfigurationTarget.USER_REMOTE ? 'userRemote' :673target === ConfigurationTarget.WORKSPACE ? 'workspace' :674'workspaceFolder';675let isConfigured = typeof inspected[targetSelector] !== 'undefined';676677const overrideIdentifiers = inspected.overrideIdentifiers;678const inspectedLanguageOverrides = new Map<string, IConfigurationValue<unknown>>();679680// We must reset isConfigured to be false if languageFilter is set, and manually681// determine whether it can be set to true later.682if (languageFilter) {683isConfigured = false;684}685if (overrideIdentifiers) {686// The setting we're looking at has language overrides.687for (const overrideIdentifier of overrideIdentifiers) {688inspectedLanguageOverrides.set(overrideIdentifier, configurationService.inspect(key, { overrideIdentifier }));689}690691// For all language filters, see if there's an override for that filter.692if (languageFilter) {693if (inspectedLanguageOverrides.has(languageFilter)) {694const overrideValue = inspectedLanguageOverrides.get(languageFilter)![targetOverrideSelector]?.override;695if (typeof overrideValue !== 'undefined') {696isConfigured = true;697}698}699}700}701702return { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector: languageFilter };703}704705function sanitizeId(id: string): string {706return id.replace(/[\.\/]/, '_');707}708709export function settingKeyToDisplayFormat(key: string, groupId: string = '', isLanguageTagSetting: boolean = false): { category: string; label: string } {710const lastDotIdx = key.lastIndexOf('.');711let category = '';712if (lastDotIdx >= 0) {713category = key.substring(0, lastDotIdx);714key = key.substring(lastDotIdx + 1);715}716717groupId = groupId.replace(/\//g, '.');718category = trimCategoryForGroup(category, groupId);719category = wordifyKey(category);720721if (isLanguageTagSetting) {722key = getLanguageTagSettingPlainKey(key);723key = '$(bracket) ' + key;724}725726const label = wordifyKey(key);727return { category, label };728}729730/**731* Removes redundant sections of the category label.732* A redundant section is a section already reflected in the groupId.733*734* @param category The category of the specific setting.735* @param groupId The author + extension ID.736* @returns The new category label to use.737*/738function trimCategoryForGroup(category: string, groupId: string): string {739const doTrim = (forward: boolean) => {740// Remove the Insiders portion if the category doesn't use it.741if (!/insiders$/i.test(category)) {742groupId = groupId.replace(/-?insiders$/i, '');743}744const parts = groupId.split('.')745.map(part => {746// Remove hyphens, but only if that results in a match with the category.747if (part.replace(/-/g, '').toLowerCase() === category.toLowerCase()) {748return part.replace(/-/g, '');749} else {750return part;751}752});753while (parts.length) {754const reg = new RegExp(`^${parts.join('\\.')}(\\.|$)`, 'i');755if (reg.test(category)) {756return category.replace(reg, '');757}758759if (forward) {760parts.pop();761} else {762parts.shift();763}764}765766return null;767};768769let trimmed = doTrim(true);770if (trimmed === null) {771trimmed = doTrim(false);772}773774if (trimmed === null) {775trimmed = category;776}777778return trimmed;779}780781function isExtensionToggleSetting(setting: ISetting, productService: IProductService): boolean {782return ENABLE_EXTENSION_TOGGLE_SETTINGS &&783!!productService.extensionRecommendations &&784!!setting.displayExtensionId;785}786787function isExcludeSetting(setting: ISetting): boolean {788return setting.key === 'files.exclude' ||789setting.key === 'search.exclude' ||790setting.key === 'workbench.localHistory.exclude' ||791setting.key === 'explorer.autoRevealExclude' ||792setting.key === 'files.readonlyExclude' ||793setting.key === 'files.watcherExclude';794}795796function isIncludeSetting(setting: ISetting): boolean {797return setting.key === 'files.readonlyInclude';798}799800// The values of the following settings when a default values has been removed801export function objectSettingSupportsRemoveDefaultValue(key: string): boolean {802return key === 'workbench.editor.customLabels.patterns';803}804805function isSimpleType(type: string | undefined): boolean {806return type === 'string' || type === 'boolean' || type === 'integer' || type === 'number';807}808809function getObjectRenderableSchemaType(schema: IJSONSchema, key: string): 'simple' | 'complex' | false {810const { type } = schema;811812if (Array.isArray(type)) {813if (objectSettingSupportsRemoveDefaultValue(key) && type.length === 2) {814if (type.includes('null') && (type.includes('string') || type.includes('boolean') || type.includes('integer') || type.includes('number'))) {815return 'simple';816}817}818819for (const t of type) {820if (!isSimpleType(t)) {821return false;822}823}824return 'complex';825}826827if (isSimpleType(type)) {828return 'simple';829}830831if (type === 'array') {832if (schema.items) {833const itemSchemas = Array.isArray(schema.items) ? schema.items : [schema.items];834for (const { type } of itemSchemas) {835if (Array.isArray(type)) {836for (const t of type) {837if (!isSimpleType(t)) {838return false;839}840}841return 'complex';842}843if (!isSimpleType(type)) {844return false;845}846return 'complex';847}848}849return false;850}851852return false;853}854855function getObjectSettingSchemaType({856key,857type,858objectProperties,859objectPatternProperties,860objectAdditionalProperties861}: ISetting): 'simple' | 'complex' | false {862if (type !== 'object') {863return false;864}865866// object can have any shape867if (868isUndefinedOrNull(objectProperties) &&869isUndefinedOrNull(objectPatternProperties) &&870isUndefinedOrNull(objectAdditionalProperties)871) {872return false;873}874875// objectAdditionalProperties allow the setting to have any shape,876// but if there's a pattern property that handles everything, then every877// property will match that patternProperty, so we don't need to look at878// the value of objectAdditionalProperties in that case.879if ((objectAdditionalProperties === true || objectAdditionalProperties === undefined)880&& !Object.keys(objectPatternProperties ?? {}).includes('.*')) {881return false;882}883884const schemas = [...Object.values(objectProperties ?? {}), ...Object.values(objectPatternProperties ?? {})];885886if (objectAdditionalProperties && typeof objectAdditionalProperties === 'object') {887schemas.push(objectAdditionalProperties);888}889890let schemaType: 'simple' | 'complex' | false = 'simple';891for (const schema of schemas) {892for (const subSchema of Array.isArray(schema.anyOf) ? schema.anyOf : [schema]) {893const subSchemaType = getObjectRenderableSchemaType(subSchema, key);894if (subSchemaType === false) {895return false;896}897if (subSchemaType === 'complex') {898schemaType = 'complex';899}900}901}902903return schemaType;904}905906function settingTypeEnumRenderable(_type: string | string[]) {907const enumRenderableSettingTypes = ['string', 'boolean', 'null', 'integer', 'number'];908const type = Array.isArray(_type) ? _type : [_type];909return type.every(type => enumRenderableSettingTypes.includes(type));910}911912export const enum SearchResultIdx {913Local = 0,914Remote = 1,915NewExtensions = 2,916Embeddings = 3,917AiSelected = 4918}919920export class SearchResultModel extends SettingsTreeModel {921private rawSearchResults: ISearchResult[] | null = null;922private cachedUniqueSearchResults: Map<boolean, ISearchResult | null>;923private newExtensionSearchResults: ISearchResult | null = null;924private searchResultCount: number | null = null;925private settingsOrderByTocIndex: Map<string, number> | null;926private aiFilterEnabled: boolean = false;927928readonly id = 'searchResultModel';929930constructor(931viewState: ISettingsEditorViewState,932settingsOrderByTocIndex: Map<string, number> | null,933isWorkspaceTrusted: boolean,934@IWorkbenchConfigurationService configurationService: IWorkbenchConfigurationService,935@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,936@ILanguageService languageService: ILanguageService,937@IUserDataProfileService userDataProfileService: IUserDataProfileService,938@IProductService productService: IProductService939) {940super(viewState, isWorkspaceTrusted, configurationService, languageService, userDataProfileService, productService);941this.settingsOrderByTocIndex = settingsOrderByTocIndex;942this.cachedUniqueSearchResults = new Map();943this.update({ id: 'searchResultModel', label: '' });944}945946set showAiResults(show: boolean) {947this.aiFilterEnabled = show;948this.updateChildren();949}950951private sortResults(filterMatches: ISettingMatch[]): ISettingMatch[] {952if (this.settingsOrderByTocIndex) {953for (const match of filterMatches) {954match.setting.internalOrder = this.settingsOrderByTocIndex.get(match.setting.key);955}956}957958// The search only has filters, so we can sort by the order in the TOC.959if (!this._viewState.query) {960return filterMatches.sort((a, b) => compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder));961}962963// Sort the settings according to their relevancy.964// https://github.com/microsoft/vscode/issues/197773965filterMatches.sort((a, b) => {966if (a.matchType !== b.matchType) {967// Sort by match type if the match types are not the same.968// The priority of the match type is given by the SettingMatchType enum.969return b.matchType - a.matchType;970} else if ((a.matchType & SettingMatchType.NonContiguousWordsInSettingsLabel) || (a.matchType & SettingMatchType.ContiguousWordsInSettingsLabel)) {971// The match types of a and b are the same and can be sorted by their number of matched words.972// If those numbers are the same, sort by the order in the table of contents.973return (b.keyMatchScore - a.keyMatchScore) || compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder);974} else if (a.matchType === SettingMatchType.RemoteMatch) {975// The match types are the same and are RemoteMatch.976// Sort by score.977return b.score - a.score;978} else {979// The match types are the same but are not RemoteMatch.980// Sort by their order in the table of contents.981return compareTwoNullableNumbers(a.setting.internalOrder, b.setting.internalOrder);982}983});984985// Remove duplicates, which sometimes occur with settings986// such as the experimental toggle setting.987return arrays.distinct(filterMatches, (match) => match.setting.key);988}989990getUniqueSearchResults(): ISearchResult | null {991const cachedResults = this.cachedUniqueSearchResults.get(this.aiFilterEnabled);992if (cachedResults) {993return cachedResults;994}995996if (!this.rawSearchResults) {997return null;998}9991000let combinedFilterMatches: ISettingMatch[] = [];10011002if (this.aiFilterEnabled) {1003const aiSelectedKeys = new Set<string>();1004const aiSelectedResult = this.rawSearchResults[SearchResultIdx.AiSelected];1005if (aiSelectedResult) {1006aiSelectedResult.filterMatches.forEach(m => aiSelectedKeys.add(m.setting.key));1007combinedFilterMatches = aiSelectedResult.filterMatches;1008}10091010const embeddingsResult = this.rawSearchResults[SearchResultIdx.Embeddings];1011if (embeddingsResult) {1012embeddingsResult.filterMatches = embeddingsResult.filterMatches.filter(m => !aiSelectedKeys.has(m.setting.key));1013combinedFilterMatches = combinedFilterMatches.concat(embeddingsResult.filterMatches);1014}1015const result = {1016filterMatches: combinedFilterMatches,1017exactMatch: false1018};1019this.cachedUniqueSearchResults.set(true, result);1020return result;1021}10221023const localMatchKeys = new Set<string>();1024const localResult = this.rawSearchResults[SearchResultIdx.Local];1025if (localResult) {1026localResult.filterMatches.forEach(m => localMatchKeys.add(m.setting.key));1027combinedFilterMatches = localResult.filterMatches;1028}10291030const remoteResult = this.rawSearchResults[SearchResultIdx.Remote];1031if (remoteResult) {1032remoteResult.filterMatches = remoteResult.filterMatches.filter(m => !localMatchKeys.has(m.setting.key));1033combinedFilterMatches = combinedFilterMatches.concat(remoteResult.filterMatches);10341035this.newExtensionSearchResults = this.rawSearchResults[SearchResultIdx.NewExtensions];1036}1037combinedFilterMatches = this.sortResults(combinedFilterMatches);1038const result = {1039filterMatches: combinedFilterMatches,1040exactMatch: localResult.exactMatch // remote results should never have an exact match1041};1042this.cachedUniqueSearchResults.set(false, result);1043return result;1044}10451046getRawResults(): ISearchResult[] {1047return this.rawSearchResults ?? [];1048}10491050private getUniqueSearchResultSettings(): ISetting[] {1051return this.getUniqueSearchResults()?.filterMatches.map(m => m.setting) ?? [];1052}10531054updateChildren(): void {1055this.update({1056id: 'searchResultModel',1057label: 'searchResultModel',1058settings: this.getUniqueSearchResultSettings()1059});10601061// Save time by filtering children in the search model instead of relying on the tree filter, which still requires heights to be calculated.1062const isRemote = !!this.environmentService.remoteAuthority;10631064const newChildren = [];1065for (const child of this.root.children) {1066if (child instanceof SettingsTreeSettingElement1067&& child.matchesAllTags(this._viewState.tagFilters)1068&& child.matchesScope(this._viewState.settingsTarget, isRemote)1069&& child.matchesAnyExtension(this._viewState.extensionFilters)1070&& child.matchesAnyId(this._viewState.idFilters)1071&& child.matchesAnyFeature(this._viewState.featureFilters)1072&& child.matchesAllLanguages(this._viewState.languageFilter)) {1073newChildren.push(child);1074} else {1075child.dispose();1076}1077}1078this.root.children = newChildren;1079this.searchResultCount = this.root.children.length;10801081if (this.newExtensionSearchResults?.filterMatches.length) {1082let resultExtensionIds = this.newExtensionSearchResults.filterMatches1083.map(result => (<IExtensionSetting>result.setting))1084.filter(setting => setting.extensionName && setting.extensionPublisher)1085.map(setting => `${setting.extensionPublisher}.${setting.extensionName}`);1086resultExtensionIds = arrays.distinct(resultExtensionIds);10871088if (resultExtensionIds.length) {1089const newExtElement = new SettingsTreeNewExtensionsElement('newExtensions', resultExtensionIds);1090newExtElement.parent = this._root;1091this._root.children.push(newExtElement);1092}1093}1094}10951096setResult(order: SearchResultIdx, result: ISearchResult | null): void {1097this.cachedUniqueSearchResults.clear();1098this.newExtensionSearchResults = null;10991100if (this.rawSearchResults && order === SearchResultIdx.Local) {1101// To prevent the Settings editor from showing1102// stale remote results mid-search.1103delete this.rawSearchResults[SearchResultIdx.Remote];1104}11051106this.rawSearchResults ??= [];1107if (!result) {1108delete this.rawSearchResults[order];1109return;1110}11111112this.rawSearchResults[order] = result;1113this.updateChildren();1114}11151116getUniqueResultsCount(): number {1117return this.searchResultCount ?? 0;1118}1119}11201121export interface IParsedQuery {1122tags: string[];1123query: string;1124extensionFilters: string[];1125idFilters: string[];1126featureFilters: string[];1127languageFilter: string | undefined;1128}11291130const tagRegex = /(^|\s)@tag:("([^"]*)"|[^"]\S*)/g;1131const extensionRegex = /(^|\s)@ext:("([^"]*)"|[^"]\S*)?/g;1132const featureRegex = /(^|\s)@feature:("([^"]*)"|[^"]\S*)?/g;1133const idRegex = /(^|\s)@id:("([^"]*)"|[^"]\S*)?/g;1134const languageRegex = /(^|\s)@lang:("([^"]*)"|[^"]\S*)?/g;11351136export function parseQuery(query: string): IParsedQuery {1137/**1138* A helper function to parse the query on one type of regex.1139*1140* @param query The search query1141* @param filterRegex The regex to use on the query1142* @param parsedParts The parts that the regex parses out will be appended to the array passed in here.1143* @returns The query with the parsed parts removed1144*/1145function getTagsForType(query: string, filterRegex: RegExp, parsedParts: string[]): string {1146return query.replace(filterRegex, (_, __, quotedParsedElement, unquotedParsedElement) => {1147const parsedElement: string = unquotedParsedElement || quotedParsedElement;1148if (parsedElement) {1149parsedParts.push(...parsedElement.split(',').map(s => s.trim()).filter(s => !isFalsyOrWhitespace(s)));1150}1151return '';1152});1153}11541155const tags: string[] = [];1156query = query.replace(tagRegex, (_, __, quotedTag, tag) => {1157tags.push(tag || quotedTag);1158return '';1159});11601161query = query.replace(`@${MODIFIED_SETTING_TAG}`, () => {1162tags.push(MODIFIED_SETTING_TAG);1163return '';1164});11651166query = query.replace(`@${POLICY_SETTING_TAG}`, () => {1167tags.push(POLICY_SETTING_TAG);1168return '';1169});11701171const extensions: string[] = [];1172const features: string[] = [];1173const ids: string[] = [];1174const langs: string[] = [];1175query = getTagsForType(query, extensionRegex, extensions);1176query = getTagsForType(query, featureRegex, features);1177query = getTagsForType(query, idRegex, ids);11781179if (ENABLE_LANGUAGE_FILTER) {1180query = getTagsForType(query, languageRegex, langs);1181}11821183query = query.trim();11841185// For now, only return the first found language filter1186return {1187tags,1188extensionFilters: extensions,1189featureFilters: features,1190idFilters: ids,1191languageFilter: langs.length ? langs[0] : undefined,1192query,1193};1194}119511961197