Path: blob/main/src/vs/workbench/contrib/preferences/browser/settingsTree.ts
5250 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 { BrowserFeatures } from '../../../../base/browser/canIUse.js';6import * as DOM from '../../../../base/browser/dom.js';7import * as domStylesheetsJs from '../../../../base/browser/domStylesheets.js';8import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';9import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js';10import { IMouseEvent } from '../../../../base/browser/mouseEvent.js';11import * as aria from '../../../../base/browser/ui/aria/aria.js';12import { Button } from '../../../../base/browser/ui/button/button.js';13import { SimpleIconLabel } from '../../../../base/browser/ui/iconLabel/simpleIconLabel.js';14import { IInputOptions, InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';15import { CachedListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';16import { DefaultStyleController, IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';17import { ISelectOptionItem, SelectBox } from '../../../../base/browser/ui/selectBox/selectBox.js';18import { Toggle, unthemedToggleStyles } from '../../../../base/browser/ui/toggle/toggle.js';19import { ToolBar } from '../../../../base/browser/ui/toolbar/toolbar.js';20import { RenderIndentGuides } from '../../../../base/browser/ui/tree/abstractTree.js';21import { IObjectTreeOptions } from '../../../../base/browser/ui/tree/objectTree.js';22import { ObjectTreeModel } from '../../../../base/browser/ui/tree/objectTreeModel.js';23import { ITreeFilter, ITreeModel, ITreeNode, ITreeRenderer, TreeFilterResult, TreeVisibility } from '../../../../base/browser/ui/tree/tree.js';24import { Action, IAction, Separator } from '../../../../base/common/actions.js';25import { distinct } from '../../../../base/common/arrays.js';26import { Codicon } from '../../../../base/common/codicons.js';27import { onUnexpectedError } from '../../../../base/common/errors.js';28import { Emitter, Event } from '../../../../base/common/event.js';29import { IJSONSchema } from '../../../../base/common/jsonSchema.js';30import { KeyCode } from '../../../../base/common/keyCodes.js';31import { Disposable, DisposableStore, isDisposable, toDisposable } from '../../../../base/common/lifecycle.js';32import { isIOS } from '../../../../base/common/platform.js';33import { escapeRegExpCharacters } from '../../../../base/common/strings.js';34import { isDefined, isUndefinedOrNull } from '../../../../base/common/types.js';35import { URI } from '../../../../base/common/uri.js';36import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';37import { ILanguageService } from '../../../../editor/common/languages/language.js';38import { localize } from '../../../../nls.js';39import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';40import { ICommandService } from '../../../../platform/commands/common/commands.js';41import { ConfigurationTarget, IConfigurationService, getLanguageTagSettingPlainKey } from '../../../../platform/configuration/common/configuration.js';42import { ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js';43import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';44import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';45import { IHoverService } from '../../../../platform/hover/browser/hover.js';46import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';47import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';48import { IListService, WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js';49import { ILogService } from '../../../../platform/log/common/log.js';50import { IOpenerService } from '../../../../platform/opener/common/opener.js';51import { IProductService } from '../../../../platform/product/common/productService.js';52import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';53import { defaultButtonStyles, getInputBoxStyle, getListStyles, getSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';54import { editorBackground, foreground } from '../../../../platform/theme/common/colorRegistry.js';55import { IThemeService } from '../../../../platform/theme/common/themeService.js';56import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';57import { getIgnoredSettings } from '../../../../platform/userDataSync/common/settingsMerge.js';58import { IUserDataSyncEnablementService, getDefaultIgnoredSettings } from '../../../../platform/userDataSync/common/userDataSync.js';59import { hasNativeContextMenu } from '../../../../platform/window/common/window.js';60import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js';61import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';62import { IExtensionService } from '../../../services/extensions/common/extensions.js';63import { ISetting, ISettingsGroup, SETTINGS_AUTHORITY, SettingValueType } from '../../../services/preferences/common/preferences.js';64import { getInvalidTypeError } from '../../../services/preferences/common/preferencesValidation.js';65import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';66import { LANGUAGE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, compareTwoNullableNumbers } from '../common/preferences.js';67import { settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js';68import { settingsMoreActionIcon } from './preferencesIcons.js';69import { SettingsTarget } from './preferencesWidgets.js';70import { ISettingOverrideClickEvent, SettingsTreeIndicatorsLabel, getIndicatorsLabelAriaLabel } from './settingsEditorSettingIndicators.js';71import { ITOCEntry, ITOCFilter } from './settingsLayout.js';72import { ISettingsEditorViewState, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement, inspectSetting, objectSettingSupportsRemoveDefaultValue, settingKeyToDisplayFormat } from './settingsTreeModels.js';73import { ExcludeSettingWidget, IBoolObjectDataItem, IIncludeExcludeDataItem, IListDataItem, IObjectDataItem, IObjectEnumOption, IObjectKeySuggester, IObjectValueSuggester, IncludeSettingWidget, ListSettingWidget, ObjectSettingCheckboxWidget, ObjectSettingDropdownWidget, ObjectValue, SettingListEvent } from './settingsWidgets.js';7475const $ = DOM.$;7677function getIncludeExcludeDisplayValue(element: SettingsTreeSettingElement): IIncludeExcludeDataItem[] {78const elementDefaultValue: Record<string, unknown> = typeof element.defaultValue === 'object'79? element.defaultValue ?? {}80: {};8182const data = element.isConfigured ?83{ ...elementDefaultValue, ...element.scopeValue } :84elementDefaultValue;8586return Object.keys(data)87.filter(key => !!data[key])88.map(key => {89const defaultValue = elementDefaultValue[key];9091// Get source if it's a default value92let source: string | undefined;93if (defaultValue === data[key] && element.setting.type === 'object' && element.defaultValueSource instanceof Map) {94const defaultSource = element.defaultValueSource.get(`${element.setting.key}.${key}`);95source = typeof defaultSource === 'string' ? defaultSource : defaultSource?.displayName;96}9798const value = data[key];99const sibling = typeof value === 'boolean' ? undefined : value.when;100return {101value: {102type: 'string',103data: key104},105sibling,106elementType: element.valueType,107source108};109});110}111112function areAllPropertiesDefined(properties: string[], itemsToDisplay: IObjectDataItem[]): boolean {113const staticProperties = new Set(properties);114itemsToDisplay.forEach(({ key }) => staticProperties.delete(key.data));115return staticProperties.size === 0;116}117118function getEnumOptionsFromSchema(schema: IJSONSchema): IObjectEnumOption[] {119if (schema.anyOf) {120return schema.anyOf.map(getEnumOptionsFromSchema).flat();121}122123const enumDescriptions = schema.enumDescriptions ?? [];124125return (schema.enum ?? []).map((value, idx) => {126const description = idx < enumDescriptions.length127? enumDescriptions[idx]128: undefined;129130return { value, description };131});132}133134function getObjectValueType(schema: IJSONSchema): ObjectValue['type'] {135if (schema.anyOf) {136const subTypes = schema.anyOf.map(getObjectValueType);137if (subTypes.some(type => type === 'enum')) {138return 'enum';139}140return 'string';141}142143if (schema.type === 'boolean') {144return 'boolean';145} else if (schema.type === 'string' && isDefined(schema.enum) && schema.enum.length > 0) {146return 'enum';147} else {148return 'string';149}150}151152function getObjectEntryValueDisplayValue(type: ObjectValue['type'], data: unknown, options: IObjectEnumOption[]): ObjectValue {153if (type === 'boolean') {154return { type, data: !!data };155} else if (type === 'enum') {156return { type, data: '' + data, options };157} else {158return { type, data: '' + data };159}160}161162function getObjectDisplayValue(element: SettingsTreeSettingElement): IObjectDataItem[] {163const elementDefaultValue: Record<string, unknown> = typeof element.defaultValue === 'object'164? element.defaultValue ?? {}165: {};166167const elementScopeValue: Record<string, unknown> = typeof element.scopeValue === 'object'168? element.scopeValue ?? {}169: {};170171const data = element.isConfigured ?172{ ...elementDefaultValue, ...elementScopeValue } :173element.hasPolicyValue ? element.scopeValue :174elementDefaultValue;175176const { objectProperties, objectPatternProperties, objectAdditionalProperties } = element.setting;177const patternsAndSchemas = Object178.entries(objectPatternProperties ?? {})179.map(([pattern, schema]) => ({180pattern: new RegExp(pattern),181schema182}));183184const wellDefinedKeyEnumOptions = Object.entries(objectProperties ?? {}).map(185([key, schema]) => ({ value: key, description: schema.description })186);187188return Object.keys(data).map(key => {189const defaultValue = elementDefaultValue[key];190191// Get source if it's a default value192let source: string | undefined;193if (defaultValue === data[key] && element.setting.type === 'object' && element.defaultValueSource instanceof Map) {194const defaultSource = element.defaultValueSource.get(`${element.setting.key}.${key}`);195source = typeof defaultSource === 'string' ? defaultSource : defaultSource?.displayName;196}197198if (isDefined(objectProperties) && key in objectProperties) {199const valueEnumOptions = getEnumOptionsFromSchema(objectProperties[key]);200return {201key: {202type: 'enum',203data: key,204options: wellDefinedKeyEnumOptions,205},206value: getObjectEntryValueDisplayValue(getObjectValueType(objectProperties[key]), data[key], valueEnumOptions),207keyDescription: objectProperties[key].description,208removable: isUndefinedOrNull(defaultValue),209resetable: !isUndefinedOrNull(defaultValue),210source211} satisfies IObjectDataItem;212}213214// The row is removable if it doesn't have a default value assigned or the setting supports removing the default value.215// If a default value is assigned and the user modified the default, it can be reset back to the default.216const removable = defaultValue === undefined || objectSettingSupportsRemoveDefaultValue(element.setting.key);217const resetable = !!defaultValue && defaultValue !== data[key];218const schema = patternsAndSchemas.find(({ pattern }) => pattern.test(key))?.schema;219if (schema) {220const valueEnumOptions = getEnumOptionsFromSchema(schema);221return {222key: { type: 'string', data: key },223value: getObjectEntryValueDisplayValue(getObjectValueType(schema), data[key], valueEnumOptions),224keyDescription: schema.description,225removable,226resetable,227source228} satisfies IObjectDataItem;229}230231const additionalValueEnums = getEnumOptionsFromSchema(232typeof objectAdditionalProperties === 'boolean'233? {}234: objectAdditionalProperties ?? {}235);236237return {238key: { type: 'string', data: key },239value: getObjectEntryValueDisplayValue(240typeof objectAdditionalProperties === 'object' ? getObjectValueType(objectAdditionalProperties) : 'string',241data[key],242additionalValueEnums,243),244keyDescription: typeof objectAdditionalProperties === 'object' ? objectAdditionalProperties.description : undefined,245removable,246resetable,247source248} satisfies IObjectDataItem;249}).filter(item => !isUndefinedOrNull(item.value.data));250}251252function getBoolObjectDisplayValue(element: SettingsTreeSettingElement): IBoolObjectDataItem[] {253const elementDefaultValue: Record<string, unknown> = typeof element.defaultValue === 'object'254? element.defaultValue ?? {}255: {};256257const elementScopeValue: Record<string, unknown> = typeof element.scopeValue === 'object'258? element.scopeValue ?? {}259: {};260261const data = element.isConfigured ?262{ ...elementDefaultValue, ...elementScopeValue } :263elementDefaultValue;264265const { objectProperties } = element.setting;266const displayValues: IBoolObjectDataItem[] = [];267for (const key in objectProperties) {268const defaultValue = elementDefaultValue[key];269270// Get source if it's a default value271let source: string | undefined;272if (defaultValue === data[key] && element.setting.type === 'object' && element.defaultValueSource instanceof Map) {273const defaultSource = element.defaultValueSource.get(key);274source = typeof defaultSource === 'string' ? defaultSource : defaultSource?.displayName;275}276277displayValues.push({278key: {279type: 'string',280data: key281},282value: {283type: 'boolean',284data: !!data[key]285},286keyDescription: objectProperties[key].description,287removable: false,288resetable: true,289source290});291}292return displayValues;293}294295function createArraySuggester(element: SettingsTreeSettingElement): IObjectKeySuggester {296return (keys, idx) => {297const enumOptions: IObjectEnumOption[] = [];298299if (element.setting.enum) {300element.setting.enum.forEach((key, i) => {301// include the currently selected value, even if uniqueItems is true302if (!element.setting.uniqueItems || (idx !== undefined && key === keys[idx]) || !keys.includes(key)) {303const description = element.setting.enumDescriptions?.[i];304enumOptions.push({ value: key, description });305}306});307}308309return enumOptions.length > 0310? { type: 'enum', data: enumOptions[0].value, options: enumOptions }311: undefined;312};313}314315function createObjectKeySuggester(element: SettingsTreeSettingElement): IObjectKeySuggester {316const { objectProperties } = element.setting;317const allStaticKeys = Object.keys(objectProperties ?? {});318319return keys => {320const existingKeys = new Set(keys);321const enumOptions: IObjectEnumOption[] = [];322323allStaticKeys.forEach(staticKey => {324if (!existingKeys.has(staticKey)) {325enumOptions.push({ value: staticKey, description: objectProperties![staticKey].description });326}327});328329return enumOptions.length > 0330? { type: 'enum', data: enumOptions[0].value, options: enumOptions }331: undefined;332};333}334335function createObjectValueSuggester(element: SettingsTreeSettingElement): IObjectValueSuggester {336const { objectProperties, objectPatternProperties, objectAdditionalProperties } = element.setting;337338const patternsAndSchemas = Object339.entries(objectPatternProperties ?? {})340.map(([pattern, schema]) => ({341pattern: new RegExp(pattern),342schema343}));344345return (key: string) => {346let suggestedSchema: IJSONSchema | undefined;347348if (isDefined(objectProperties) && key in objectProperties) {349suggestedSchema = objectProperties[key];350}351352const patternSchema = suggestedSchema ?? patternsAndSchemas.find(({ pattern }) => pattern.test(key))?.schema;353354if (isDefined(patternSchema)) {355suggestedSchema = patternSchema;356} else if (isDefined(objectAdditionalProperties) && typeof objectAdditionalProperties === 'object') {357suggestedSchema = objectAdditionalProperties;358}359360if (isDefined(suggestedSchema)) {361const type = getObjectValueType(suggestedSchema);362363if (type === 'boolean') {364return { type, data: suggestedSchema.default ?? true };365} else if (type === 'enum') {366const options = getEnumOptionsFromSchema(suggestedSchema);367return { type, data: suggestedSchema.default ?? options[0].value, options };368} else {369return { type, data: suggestedSchema.default ?? '' };370}371}372373return;374};375}376377function isNonNullableNumericType(type: unknown): type is 'number' | 'integer' {378return type === 'number' || type === 'integer';379}380381function parseNumericObjectValues(dataElement: SettingsTreeSettingElement, v: Record<string, unknown>): Record<string, unknown> {382const newRecord: Record<string, unknown> = {};383for (const key in v) {384// Set to true/false once we're sure of the answer385let keyMatchesNumericProperty: boolean | undefined;386const patternProperties = dataElement.setting.objectPatternProperties;387const properties = dataElement.setting.objectProperties;388const additionalProperties = dataElement.setting.objectAdditionalProperties;389390// Match the current record key against the properties of the object391if (properties) {392for (const propKey in properties) {393if (propKey === key) {394keyMatchesNumericProperty = isNonNullableNumericType(properties[propKey].type);395break;396}397}398}399if (keyMatchesNumericProperty === undefined && patternProperties) {400for (const patternKey in patternProperties) {401if (key.match(patternKey)) {402keyMatchesNumericProperty = isNonNullableNumericType(patternProperties[patternKey].type);403break;404}405}406}407if (keyMatchesNumericProperty === undefined && additionalProperties && typeof additionalProperties !== 'boolean') {408if (isNonNullableNumericType(additionalProperties.type)) {409keyMatchesNumericProperty = true;410}411}412newRecord[key] = keyMatchesNumericProperty ? Number(v[key]) : v[key];413}414return newRecord;415}416417function getListDisplayValue(element: SettingsTreeSettingElement): IListDataItem[] {418if (!element.value || !Array.isArray(element.value)) {419return [];420}421422if (element.setting.arrayItemType === 'enum') {423let enumOptions: IObjectEnumOption[] = [];424if (element.setting.enum) {425enumOptions = element.setting.enum.map((setting, i) => {426return {427value: setting,428description: element.setting.enumDescriptions?.[i]429};430});431}432return element.value.map((key: string) => {433return {434value: {435type: 'enum',436data: key,437options: enumOptions438}439};440});441} else {442return element.value.map((key: string) => {443return {444value: {445type: 'string',446data: key447}448};449});450}451}452453function getShowAddButtonList(dataElement: SettingsTreeSettingElement, listDisplayValue: IListDataItem[]): boolean {454if (dataElement.setting.enum && dataElement.setting.uniqueItems) {455return dataElement.setting.enum.length - listDisplayValue.length > 0;456} else {457return true;458}459}460461export function resolveSettingsTree(tocData: ITOCEntry<string>, coreSettingsGroups: ISettingsGroup[], filter: ITOCFilter | undefined, logService: ILogService): { tree: ITOCEntry<ISetting>; leftoverSettings: Set<ISetting> } {462const allSettings = getFlatSettings(coreSettingsGroups);463return {464tree: _resolveSettingsTree(tocData, allSettings, filter, logService),465leftoverSettings: allSettings466};467}468469export function resolveConfiguredUntrustedSettings(groups: ISettingsGroup[], target: SettingsTarget, languageFilter: string | undefined, configurationService: IWorkbenchConfigurationService): ISetting[] {470const allSettings = getFlatSettings(groups);471return [...allSettings].filter(setting => setting.restricted && inspectSetting(setting.key, target, languageFilter, configurationService).isConfigured);472}473474export async function createTocTreeForExtensionSettings(extensionService: IExtensionService, groups: ISettingsGroup[], filter: ITOCFilter | undefined): Promise<ITOCEntry<ISetting>> {475const extGroupTree = new Map<string, ITOCEntry<ISetting>>();476const addEntryToTree = (extensionId: string, extensionName: string, childEntry: ITOCEntry<ISetting>) => {477if (!extGroupTree.has(extensionId)) {478const rootEntry = {479id: extensionId,480label: extensionName,481children: []482};483extGroupTree.set(extensionId, rootEntry);484}485extGroupTree.get(extensionId)!.children!.push(childEntry);486};487const processGroupEntry = async (group: ISettingsGroup) => {488const flatSettings = group.sections.map(section => section.settings).flat();489const settings = filter ? getMatchingSettings(new Set(flatSettings), filter) : flatSettings;490sortSettings(settings);491492const extensionId = group.extensionInfo!.id;493const extension = await extensionService.getExtension(extensionId);494const extensionName = extension?.displayName ?? extension?.name ?? extensionId;495496// There could be multiple groups with the same extension id that all belong to the same extension.497// To avoid highlighting all groups upon expanding the extension's ToC entry,498// use the group ID only if it is non-empty and isn't the extension ID.499// Ref https://github.com/microsoft/vscode/issues/241521.500const settingGroupId = (group.id && group.id !== extensionId) ? group.id : group.title;501502const childEntry: ITOCEntry<ISetting> = {503id: settingGroupId,504label: group.title,505order: group.order,506settings507};508addEntryToTree(extensionId, extensionName, childEntry);509};510511const processPromises = groups.map(g => processGroupEntry(g));512return Promise.all(processPromises).then(() => {513const extGroups: ITOCEntry<ISetting>[] = [];514for (const extensionRootEntry of extGroupTree.values()) {515if (extensionRootEntry.children!.length === 1) {516// There is a single category for this extension.517// Push a flattened setting.518extGroups.push({519id: extensionRootEntry.id,520label: extensionRootEntry.children![0].label,521settings: extensionRootEntry.children![0].settings522});523} else {524// Sort the categories.525// Leave the undefined order categories untouched.526extensionRootEntry.children!.sort((a, b) => {527return compareTwoNullableNumbers(a.order, b.order);528});529530// If there is a category that matches the setting name,531// add the settings in manually as "ungrouped" settings.532// https://github.com/microsoft/vscode/issues/137259533const ungroupedChild = extensionRootEntry.children!.find(child => child.label === extensionRootEntry.label);534if (ungroupedChild && !ungroupedChild.children) {535const groupedChildren = extensionRootEntry.children!.filter(child => child !== ungroupedChild);536extGroups.push({537id: extensionRootEntry.id,538label: extensionRootEntry.label,539settings: ungroupedChild.settings,540children: groupedChildren541});542} else {543// Push all the groups as-is.544extGroups.push(extensionRootEntry);545}546}547}548549// Sort the outermost settings.550extGroups.sort((a, b) => a.label.localeCompare(b.label));551552return {553id: 'extensions',554label: localize('extensions', "Extensions"),555children: extGroups556};557});558}559560function _resolveSettingsTree(tocData: ITOCEntry<string>, allSettings: Set<ISetting>, filter: ITOCFilter | undefined, logService: ILogService): ITOCEntry<ISetting> {561let children: ITOCEntry<ISetting>[] | undefined;562if (tocData.children) {563children = tocData.children564.filter(child => child.hide !== true)565.map(child => _resolveSettingsTree(child, allSettings, filter, logService))566.filter(child => child.children?.length || child.settings?.length);567}568569let settings: ISetting[] | undefined;570if (filter || tocData.settings) {571settings = getMatchingSettings(allSettings, {572include: {573keyPatterns: [...filter?.include?.keyPatterns ?? [], ...tocData.settings ?? []],574tags: filter?.include?.tags ? [...filter.include.tags] : []575},576exclude: filter?.exclude ?? {}577});578sortSettings(settings);579}580581if (!children && !settings) {582throw new Error(`TOC node has no child groups or settings: ${tocData.id}`);583}584585return {586id: tocData.id,587label: tocData.label,588children,589settings590};591}592593/**594* Sort settings so that preview and experimental settings are deprioritized.595* Within each tier, sort the settings by order, then alphabetically.596*/597function sortSettings(settings: ISetting[]): void {598const SETTING_STATUS_NORMAL = 0;599const SETTING_STATUS_PREVIEW = 1;600const SETTING_STATUS_EXPERIMENTAL = 2;601602const getExperimentalStatus = (setting: ISetting) => {603if (setting.tags?.includes('experimental')) {604return SETTING_STATUS_EXPERIMENTAL;605} else if (setting.tags?.includes('preview')) {606return SETTING_STATUS_PREVIEW;607}608return SETTING_STATUS_NORMAL;609};610611settings.sort((a, b) => {612const experimentalStatusA = getExperimentalStatus(a);613const experimentalStatusB = getExperimentalStatus(b);614if (experimentalStatusA !== experimentalStatusB) {615return experimentalStatusA - experimentalStatusB;616}617618const orderComparison = compareTwoNullableNumbers(a.order, b.order);619return orderComparison !== 0 ? orderComparison : a.key.localeCompare(b.key);620});621}622623function getMatchingSettings(allSettings: Set<ISetting>, filter: ITOCFilter): ISetting[] {624const result: ISetting[] = [];625626allSettings.forEach(setting => {627let shouldInclude = false;628let shouldExclude = false;629630// Check include filters631if (filter.include?.keyPatterns) {632shouldInclude = filter.include.keyPatterns.some(pattern => {633if (pattern.startsWith('@tag:')) {634const tagName = pattern.substring(5);635return setting.tags?.includes(tagName);636} else {637return settingMatches(setting, pattern);638}639});640} else {641shouldInclude = true;642}643644if (shouldInclude && filter.include?.tags?.length) {645shouldInclude = filter.include.tags.some(tag => setting.tags?.includes(tag));646}647648// Check exclude filters (takes precedence)649if (filter.exclude?.keyPatterns) {650shouldExclude = filter.exclude.keyPatterns.some(pattern => {651if (pattern.startsWith('@tag:')) {652const tagName = pattern.substring(5);653return setting.tags?.includes(tagName);654} else {655return settingMatches(setting, pattern);656}657});658}659660if (!shouldExclude && filter.exclude?.tags?.length) {661shouldExclude = filter.exclude.tags.some(tag => setting.tags?.includes(tag));662}663664// Include if matches include filter and doesn't match exclude filter665if (shouldInclude && !shouldExclude) {666result.push(setting);667allSettings.delete(setting);668}669});670671return result;672}673674const settingPatternCache = new Map<string, RegExp>();675676export function createSettingMatchRegExp(pattern: string): RegExp {677pattern = escapeRegExpCharacters(pattern)678.replace(/\\\*/g, '.*');679680return new RegExp(`^${pattern}$`, 'i');681}682683function settingMatches(s: ISetting, pattern: string): boolean {684let regExp = settingPatternCache.get(pattern);685if (!regExp) {686regExp = createSettingMatchRegExp(pattern);687settingPatternCache.set(pattern, regExp);688}689690return regExp.test(s.key);691}692693function getFlatSettings(settingsGroups: ISettingsGroup[]) {694const result: Set<ISetting> = new Set();695696for (const group of settingsGroups) {697for (const section of group.sections) {698for (const s of section.settings) {699if (!s.overrides || !s.overrides.length) {700result.add(s);701}702}703}704}705706return result;707}708709interface IDisposableTemplate {710readonly toDispose: DisposableStore;711}712713interface ISettingItemTemplate<T = any> extends IDisposableTemplate {714onChange?: (value: T) => void;715716context?: SettingsTreeSettingElement;717containerElement: HTMLElement;718categoryElement: HTMLElement;719labelElement: SimpleIconLabel;720descriptionElement: HTMLElement;721controlElement: HTMLElement;722deprecationWarningElement: HTMLElement;723indicatorsLabel: SettingsTreeIndicatorsLabel;724toolbar: ToolBar;725readonly elementDisposables: DisposableStore;726}727728interface ISettingBoolItemTemplate extends ISettingItemTemplate<boolean> {729checkbox: Toggle;730}731732interface ISettingExtensionToggleItemTemplate extends ISettingItemTemplate<undefined> {733actionButton: Button;734dismissButton: Button;735}736737interface ISettingTextItemTemplate extends ISettingItemTemplate<string> {738inputBox: InputBox;739validationErrorMessageElement: HTMLElement;740}741742type ISettingNumberItemTemplate = ISettingTextItemTemplate;743744interface ISettingEnumItemTemplate extends ISettingItemTemplate<number> {745selectBox: SelectBox;746selectElement: HTMLSelectElement | null;747enumDescriptionElement: HTMLElement;748}749750interface ISettingComplexItemTemplate extends ISettingItemTemplate<void> {751button: HTMLElement;752validationErrorMessageElement: HTMLElement;753}754755interface ISettingComplexObjectItemTemplate extends ISettingComplexItemTemplate {756objectSettingWidget: ObjectSettingDropdownWidget;757}758759interface ISettingListItemTemplate extends ISettingItemTemplate<string[] | undefined> {760listWidget: ListSettingWidget<IListDataItem>;761validationErrorMessageElement: HTMLElement;762}763764interface ISettingIncludeExcludeItemTemplate extends ISettingItemTemplate<void> {765includeExcludeWidget: ListSettingWidget<IIncludeExcludeDataItem>;766}767768interface ISettingObjectItemTemplate extends ISettingItemTemplate<Record<string, unknown> | undefined> {769objectDropdownWidget?: ObjectSettingDropdownWidget;770objectCheckboxWidget?: ObjectSettingCheckboxWidget;771validationErrorMessageElement: HTMLElement;772}773774interface ISettingNewExtensionsTemplate extends IDisposableTemplate {775button: Button;776context?: SettingsTreeNewExtensionsElement;777}778779interface IGroupTitleTemplate extends IDisposableTemplate {780context?: SettingsTreeGroupElement;781parent: HTMLElement;782}783784const SETTINGS_TEXT_TEMPLATE_ID = 'settings.text.template';785const SETTINGS_MULTILINE_TEXT_TEMPLATE_ID = 'settings.multilineText.template';786const SETTINGS_NUMBER_TEMPLATE_ID = 'settings.number.template';787const SETTINGS_ENUM_TEMPLATE_ID = 'settings.enum.template';788const SETTINGS_BOOL_TEMPLATE_ID = 'settings.bool.template';789const SETTINGS_ARRAY_TEMPLATE_ID = 'settings.array.template';790const SETTINGS_EXCLUDE_TEMPLATE_ID = 'settings.exclude.template';791const SETTINGS_INCLUDE_TEMPLATE_ID = 'settings.include.template';792const SETTINGS_OBJECT_TEMPLATE_ID = 'settings.object.template';793const SETTINGS_BOOL_OBJECT_TEMPLATE_ID = 'settings.boolObject.template';794const SETTINGS_COMPLEX_TEMPLATE_ID = 'settings.complex.template';795const SETTINGS_COMPLEX_OBJECT_TEMPLATE_ID = 'settings.complexObject.template';796const SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID = 'settings.newExtensions.template';797const SETTINGS_ELEMENT_TEMPLATE_ID = 'settings.group.template';798const SETTINGS_EXTENSION_TOGGLE_TEMPLATE_ID = 'settings.extensionToggle.template';799800export interface ISettingChangeEvent {801key: string;802value: unknown; // undefined => reset/unconfigure803type: SettingValueType | SettingValueType[];804manualReset: boolean;805scope: ConfigurationScope | undefined;806}807808export interface ISettingLinkClickEvent {809source: SettingsTreeSettingElement;810targetKey: string;811}812813function removeChildrenFromTabOrder(node: Element): void {814// eslint-disable-next-line no-restricted-syntax815const focusableElements = node.querySelectorAll(`816[tabindex="0"],817input:not([tabindex="-1"]),818select:not([tabindex="-1"]),819textarea:not([tabindex="-1"]),820a:not([tabindex="-1"]),821button:not([tabindex="-1"]),822area:not([tabindex="-1"])823`);824825focusableElements.forEach(element => {826element.setAttribute(AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR, 'true');827element.setAttribute('tabindex', '-1');828});829}830831function addChildrenToTabOrder(node: Element): void {832// eslint-disable-next-line no-restricted-syntax833const focusableElements = node.querySelectorAll(834`[${AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR}="true"]`835);836837focusableElements.forEach(element => {838element.removeAttribute(AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR);839element.setAttribute('tabindex', '0');840});841}842843export interface HeightChangeParams {844element: SettingsTreeElement;845height: number;846}847848export abstract class AbstractSettingRenderer extends Disposable implements ITreeRenderer<SettingsTreeElement, never, any> {849/** To override */850abstract get templateId(): string;851852static readonly CONTROL_CLASS = 'setting-control-focus-target';853static readonly CONTROL_SELECTOR = '.' + this.CONTROL_CLASS;854static readonly CONTENTS_CLASS = 'setting-item-contents';855static readonly CONTENTS_SELECTOR = '.' + this.CONTENTS_CLASS;856static readonly ALL_ROWS_SELECTOR = '.monaco-list-row';857858static readonly SETTING_KEY_ATTR = 'data-key';859static readonly SETTING_ID_ATTR = 'data-id';860static readonly ELEMENT_FOCUSABLE_ATTR = 'data-focusable';861862private readonly _onDidClickOverrideElement = this._register(new Emitter<ISettingOverrideClickEvent>());863readonly onDidClickOverrideElement: Event<ISettingOverrideClickEvent> = this._onDidClickOverrideElement.event;864865protected readonly _onDidChangeSetting = this._register(new Emitter<ISettingChangeEvent>());866readonly onDidChangeSetting: Event<ISettingChangeEvent> = this._onDidChangeSetting.event;867868protected readonly _onDidOpenSettings = this._register(new Emitter<string>());869readonly onDidOpenSettings: Event<string> = this._onDidOpenSettings.event;870871private readonly _onDidClickSettingLink = this._register(new Emitter<ISettingLinkClickEvent>());872readonly onDidClickSettingLink: Event<ISettingLinkClickEvent> = this._onDidClickSettingLink.event;873874protected readonly _onDidFocusSetting = this._register(new Emitter<SettingsTreeSettingElement>());875readonly onDidFocusSetting: Event<SettingsTreeSettingElement> = this._onDidFocusSetting.event;876877private ignoredSettings: string[];878private readonly _onDidChangeIgnoredSettings = this._register(new Emitter<void>());879readonly onDidChangeIgnoredSettings: Event<void> = this._onDidChangeIgnoredSettings.event;880881protected readonly _onDidChangeSettingHeight = this._register(new Emitter<HeightChangeParams>());882readonly onDidChangeSettingHeight: Event<HeightChangeParams> = this._onDidChangeSettingHeight.event;883884protected readonly _onApplyFilter = this._register(new Emitter<string>());885readonly onApplyFilter: Event<string> = this._onApplyFilter.event;886887constructor(888private readonly settingActions: IAction[],889private readonly disposableActionFactory: (setting: ISetting, settingTarget: SettingsTarget) => IAction[],890@IThemeService protected readonly _themeService: IThemeService,891@IContextViewService protected readonly _contextViewService: IContextViewService,892@IOpenerService protected readonly _openerService: IOpenerService,893@IInstantiationService protected readonly _instantiationService: IInstantiationService,894@ICommandService protected readonly _commandService: ICommandService,895@IContextMenuService protected readonly _contextMenuService: IContextMenuService,896@IKeybindingService protected readonly _keybindingService: IKeybindingService,897@IConfigurationService protected readonly _configService: IConfigurationService,898@IExtensionService protected readonly _extensionsService: IExtensionService,899@IExtensionsWorkbenchService protected readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,900@IProductService protected readonly _productService: IProductService,901@ITelemetryService protected readonly _telemetryService: ITelemetryService,902@IHoverService protected readonly _hoverService: IHoverService,903@IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService,904) {905super();906907this.ignoredSettings = getIgnoredSettings(getDefaultIgnoredSettings(), this._configService);908this._register(this._configService.onDidChangeConfiguration(e => {909this.ignoredSettings = getIgnoredSettings(getDefaultIgnoredSettings(), this._configService);910this._onDidChangeIgnoredSettings.fire();911}));912}913914abstract renderTemplate(container: HTMLElement): any;915916abstract renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: unknown): void;917918protected renderCommonTemplate(tree: unknown, _container: HTMLElement, typeClass: string): ISettingItemTemplate {919_container.classList.add('setting-item');920_container.classList.add('setting-item-' + typeClass);921922const toDispose = new DisposableStore();923924const container = DOM.append(_container, $(AbstractSettingRenderer.CONTENTS_SELECTOR));925container.classList.add('settings-row-inner-container');926const titleElement = DOM.append(container, $('.setting-item-title'));927const labelCategoryContainer = DOM.append(titleElement, $('.setting-item-cat-label-container'));928const categoryElement = DOM.append(labelCategoryContainer, $('span.setting-item-category'));929const labelElementContainer = DOM.append(labelCategoryContainer, $('span.setting-item-label'));930const labelElement = toDispose.add(new SimpleIconLabel(labelElementContainer));931const indicatorsLabel = toDispose.add(this._instantiationService.createInstance(SettingsTreeIndicatorsLabel, titleElement));932933const descriptionElement = DOM.append(container, $('.setting-item-description'));934const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator'));935toDispose.add(this._hoverService.setupDelayedHover(modifiedIndicatorElement, {936content: localize('modified', "The setting has been configured in the current scope.")937}));938939const valueElement = DOM.append(container, $('.setting-item-value'));940const controlElement = DOM.append(valueElement, $('div.setting-item-control'));941942const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message'));943944const toolbarContainer = DOM.append(container, $('.setting-toolbar-container'));945const toolbar = this.renderSettingToolbar(toolbarContainer);946947const template: ISettingItemTemplate = {948toDispose,949elementDisposables: toDispose.add(new DisposableStore()),950951containerElement: container,952categoryElement,953labelElement,954descriptionElement,955controlElement,956deprecationWarningElement,957indicatorsLabel,958toolbar959};960961// Prevent clicks from being handled by list962toDispose.add(DOM.addDisposableListener(controlElement, DOM.EventType.MOUSE_DOWN, e => e.stopPropagation()));963964toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_ENTER, e => container.classList.add('mouseover')));965toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover')));966967return template;968}969970protected addSettingElementFocusHandler(template: ISettingItemTemplate): void {971const focusTracker = DOM.trackFocus(template.containerElement);972template.toDispose.add(focusTracker);973template.toDispose.add(focusTracker.onDidBlur(() => {974if (template.containerElement.classList.contains('focused')) {975template.containerElement.classList.remove('focused');976}977}));978979template.toDispose.add(focusTracker.onDidFocus(() => {980template.containerElement.classList.add('focused');981982if (template.context) {983this._onDidFocusSetting.fire(template.context);984}985}));986}987988protected renderSettingToolbar(container: HTMLElement): ToolBar {989const toggleMenuTitle = this._keybindingService.appendKeybinding(990localize('settingsContextMenuTitle', "More Actions... "),991SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU);992993const toolbar = new ToolBar(container, this._contextMenuService, {994toggleMenuTitle,995renderDropdownAsChildElement: !isIOS,996moreIcon: settingsMoreActionIcon997});998return toolbar;999}10001001protected renderSettingElement(node: ITreeNode<SettingsTreeSettingElement, never>, index: number, template: ISettingItemTemplate | ISettingBoolItemTemplate): void {1002const element = node.element;10031004// The element must inspect itself to get information for1005// the modified indicator and the overridden Settings indicators.1006element.inspectSelf();10071008template.context = element;1009template.toolbar.context = element;1010const actions = this.disposableActionFactory(element.setting, element.settingsTarget);1011actions.forEach(a => isDisposable(a) && template.elementDisposables.add(a));1012template.toolbar.setActions([], [...this.settingActions, ...actions]);10131014const setting = element.setting;10151016template.containerElement.classList.toggle('is-configured', element.isConfigured);1017template.containerElement.setAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR, element.setting.key);1018template.containerElement.setAttribute(AbstractSettingRenderer.SETTING_ID_ATTR, element.id);10191020const titleTooltip = setting.key + (element.isConfigured ? ' - Modified' : '');1021template.categoryElement.textContent = element.displayCategory ? (element.displayCategory + ': ') : '';1022template.elementDisposables.add(this._hoverService.setupDelayedHover(template.categoryElement, { content: titleTooltip }));10231024template.labelElement.text = element.displayLabel;1025template.labelElement.title = titleTooltip;10261027template.descriptionElement.innerText = '';1028if (element.setting.descriptionIsMarkdown) {1029const renderedDescription = this.renderSettingMarkdown(element, template.containerElement, element.description, template.elementDisposables);1030template.descriptionElement.appendChild(renderedDescription);1031} else {1032template.descriptionElement.innerText = element.description;1033}10341035template.indicatorsLabel.updateScopeOverrides(element, this._onDidClickOverrideElement, this._onApplyFilter);1036template.elementDisposables.add(this._configService.onDidChangeConfiguration(e => {1037if (e.affectsConfiguration(APPLY_ALL_PROFILES_SETTING)) {1038template.indicatorsLabel.updateScopeOverrides(element, this._onDidClickOverrideElement, this._onApplyFilter);1039}1040}));10411042const onChange = (value: unknown) => this._onDidChangeSetting.fire({1043key: element.setting.key,1044value,1045type: template.context!.valueType,1046manualReset: false,1047scope: element.setting.scope1048});1049const deprecationText = element.setting.deprecationMessage || '';1050if (deprecationText && element.setting.deprecationMessageIsMarkdown) {1051template.deprecationWarningElement.innerText = '';1052template.deprecationWarningElement.appendChild(this.renderSettingMarkdown(element, template.containerElement, element.setting.deprecationMessage!, template.elementDisposables));1053} else {1054template.deprecationWarningElement.innerText = deprecationText;1055}1056template.deprecationWarningElement.prepend($('.codicon.codicon-error'));1057template.containerElement.classList.toggle('is-deprecated', !!deprecationText);10581059this.renderValue(element, <ISettingItemTemplate>template, onChange);10601061template.indicatorsLabel.updateWorkspaceTrust(element);1062template.indicatorsLabel.updateSyncIgnored(element, this.ignoredSettings);1063template.indicatorsLabel.updateDefaultOverrideIndicator(element);1064template.indicatorsLabel.updatePreviewIndicator(element);1065template.indicatorsLabel.updateAdvancedIndicator(element);1066template.elementDisposables.add(this.onDidChangeIgnoredSettings(() => {1067template.indicatorsLabel.updateSyncIgnored(element, this.ignoredSettings);1068}));10691070this.updateSettingTabbable(element, template);1071template.elementDisposables.add(element.onDidChangeTabbable(() => {1072this.updateSettingTabbable(element, template);1073}));1074}10751076private updateSettingTabbable(element: SettingsTreeSettingElement, template: ISettingItemTemplate | ISettingBoolItemTemplate): void {1077if (element.tabbable) {1078addChildrenToTabOrder(template.containerElement);1079} else {1080removeChildrenFromTabOrder(template.containerElement);1081}1082}10831084private renderSettingMarkdown(element: SettingsTreeSettingElement, container: HTMLElement, text: string, disposables: DisposableStore): HTMLElement {1085// Rewrite `#editor.fontSize#` to link format1086text = fixSettingLinks(text);10871088const renderedMarkdown = disposables.add(this._markdownRendererService.render({ value: text, isTrusted: true }, {1089actionHandler: (content: string) => {1090if (content.startsWith('#')) {1091const e: ISettingLinkClickEvent = {1092source: element,1093targetKey: content.substring(1)1094};1095this._onDidClickSettingLink.fire(e);1096} else {1097this._openerService.open(content, { allowCommands: true }).catch(onUnexpectedError);1098}1099},1100asyncRenderCallback: () => {1101const height = container.clientHeight;1102if (height) {1103this._onDidChangeSettingHeight.fire({ element, height });1104}1105},1106}));11071108renderedMarkdown.element.classList.add('setting-item-markdown');1109cleanRenderedMarkdown(renderedMarkdown.element);1110return renderedMarkdown.element;1111}11121113protected abstract renderValue(dataElement: SettingsTreeSettingElement, template: ISettingItemTemplate, onChange: (value: unknown) => void): void;11141115disposeTemplate(template: IDisposableTemplate): void {1116template.toDispose.dispose();1117}11181119disposeElement(_element: ITreeNode<SettingsTreeElement>, _index: number, template: IDisposableTemplate): void {1120(template as ISettingItemTemplate).elementDisposables?.clear();1121}1122}11231124class SettingGroupRenderer implements ITreeRenderer<SettingsTreeGroupElement, never, IGroupTitleTemplate> {1125templateId = SETTINGS_ELEMENT_TEMPLATE_ID;11261127renderTemplate(container: HTMLElement): IGroupTitleTemplate {1128container.classList.add('group-title');11291130const template: IGroupTitleTemplate = {1131parent: container,1132toDispose: new DisposableStore()1133};11341135return template;1136}11371138renderElement(element: ITreeNode<SettingsTreeGroupElement, never>, index: number, templateData: IGroupTitleTemplate): void {1139templateData.parent.innerText = '';1140const labelElement = DOM.append(templateData.parent, $('div.settings-group-title-label.settings-row-inner-container'));1141labelElement.classList.add(`settings-group-level-${element.element.level}`);1142labelElement.textContent = element.element.label;11431144if (element.element.isFirstGroup) {1145labelElement.classList.add('settings-group-first');1146}1147}11481149disposeTemplate(templateData: IGroupTitleTemplate): void {1150templateData.toDispose.dispose();1151}1152}11531154export class SettingNewExtensionsRenderer implements ITreeRenderer<SettingsTreeNewExtensionsElement, never, ISettingNewExtensionsTemplate> {1155templateId = SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID;11561157constructor(1158@ICommandService private readonly _commandService: ICommandService,1159) {1160}11611162renderTemplate(container: HTMLElement): ISettingNewExtensionsTemplate {1163const toDispose = new DisposableStore();11641165container.classList.add('setting-item-new-extensions');11661167const button = new Button(container, { title: true, ...defaultButtonStyles });1168toDispose.add(button);1169toDispose.add(button.onDidClick(() => {1170if (template.context) {1171this._commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', template.context.extensionIds);1172}1173}));1174button.label = localize('newExtensionsButtonLabel', "Show matching extensions");1175button.element.classList.add('settings-new-extensions-button');11761177const template: ISettingNewExtensionsTemplate = {1178button,1179toDispose1180};11811182return template;1183}11841185renderElement(element: ITreeNode<SettingsTreeNewExtensionsElement, never>, index: number, templateData: ISettingNewExtensionsTemplate): void {1186templateData.context = element.element;1187}11881189disposeTemplate(template: IDisposableTemplate): void {1190template.toDispose.dispose();1191}1192}11931194export class SettingComplexRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingComplexItemTemplate> {1195private static readonly EDIT_IN_JSON_LABEL = localize('editInSettingsJson', "Edit in settings.json");11961197templateId = SETTINGS_COMPLEX_TEMPLATE_ID;11981199renderTemplate(container: HTMLElement): ISettingComplexItemTemplate {1200const common = this.renderCommonTemplate(null, container, 'complex');12011202const openSettingsButton = DOM.append(common.controlElement, $('a.edit-in-settings-button'));1203openSettingsButton.classList.add(AbstractSettingRenderer.CONTROL_CLASS);1204openSettingsButton.role = 'button';12051206const validationErrorMessageElement = $('.setting-item-validation-message');1207common.containerElement.appendChild(validationErrorMessageElement);12081209const template: ISettingComplexItemTemplate = {1210...common,1211button: openSettingsButton,1212validationErrorMessageElement1213};12141215this.addSettingElementFocusHandler(template);12161217return template;1218}12191220renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingComplexItemTemplate): void {1221super.renderSettingElement(element, index, templateData);1222}12231224protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingComplexItemTemplate, onChange: (value: string) => void): void {1225const plainKey = getLanguageTagSettingPlainKey(dataElement.setting.key);1226const editLanguageSettingLabel = localize('editLanguageSettingLabel', "Edit settings for {0}", plainKey);1227const isLanguageTagSetting = dataElement.setting.isLanguageTagSetting;1228template.button.textContent = isLanguageTagSetting1229? editLanguageSettingLabel1230: SettingComplexRenderer.EDIT_IN_JSON_LABEL;12311232const onClickOrKeydown = (e: UIEvent) => {1233if (isLanguageTagSetting) {1234this._onApplyFilter.fire(`@${LANGUAGE_SETTING_TAG}${plainKey.replaceAll(' ', '')}`);1235} else {1236this._onDidOpenSettings.fire(dataElement.setting.key);1237}1238e.preventDefault();1239e.stopPropagation();1240};1241template.elementDisposables.add(DOM.addDisposableListener(template.button, DOM.EventType.CLICK, (e) => {1242onClickOrKeydown(e);1243}));1244template.elementDisposables.add(DOM.addDisposableListener(template.button, DOM.EventType.KEY_DOWN, (e) => {1245const ev = new StandardKeyboardEvent(e);1246if (ev.equals(KeyCode.Space) || ev.equals(KeyCode.Enter)) {1247onClickOrKeydown(e);1248}1249}));12501251this.renderValidations(dataElement, template);12521253if (isLanguageTagSetting) {1254template.button.setAttribute('aria-label', editLanguageSettingLabel);1255} else {1256template.button.setAttribute('aria-label', `${SettingComplexRenderer.EDIT_IN_JSON_LABEL}: ${dataElement.setting.key}`);1257}1258}12591260private renderValidations(dataElement: SettingsTreeSettingElement, template: ISettingComplexItemTemplate) {1261const errMsg = dataElement.isConfigured && getInvalidTypeError(dataElement.value, dataElement.setting.type);1262if (errMsg) {1263template.containerElement.classList.add('invalid-input');1264template.validationErrorMessageElement.innerText = errMsg;1265return;1266}12671268template.containerElement.classList.remove('invalid-input');1269}1270}12711272class SettingComplexObjectRenderer extends SettingComplexRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingComplexObjectItemTemplate> {12731274override templateId = SETTINGS_COMPLEX_OBJECT_TEMPLATE_ID;12751276override renderTemplate(container: HTMLElement): ISettingComplexObjectItemTemplate {1277const common = this.renderCommonTemplate(null, container, 'list');12781279const objectSettingWidget = common.toDispose.add(this._instantiationService.createInstance(ObjectSettingDropdownWidget, common.controlElement));1280objectSettingWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS);12811282const openSettingsButton = DOM.append(DOM.append(common.controlElement, $('.complex-object-edit-in-settings-button-container')), $('a.complex-object.edit-in-settings-button'));1283openSettingsButton.classList.add(AbstractSettingRenderer.CONTROL_CLASS);1284openSettingsButton.role = 'button';12851286const validationErrorMessageElement = $('.setting-item-validation-message');1287common.containerElement.appendChild(validationErrorMessageElement);12881289const template: ISettingComplexObjectItemTemplate = {1290...common,1291button: openSettingsButton,1292validationErrorMessageElement,1293objectSettingWidget1294};12951296this.addSettingElementFocusHandler(template);12971298return template;1299}13001301protected override renderValue(dataElement: SettingsTreeSettingElement, template: ISettingComplexObjectItemTemplate, onChange: (value: string) => void): void {1302const items = getObjectDisplayValue(dataElement);1303template.objectSettingWidget.setValue(items, {1304settingKey: dataElement.setting.key,1305showAddButton: false,1306isReadOnly: true,1307});1308template.button.parentElement?.classList.toggle('hide', dataElement.hasPolicyValue);1309super.renderValue(dataElement, template, onChange);1310}1311}13121313class SettingArrayRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingListItemTemplate> {1314templateId = SETTINGS_ARRAY_TEMPLATE_ID;13151316renderTemplate(container: HTMLElement): ISettingListItemTemplate {1317const common = this.renderCommonTemplate(null, container, 'list');1318// eslint-disable-next-line no-restricted-syntax1319const descriptionElement = common.containerElement.querySelector('.setting-item-description')!;1320const validationErrorMessageElement = $('.setting-item-validation-message');1321descriptionElement.after(validationErrorMessageElement);13221323const listWidget = this._instantiationService.createInstance(ListSettingWidget, common.controlElement);1324listWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS);1325common.toDispose.add(listWidget);13261327const template: ISettingListItemTemplate = {1328...common,1329listWidget,1330validationErrorMessageElement1331};13321333this.addSettingElementFocusHandler(template);13341335common.toDispose.add(1336listWidget.onDidChangeList(e => {1337const newList = this.computeNewList(template, e);1338template.onChange?.(newList);1339})1340);13411342return template;1343}13441345private computeNewList(template: ISettingListItemTemplate, e: SettingListEvent<IListDataItem>): string[] | undefined {1346if (template.context) {1347let newValue: string[] = [];1348if (Array.isArray(template.context.scopeValue)) {1349newValue = [...template.context.scopeValue];1350} else if (Array.isArray(template.context.value)) {1351newValue = [...template.context.value];1352}13531354if (e.type === 'move') {1355// A drag and drop occurred1356const sourceIndex = e.sourceIndex;1357const targetIndex = e.targetIndex;1358const splicedElem = newValue.splice(sourceIndex, 1)[0];1359newValue.splice(targetIndex, 0, splicedElem);1360} else if (e.type === 'remove' || e.type === 'reset') {1361newValue.splice(e.targetIndex, 1);1362} else if (e.type === 'change') {1363const itemValueData = e.newItem.value.data.toString();13641365// Update value1366if (e.targetIndex > -1) {1367newValue[e.targetIndex] = itemValueData;1368}1369// For some reason, we are updating and cannot find original value1370// Just append the value in this case1371else {1372newValue.push(itemValueData);1373}1374} else if (e.type === 'add') {1375newValue.push(e.newItem.value.data.toString());1376}13771378if (1379template.context.defaultValue &&1380Array.isArray(template.context.defaultValue) &&1381template.context.defaultValue.length === newValue.length &&1382template.context.defaultValue.join() === newValue.join()1383) {1384return undefined;1385}1386return newValue;1387}13881389return undefined;1390}13911392renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingListItemTemplate): void {1393super.renderSettingElement(element, index, templateData);1394}13951396protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingListItemTemplate, onChange: (value: string[] | number[] | undefined) => void): void {1397const value = getListDisplayValue(dataElement);1398const keySuggester = dataElement.setting.enum ? createArraySuggester(dataElement) : undefined;1399template.listWidget.setValue(value, {1400showAddButton: getShowAddButtonList(dataElement, value),1401keySuggester1402});1403template.context = dataElement;14041405template.elementDisposables.add(toDisposable(() => {1406template.listWidget.cancelEdit();1407}));14081409template.onChange = (v: string[] | undefined) => {1410if (v && !renderArrayValidations(dataElement, template, v, false)) {1411const itemType = dataElement.setting.arrayItemType;1412const arrToSave = isNonNullableNumericType(itemType) ? v.map(a => +a) : v;1413onChange(arrToSave);1414} else {1415// Save the setting unparsed and containing the errors.1416// renderArrayValidations will render relevant error messages.1417onChange(v);1418}1419};14201421renderArrayValidations(dataElement, template, value.map(v => v.value.data.toString()), true);1422}1423}14241425abstract class AbstractSettingObjectRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingObjectItemTemplate> {14261427protected renderTemplateWithWidget(common: ISettingItemTemplate, widget: ObjectSettingCheckboxWidget | ObjectSettingDropdownWidget): ISettingObjectItemTemplate {1428widget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS);1429common.toDispose.add(widget);14301431// eslint-disable-next-line no-restricted-syntax1432const descriptionElement = common.containerElement.querySelector('.setting-item-description')!;1433const validationErrorMessageElement = $('.setting-item-validation-message');1434descriptionElement.after(validationErrorMessageElement);14351436const template: ISettingObjectItemTemplate = {1437...common,1438validationErrorMessageElement1439};1440if (widget instanceof ObjectSettingCheckboxWidget) {1441template.objectCheckboxWidget = widget;1442} else {1443template.objectDropdownWidget = widget;1444}14451446this.addSettingElementFocusHandler(template);1447return template;1448}14491450renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingObjectItemTemplate): void {1451super.renderSettingElement(element, index, templateData);1452}1453}14541455class SettingObjectRenderer extends AbstractSettingObjectRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingObjectItemTemplate> {1456override templateId = SETTINGS_OBJECT_TEMPLATE_ID;14571458renderTemplate(container: HTMLElement): ISettingObjectItemTemplate {1459const common = this.renderCommonTemplate(null, container, 'list');1460const widget = this._instantiationService.createInstance(ObjectSettingDropdownWidget, common.controlElement);1461const template = this.renderTemplateWithWidget(common, widget);1462common.toDispose.add(widget.onDidChangeList(e => {1463this.onDidChangeObject(template, e);1464}));1465return template;1466}14671468private onDidChangeObject(template: ISettingObjectItemTemplate, e: SettingListEvent<IObjectDataItem>): void {1469const widget = template.objectDropdownWidget!;1470if (template.context) {1471const settingSupportsRemoveDefault = objectSettingSupportsRemoveDefaultValue(template.context.setting.key);1472const defaultValue: Record<string, unknown> = typeof template.context.defaultValue === 'object'1473? template.context.defaultValue ?? {}1474: {};14751476const scopeValue: Record<string, unknown> = typeof template.context.scopeValue === 'object'1477? template.context.scopeValue ?? {}1478: {};14791480const newValue: Record<string, unknown> = { ...template.context.scopeValue }; // Initialize with scoped values as removed default values are not rendered1481const newItems: IObjectDataItem[] = [];14821483widget.items.forEach((item, idx) => {1484// Item was updated1485if ((e.type === 'change' || e.type === 'move') && e.targetIndex === idx) {1486// If the key of the default value is changed, remove the default value1487if (e.originalItem.key.data !== e.newItem.key.data && settingSupportsRemoveDefault && e.originalItem.key.data in defaultValue) {1488newValue[e.originalItem.key.data] = null;1489} else {1490delete newValue[e.originalItem.key.data];1491}1492newValue[e.newItem.key.data] = e.newItem.value.data;1493newItems.push(e.newItem);1494}1495// All remaining items, but skip the one that we just updated1496else if ((e.type !== 'change' && e.type !== 'move') || e.newItem.key.data !== item.key.data) {1497newValue[item.key.data] = item.value.data;1498newItems.push(item);1499}1500});15011502// Item was deleted1503if (e.type === 'remove' || e.type === 'reset') {1504const objectKey = e.originalItem.key.data;1505const removingDefaultValue = e.type === 'remove' && settingSupportsRemoveDefault && defaultValue[objectKey] === e.originalItem.value.data;1506if (removingDefaultValue) {1507newValue[objectKey] = null;1508} else {1509delete newValue[objectKey];1510}15111512const itemToDelete = newItems.findIndex(item => item.key.data === objectKey);1513const defaultItemValue = defaultValue[objectKey] as string | boolean;15141515// Item does not have a default or default is bing removed1516if (removingDefaultValue || isUndefinedOrNull(defaultValue[objectKey]) && itemToDelete > -1) {1517newItems.splice(itemToDelete, 1);1518} else if (!removingDefaultValue && itemToDelete > -1) {1519newItems[itemToDelete].value.data = defaultItemValue;1520}1521}1522// New item was added1523else if (e.type === 'add') {1524newValue[e.newItem.key.data] = e.newItem.value.data;1525newItems.push(e.newItem);1526}15271528Object.entries(newValue).forEach(([key, value]) => {1529// value from the scope has changed back to the default1530if (scopeValue[key] !== value && defaultValue[key] === value && !(settingSupportsRemoveDefault && value === null)) {1531delete newValue[key];1532}1533});15341535const newObject = Object.keys(newValue).length === 0 ? undefined : newValue;1536template.objectDropdownWidget!.setValue(newItems);1537template.onChange?.(newObject);1538}1539}15401541protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingObjectItemTemplate, onChange: (value: Record<string, unknown> | undefined) => void): void {1542const items = getObjectDisplayValue(dataElement);1543const { key, objectProperties, objectPatternProperties, objectAdditionalProperties, propertyNames } = dataElement.setting;15441545template.objectDropdownWidget!.setValue(items, {1546settingKey: key,1547showAddButton: objectAdditionalProperties === false1548? (1549!areAllPropertiesDefined(Object.keys(objectProperties ?? {}), items) ||1550isDefined(objectPatternProperties)1551)1552: true,1553keySuggester: createObjectKeySuggester(dataElement),1554valueSuggester: createObjectValueSuggester(dataElement),1555propertyNames1556});15571558template.context = dataElement;15591560template.elementDisposables.add(toDisposable(() => {1561template.objectDropdownWidget!.cancelEdit();1562}));15631564template.onChange = (v: Record<string, unknown> | undefined) => {1565if (v && !renderArrayValidations(dataElement, template, v, false)) {1566const parsedRecord = parseNumericObjectValues(dataElement, v);1567onChange(parsedRecord);1568} else {1569// Save the setting unparsed and containing the errors.1570// renderArrayValidations will render relevant error messages.1571onChange(v);1572}1573};1574renderArrayValidations(dataElement, template, dataElement.value, true);1575}1576}15771578class SettingBoolObjectRenderer extends AbstractSettingObjectRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingObjectItemTemplate> {1579override templateId = SETTINGS_BOOL_OBJECT_TEMPLATE_ID;15801581renderTemplate(container: HTMLElement): ISettingObjectItemTemplate {1582const common = this.renderCommonTemplate(null, container, 'list');1583const widget = this._instantiationService.createInstance(ObjectSettingCheckboxWidget, common.controlElement);1584const template = this.renderTemplateWithWidget(common, widget);1585common.toDispose.add(widget.onDidChangeList(e => {1586this.onDidChangeObject(template, e);1587}));1588return template;1589}15901591protected onDidChangeObject(template: ISettingObjectItemTemplate, e: SettingListEvent<IBoolObjectDataItem>): void {1592if (template.context) {1593const widget = template.objectCheckboxWidget!;1594const defaultValue: Record<string, unknown> = typeof template.context.defaultValue === 'object'1595? template.context.defaultValue ?? {}1596: {};15971598const scopeValue: Record<string, unknown> = typeof template.context.scopeValue === 'object'1599? template.context.scopeValue ?? {}1600: {};16011602const newValue: Record<string, unknown> = { ...template.context.scopeValue }; // Initialize with scoped values as removed default values are not rendered1603const newItems: IBoolObjectDataItem[] = [];16041605if (e.type !== 'change') {1606console.warn('Unexpected event type', e.type, 'for bool object setting', template.context.setting.key);1607return;1608}16091610widget.items.forEach((item, idx) => {1611// Item was updated1612if (e.targetIndex === idx) {1613newValue[e.newItem.key.data] = e.newItem.value.data;1614newItems.push(e.newItem);1615}1616// All remaining items, but skip the one that we just updated1617else if (e.newItem.key.data !== item.key.data) {1618newValue[item.key.data] = item.value.data;1619newItems.push(item);1620}1621});16221623Object.entries(newValue).forEach(([key, value]) => {1624// value from the scope has changed back to the default1625if (scopeValue[key] !== value && defaultValue[key] === value) {1626delete newValue[key];1627}1628});16291630const newObject = Object.keys(newValue).length === 0 ? undefined : newValue;1631template.objectCheckboxWidget!.setValue(newItems);1632template.onChange?.(newObject);16331634// Focus this setting explicitly, in case we were previously1635// focused on another setting and clicked a checkbox/value container1636// for this setting.1637this._onDidFocusSetting.fire(template.context);1638}1639}16401641protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingObjectItemTemplate, onChange: (value: Record<string, unknown> | undefined) => void): void {1642const items = getBoolObjectDisplayValue(dataElement);1643const { key } = dataElement.setting;16441645template.objectCheckboxWidget!.setValue(items, {1646settingKey: key1647});16481649template.context = dataElement;1650template.onChange = (v: Record<string, unknown> | undefined) => {1651onChange(v);1652};1653}1654}16551656abstract class SettingIncludeExcludeRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingIncludeExcludeItemTemplate> {16571658protected abstract isExclude(): boolean;16591660renderTemplate(container: HTMLElement): ISettingIncludeExcludeItemTemplate {1661const common = this.renderCommonTemplate(null, container, 'list');16621663const includeExcludeWidget = this._instantiationService.createInstance(this.isExclude() ? ExcludeSettingWidget : IncludeSettingWidget, common.controlElement);1664includeExcludeWidget.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS);1665common.toDispose.add(includeExcludeWidget);16661667const template: ISettingIncludeExcludeItemTemplate = {1668...common,1669includeExcludeWidget1670};16711672this.addSettingElementFocusHandler(template);16731674common.toDispose.add(includeExcludeWidget.onDidChangeList(e => this.onDidChangeIncludeExclude(template, e)));16751676return template;1677}16781679private onDidChangeIncludeExclude(template: ISettingIncludeExcludeItemTemplate, e: SettingListEvent<IListDataItem>): void {1680if (template.context) {1681const newValue = { ...template.context.scopeValue };16821683// first delete the existing entry, if present1684if (e.type !== 'add') {1685if (e.originalItem.value.data.toString() in template.context.defaultValue) {1686// delete a default by overriding it1687newValue[e.originalItem.value.data.toString()] = false;1688} else {1689delete newValue[e.originalItem.value.data.toString()];1690}1691}16921693// then add the new or updated entry, if present1694if (e.type === 'change' || e.type === 'add' || e.type === 'move') {1695if (e.newItem.value.data.toString() in template.context.defaultValue && !e.newItem.sibling) {1696// add a default by deleting its override1697delete newValue[e.newItem.value.data.toString()];1698} else {1699newValue[e.newItem.value.data.toString()] = e.newItem.sibling ? { when: e.newItem.sibling } : true;1700}1701}17021703function sortKeys<T extends object>(obj: T) {1704const sortedKeys = Object.keys(obj)1705.sort((a, b) => a.localeCompare(b)) as Array<keyof T>;17061707const retVal: Partial<T> = {};1708for (const key of sortedKeys) {1709retVal[key] = obj[key];1710}1711return retVal;1712}17131714this._onDidChangeSetting.fire({1715key: template.context.setting.key,1716value: Object.keys(newValue).length === 0 ? undefined : sortKeys(newValue),1717type: template.context.valueType,1718manualReset: false,1719scope: template.context.setting.scope1720});1721}1722}17231724renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingIncludeExcludeItemTemplate): void {1725super.renderSettingElement(element, index, templateData);1726}17271728protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingIncludeExcludeItemTemplate, onChange: (value: string) => void): void {1729const value = getIncludeExcludeDisplayValue(dataElement);1730template.includeExcludeWidget.setValue(value, { isReadOnly: dataElement.hasPolicyValue });1731template.context = dataElement;1732template.elementDisposables.add(toDisposable(() => {1733template.includeExcludeWidget.cancelEdit();1734}));1735}1736}17371738class SettingExcludeRenderer extends SettingIncludeExcludeRenderer {1739templateId = SETTINGS_EXCLUDE_TEMPLATE_ID;17401741protected override isExclude(): boolean {1742return true;1743}1744}17451746class SettingIncludeRenderer extends SettingIncludeExcludeRenderer {1747templateId = SETTINGS_INCLUDE_TEMPLATE_ID;17481749protected override isExclude(): boolean {1750return false;1751}1752}17531754const settingsInputBoxStyles = getInputBoxStyle({1755inputBackground: settingsTextInputBackground,1756inputForeground: settingsTextInputForeground,1757inputBorder: settingsTextInputBorder1758});17591760abstract class AbstractSettingTextRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingTextItemTemplate> {1761private readonly MULTILINE_MAX_HEIGHT = 150;17621763renderTemplate(_container: HTMLElement, useMultiline?: boolean): ISettingTextItemTemplate {1764const common = this.renderCommonTemplate(null, _container, 'text');1765const validationErrorMessageElement = DOM.append(common.containerElement, $('.setting-item-validation-message'));17661767const inputBoxOptions: IInputOptions = {1768flexibleHeight: useMultiline,1769flexibleWidth: false,1770flexibleMaxHeight: this.MULTILINE_MAX_HEIGHT,1771inputBoxStyles: settingsInputBoxStyles1772};1773const inputBox = new InputBox(common.controlElement, this._contextViewService, inputBoxOptions);1774common.toDispose.add(inputBox);1775common.toDispose.add(1776inputBox.onDidChange(e => {1777template.onChange?.(e);1778}));1779common.toDispose.add(inputBox);1780inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS);1781inputBox.inputElement.tabIndex = 0;17821783const template: ISettingTextItemTemplate = {1784...common,1785inputBox,1786validationErrorMessageElement1787};17881789this.addSettingElementFocusHandler(template);17901791return template;1792}17931794renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingTextItemTemplate): void {1795super.renderSettingElement(element, index, templateData);1796}17971798protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, onChange: (value: string) => void): void {1799template.onChange = undefined;1800template.inputBox.value = dataElement.value;1801template.inputBox.setEnabled(!dataElement.hasPolicyValue);1802template.inputBox.setAriaLabel(dataElement.setting.key);1803template.onChange = value => {1804if (!renderValidations(dataElement, template, false)) {1805onChange(value);1806}1807};18081809renderValidations(dataElement, template, true);1810}1811}18121813class SettingTextRenderer extends AbstractSettingTextRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingTextItemTemplate> {1814templateId = SETTINGS_TEXT_TEMPLATE_ID;18151816override renderTemplate(_container: HTMLElement): ISettingTextItemTemplate {1817const template = super.renderTemplate(_container, false);18181819// TODO@9at8: listWidget filters out all key events from input boxes, so we need to come up with a better way1820// Disable ArrowUp and ArrowDown behaviour in favor of list navigation1821template.toDispose.add(DOM.addStandardDisposableListener(template.inputBox.inputElement, DOM.EventType.KEY_DOWN, e => {1822if (e.equals(KeyCode.UpArrow) || e.equals(KeyCode.DownArrow)) {1823e.preventDefault();1824}1825}));18261827return template;1828}1829}18301831class SettingMultilineTextRenderer extends AbstractSettingTextRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingTextItemTemplate> {1832templateId = SETTINGS_MULTILINE_TEXT_TEMPLATE_ID;18331834override renderTemplate(_container: HTMLElement): ISettingTextItemTemplate {1835return super.renderTemplate(_container, true);1836}18371838protected override renderValue(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, onChange: (value: string) => void) {1839const onChangeOverride = (value: string) => {1840// Ensure the model is up to date since a different value will be rendered as different height when probing the height.1841dataElement.value = value;1842onChange(value);1843};1844super.renderValue(dataElement, template, onChangeOverride);1845template.elementDisposables.add(1846template.inputBox.onDidHeightChange(e => {1847const height = template.containerElement.clientHeight;1848// Don't fire event if height is reported as 0,1849// which sometimes happens when clicking onto a new setting.1850if (height) {1851this._onDidChangeSettingHeight.fire({1852element: dataElement,1853height: template.containerElement.clientHeight1854});1855}1856})1857);1858template.inputBox.layout();1859}1860}18611862class SettingEnumRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingEnumItemTemplate> {1863templateId = SETTINGS_ENUM_TEMPLATE_ID;18641865renderTemplate(container: HTMLElement): ISettingEnumItemTemplate {1866const common = this.renderCommonTemplate(null, container, 'enum');18671868const styles = getSelectBoxStyles({1869selectBackground: settingsSelectBackground,1870selectForeground: settingsSelectForeground,1871selectBorder: settingsSelectBorder,1872selectListBorder: settingsSelectListBorder1873});18741875const selectBox = new SelectBox([], 0, this._contextViewService, styles, {1876useCustomDrawn: !hasNativeContextMenu(this._configService) || !(isIOS && BrowserFeatures.pointerEvents)1877});18781879common.toDispose.add(selectBox);1880selectBox.render(common.controlElement);1881// eslint-disable-next-line no-restricted-syntax1882const selectElement = common.controlElement.querySelector('select');1883if (selectElement) {1884selectElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS);1885selectElement.tabIndex = 0;1886}18871888common.toDispose.add(1889selectBox.onDidSelect(e => {1890template.onChange?.(e.index);1891}));18921893const enumDescriptionElement = common.containerElement.insertBefore($('.setting-item-enumDescription'), common.descriptionElement.nextSibling);18941895const template: ISettingEnumItemTemplate = {1896...common,1897selectBox,1898selectElement,1899enumDescriptionElement1900};19011902this.addSettingElementFocusHandler(template);19031904return template;1905}19061907renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingEnumItemTemplate): void {1908super.renderSettingElement(element, index, templateData);1909}19101911protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingEnumItemTemplate, onChange: (value: string) => void): void {1912// Make shallow copies here so that we don't modify the actual dataElement later1913const enumItemLabels = dataElement.setting.enumItemLabels ? [...dataElement.setting.enumItemLabels] : [];1914const enumDescriptions = dataElement.setting.enumDescriptions ? [...dataElement.setting.enumDescriptions] : [];1915const settingEnum = [...dataElement.setting.enum!];1916const enumDescriptionsAreMarkdown = dataElement.setting.enumDescriptionsAreMarkdown;19171918const disposables = new DisposableStore();1919template.elementDisposables.add(disposables);19201921let createdDefault = false;1922if (!settingEnum.includes(dataElement.defaultValue)) {1923// Add a new potentially blank default setting1924settingEnum.unshift(dataElement.defaultValue);1925enumDescriptions.unshift('');1926enumItemLabels.unshift('');1927createdDefault = true;1928}19291930// Use String constructor in case of null or undefined values1931const stringifiedDefaultValue = escapeInvisibleChars(String(dataElement.defaultValue));1932const displayOptions: ISelectOptionItem[] = settingEnum1933.map(String)1934.map(escapeInvisibleChars)1935.map((data, index) => {1936const description = (enumDescriptions[index] && (enumDescriptionsAreMarkdown ? fixSettingLinks(enumDescriptions[index], false) : enumDescriptions[index]));1937return {1938text: enumItemLabels[index] ? enumItemLabels[index] : data,1939detail: enumItemLabels[index] ? data : '',1940description,1941descriptionIsMarkdown: enumDescriptionsAreMarkdown,1942descriptionMarkdownActionHandler: (content) => {1943this._openerService.open(content).catch(onUnexpectedError);1944},1945decoratorRight: (((data === stringifiedDefaultValue) || (createdDefault && index === 0)) ? localize('settings.Default', "default") : '')1946} satisfies ISelectOptionItem;1947});19481949template.selectBox.setOptions(displayOptions);1950template.selectBox.setAriaLabel(dataElement.setting.key);1951template.selectBox.setEnabled(!dataElement.hasPolicyValue);19521953let idx = settingEnum.indexOf(dataElement.value);1954if (idx === -1) {1955idx = 0;1956}19571958template.onChange = undefined;1959template.selectBox.select(idx);1960template.onChange = (idx) => {1961if (createdDefault && idx === 0) {1962onChange(dataElement.defaultValue);1963} else {1964onChange(settingEnum[idx]);1965}1966};19671968template.enumDescriptionElement.innerText = '';1969}1970}19711972const settingsNumberInputBoxStyles = getInputBoxStyle({1973inputBackground: settingsNumberInputBackground,1974inputForeground: settingsNumberInputForeground,1975inputBorder: settingsNumberInputBorder1976});19771978class SettingNumberRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingNumberItemTemplate> {1979templateId = SETTINGS_NUMBER_TEMPLATE_ID;19801981renderTemplate(_container: HTMLElement): ISettingNumberItemTemplate {1982const common = super.renderCommonTemplate(null, _container, 'number');1983const validationErrorMessageElement = DOM.append(common.containerElement, $('.setting-item-validation-message'));19841985const inputBox = new InputBox(common.controlElement, this._contextViewService, { type: 'number', inputBoxStyles: settingsNumberInputBoxStyles });1986common.toDispose.add(inputBox);1987common.toDispose.add(1988inputBox.onDidChange(e => {1989template.onChange?.(e);1990}));1991common.toDispose.add(inputBox);1992inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS);1993inputBox.inputElement.tabIndex = 0;19941995const template: ISettingNumberItemTemplate = {1996...common,1997inputBox,1998validationErrorMessageElement1999};20002001this.addSettingElementFocusHandler(template);20022003return template;2004}20052006renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingNumberItemTemplate): void {2007super.renderSettingElement(element, index, templateData);2008}20092010protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingNumberItemTemplate, onChange: (value: number | null) => void): void {2011const numParseFn = (dataElement.valueType === 'integer' || dataElement.valueType === 'nullable-integer')2012? parseInt : parseFloat;20132014const nullNumParseFn = (dataElement.valueType === 'nullable-integer' || dataElement.valueType === 'nullable-number')2015? ((v: string) => v === '' ? null : numParseFn(v)) : numParseFn;20162017template.onChange = undefined;2018template.inputBox.value = typeof dataElement.value === 'number' ?2019dataElement.value.toString() : '';2020template.inputBox.step = dataElement.valueType.includes('integer') ? '1' : 'any';2021template.inputBox.setAriaLabel(dataElement.setting.key);2022template.inputBox.setEnabled(!dataElement.hasPolicyValue);2023template.onChange = value => {2024if (!renderValidations(dataElement, template, false)) {2025onChange(nullNumParseFn(value));2026}2027};20282029renderValidations(dataElement, template, true);2030}2031}20322033class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingBoolItemTemplate> {2034templateId = SETTINGS_BOOL_TEMPLATE_ID;20352036renderTemplate(_container: HTMLElement): ISettingBoolItemTemplate {2037_container.classList.add('setting-item');2038_container.classList.add('setting-item-bool');20392040const toDispose = new DisposableStore();20412042const container = DOM.append(_container, $(AbstractSettingRenderer.CONTENTS_SELECTOR));2043container.classList.add('settings-row-inner-container');20442045const titleElement = DOM.append(container, $('.setting-item-title'));2046const categoryElement = DOM.append(titleElement, $('span.setting-item-category'));2047const labelElementContainer = DOM.append(titleElement, $('span.setting-item-label'));2048const labelElement = toDispose.add(new SimpleIconLabel(labelElementContainer));2049const indicatorsLabel = toDispose.add(this._instantiationService.createInstance(SettingsTreeIndicatorsLabel, titleElement));20502051const descriptionAndValueElement = DOM.append(container, $('.setting-item-value-description'));2052const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control'));2053const descriptionElement = DOM.append(descriptionAndValueElement, $('.setting-item-description'));2054const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator'));2055toDispose.add(this._hoverService.setupDelayedHover(modifiedIndicatorElement, {2056content: localize('modified', "The setting has been configured in the current scope.")2057}));20582059const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message'));20602061const checkbox = new Toggle({ icon: Codicon.check, actionClassName: 'setting-value-checkbox', isChecked: true, title: '', ...unthemedToggleStyles });2062controlElement.appendChild(checkbox.domNode);2063toDispose.add(checkbox);2064toDispose.add(checkbox.onChange(() => {2065template.onChange!(checkbox.checked);2066}));20672068checkbox.domNode.classList.add(AbstractSettingRenderer.CONTROL_CLASS);2069const toolbarContainer = DOM.append(container, $('.setting-toolbar-container'));2070const toolbar = this.renderSettingToolbar(toolbarContainer);2071toDispose.add(toolbar);20722073const template: ISettingBoolItemTemplate = {2074toDispose,2075elementDisposables: toDispose.add(new DisposableStore()),20762077containerElement: container,2078categoryElement,2079labelElement,2080controlElement,2081checkbox,2082descriptionElement,2083deprecationWarningElement,2084indicatorsLabel,2085toolbar2086};20872088this.addSettingElementFocusHandler(template);20892090// Prevent clicks from being handled by list2091toDispose.add(DOM.addDisposableListener(controlElement, 'mousedown', (e: IMouseEvent) => e.stopPropagation()));2092toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_ENTER, e => container.classList.add('mouseover')));2093toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover')));20942095return template;2096}20972098renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingBoolItemTemplate): void {2099super.renderSettingElement(element, index, templateData);2100}21012102protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingBoolItemTemplate, onChange: (value: boolean) => void): void {2103template.onChange = undefined;2104template.checkbox.checked = dataElement.value;2105if (dataElement.hasPolicyValue) {2106template.checkbox.disable();2107template.descriptionElement.classList.add('disabled');2108} else {2109template.checkbox.enable();2110template.descriptionElement.classList.remove('disabled');21112112// Need to listen for mouse clicks on description and toggle checkbox - use target ID for safety2113// Also have to ignore embedded links - too buried to stop propagation2114template.elementDisposables.add(DOM.addDisposableListener(template.descriptionElement, DOM.EventType.MOUSE_DOWN, (e) => {2115const targetElement = <HTMLElement>e.target;21162117// Toggle target checkbox2118if (targetElement.tagName.toLowerCase() !== 'a') {2119template.checkbox.checked = !template.checkbox.checked;2120template.onChange!(template.checkbox.checked);2121}2122DOM.EventHelper.stop(e);2123}));2124}2125template.checkbox.setTitle(dataElement.setting.key);2126template.onChange = onChange;2127}2128}21292130type ManageExtensionClickTelemetryClassification = {2131extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension the user went to manage.' };2132owner: 'rzhao271';2133comment: 'Event used to gain insights into when users interact with an extension management setting';2134};21352136class SettingsExtensionToggleRenderer extends AbstractSettingRenderer implements ITreeRenderer<SettingsTreeSettingElement, never, ISettingExtensionToggleItemTemplate> {2137templateId = SETTINGS_EXTENSION_TOGGLE_TEMPLATE_ID;21382139private readonly _onDidDismissExtensionSetting = this._register(new Emitter<string>());2140readonly onDidDismissExtensionSetting = this._onDidDismissExtensionSetting.event;21412142renderTemplate(_container: HTMLElement): ISettingExtensionToggleItemTemplate {2143const common = super.renderCommonTemplate(null, _container, 'extension-toggle');21442145const actionButton = new Button(common.containerElement, {2146title: false,2147...defaultButtonStyles2148});2149actionButton.element.classList.add('setting-item-extension-toggle-button');2150actionButton.label = localize('showExtension', "Show Extension");21512152const dismissButton = new Button(common.containerElement, {2153title: false,2154secondary: true,2155...defaultButtonStyles2156});2157dismissButton.element.classList.add('setting-item-extension-dismiss-button');2158dismissButton.label = localize('dismiss', "Dismiss");21592160const template: ISettingExtensionToggleItemTemplate = {2161...common,2162actionButton,2163dismissButton2164};21652166this.addSettingElementFocusHandler(template);21672168return template;2169}21702171renderElement(element: ITreeNode<SettingsTreeSettingElement, never>, index: number, templateData: ISettingExtensionToggleItemTemplate): void {2172super.renderSettingElement(element, index, templateData);2173}21742175protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingExtensionToggleItemTemplate, onChange: (_: undefined) => void): void {2176template.elementDisposables.clear();21772178const extensionId = dataElement.setting.displayExtensionId!;2179template.elementDisposables.add(template.actionButton.onDidClick(async () => {2180this._telemetryService.publicLog2<{ extensionId: String }, ManageExtensionClickTelemetryClassification>('ManageExtensionClick', { extensionId });2181this._commandService.executeCommand('extension.open', extensionId);2182}));21832184template.elementDisposables.add(template.dismissButton.onDidClick(async () => {2185this._telemetryService.publicLog2<{ extensionId: String }, ManageExtensionClickTelemetryClassification>('DismissExtensionClick', { extensionId });2186this._onDidDismissExtensionSetting.fire(extensionId);2187}));2188}2189}21902191export class SettingTreeRenderers extends Disposable {2192readonly onDidClickOverrideElement: Event<ISettingOverrideClickEvent>;21932194private readonly _onDidChangeSetting = this._register(new Emitter<ISettingChangeEvent>());2195readonly onDidChangeSetting: Event<ISettingChangeEvent>;21962197readonly onDidDismissExtensionSetting: Event<string>;21982199readonly onDidOpenSettings: Event<string>;22002201readonly onDidClickSettingLink: Event<ISettingLinkClickEvent>;22022203readonly onDidFocusSetting: Event<SettingsTreeSettingElement>;22042205readonly onDidChangeSettingHeight: Event<HeightChangeParams>;22062207readonly onApplyFilter: Event<string>;22082209readonly allRenderers: ITreeRenderer<SettingsTreeElement, never, any>[];22102211private readonly settingActions: IAction[];22122213constructor(2214@IInstantiationService private readonly _instantiationService: IInstantiationService,2215@IContextMenuService private readonly _contextMenuService: IContextMenuService,2216@IContextViewService private readonly _contextViewService: IContextViewService,2217@IUserDataSyncEnablementService private readonly _userDataSyncEnablementService: IUserDataSyncEnablementService,2218) {2219super();2220this.settingActions = [2221new Action('settings.resetSetting', localize('resetSettingLabel', "Reset Setting"), undefined, undefined, async context => {2222if (context instanceof SettingsTreeSettingElement) {2223if (!context.isUntrusted) {2224this._onDidChangeSetting.fire({2225key: context.setting.key,2226value: undefined,2227type: context.setting.type as SettingValueType,2228manualReset: true,2229scope: context.setting.scope2230});2231}2232}2233}),2234new Separator(),2235this._instantiationService.createInstance(CopySettingIdAction),2236this._instantiationService.createInstance(CopySettingAsJSONAction),2237this._instantiationService.createInstance(CopySettingAsURLAction),2238];22392240const actionFactory = (setting: ISetting, settingTarget: SettingsTarget) => this.getActionsForSetting(setting, settingTarget);2241const emptyActionFactory = (_: ISetting) => [];2242const extensionRenderer = this._instantiationService.createInstance(SettingsExtensionToggleRenderer, [], emptyActionFactory);2243const settingRenderers = [2244this._instantiationService.createInstance(SettingBoolRenderer, this.settingActions, actionFactory),2245this._instantiationService.createInstance(SettingNumberRenderer, this.settingActions, actionFactory),2246this._instantiationService.createInstance(SettingArrayRenderer, this.settingActions, actionFactory),2247this._instantiationService.createInstance(SettingComplexRenderer, this.settingActions, actionFactory),2248this._instantiationService.createInstance(SettingComplexObjectRenderer, this.settingActions, actionFactory),2249this._instantiationService.createInstance(SettingTextRenderer, this.settingActions, actionFactory),2250this._instantiationService.createInstance(SettingMultilineTextRenderer, this.settingActions, actionFactory),2251this._instantiationService.createInstance(SettingExcludeRenderer, this.settingActions, actionFactory),2252this._instantiationService.createInstance(SettingIncludeRenderer, this.settingActions, actionFactory),2253this._instantiationService.createInstance(SettingEnumRenderer, this.settingActions, actionFactory),2254this._instantiationService.createInstance(SettingObjectRenderer, this.settingActions, actionFactory),2255this._instantiationService.createInstance(SettingBoolObjectRenderer, this.settingActions, actionFactory),2256extensionRenderer2257];22582259this.onDidClickOverrideElement = Event.any(...settingRenderers.map(r => r.onDidClickOverrideElement));2260this.onDidChangeSetting = Event.any(2261...settingRenderers.map(r => r.onDidChangeSetting),2262this._onDidChangeSetting.event2263);2264this.onDidDismissExtensionSetting = extensionRenderer.onDidDismissExtensionSetting;2265this.onDidOpenSettings = Event.any(...settingRenderers.map(r => r.onDidOpenSettings));2266this.onDidClickSettingLink = Event.any(...settingRenderers.map(r => r.onDidClickSettingLink));2267this.onDidFocusSetting = Event.any(...settingRenderers.map(r => r.onDidFocusSetting));2268this.onDidChangeSettingHeight = Event.any(...settingRenderers.map(r => r.onDidChangeSettingHeight));2269this.onApplyFilter = Event.any(...settingRenderers.map(r => r.onApplyFilter));22702271this.allRenderers = [2272...settingRenderers,2273this._instantiationService.createInstance(SettingGroupRenderer),2274this._instantiationService.createInstance(SettingNewExtensionsRenderer),2275];2276}22772278private getActionsForSetting(setting: ISetting, settingTarget: SettingsTarget): IAction[] {2279const actions: IAction[] = [];2280if (!(setting.scope && APPLICATION_SCOPES.includes(setting.scope)) && settingTarget === ConfigurationTarget.USER_LOCAL) {2281actions.push(this._instantiationService.createInstance(ApplySettingToAllProfilesAction, setting));2282}2283if (this._userDataSyncEnablementService.isEnabled() && !setting.disallowSyncIgnore) {2284actions.push(this._instantiationService.createInstance(SyncSettingAction, setting));2285}2286if (actions.length) {2287actions.splice(0, 0, new Separator());2288}2289return actions;2290}22912292cancelSuggesters() {2293this._contextViewService.hideContextView();2294}22952296showContextMenu(element: SettingsTreeSettingElement, settingDOMElement: HTMLElement): void {2297// eslint-disable-next-line no-restricted-syntax2298const toolbarElement = settingDOMElement.querySelector('.monaco-toolbar');2299if (toolbarElement) {2300this._contextMenuService.showContextMenu({2301getActions: () => this.settingActions,2302getAnchor: () => <HTMLElement>toolbarElement,2303getActionsContext: () => element2304});2305}2306}23072308getSettingDOMElementForDOMElement(domElement: HTMLElement): HTMLElement | null {2309const parent = DOM.findParentWithClass(domElement, AbstractSettingRenderer.CONTENTS_CLASS);2310if (parent) {2311return parent;2312}23132314return null;2315}23162317getDOMElementsForSettingKey(treeContainer: HTMLElement, key: string): NodeListOf<HTMLElement> {2318// eslint-disable-next-line no-restricted-syntax2319return treeContainer.querySelectorAll(`[${AbstractSettingRenderer.SETTING_KEY_ATTR}="${key}"]`);2320}23212322getKeyForDOMElementInSetting(element: HTMLElement): string | null {2323const settingElement = this.getSettingDOMElementForDOMElement(element);2324return settingElement && settingElement.getAttribute(AbstractSettingRenderer.SETTING_KEY_ATTR);2325}23262327getIdForDOMElementInSetting(element: HTMLElement): string | null {2328const settingElement = this.getSettingDOMElementForDOMElement(element);2329return settingElement && settingElement.getAttribute(AbstractSettingRenderer.SETTING_ID_ATTR);2330}23312332override dispose(): void {2333super.dispose();2334this.settingActions.forEach(action => {2335if (isDisposable(action)) {2336action.dispose();2337}2338});2339this.allRenderers.forEach(renderer => {2340if (isDisposable(renderer)) {2341renderer.dispose();2342}2343});2344}2345}23462347/**2348* Validate and render any error message. Returns true if the value is invalid.2349*/2350function renderValidations(dataElement: SettingsTreeSettingElement, template: ISettingTextItemTemplate, calledOnStartup: boolean): boolean {2351if (dataElement.setting.validator) {2352const errMsg = dataElement.setting.validator(template.inputBox.value);2353if (errMsg) {2354template.containerElement.classList.add('invalid-input');2355template.validationErrorMessageElement.innerText = errMsg;2356const validationError = localize('validationError', "Validation Error.");2357template.inputBox.inputElement.parentElement!.setAttribute('aria-label', [validationError, errMsg].join(' '));2358if (!calledOnStartup) { aria.status(validationError + ' ' + errMsg); }2359return true;2360} else {2361template.inputBox.inputElement.parentElement!.removeAttribute('aria-label');2362}2363}2364template.containerElement.classList.remove('invalid-input');2365return false;2366}23672368/**2369* Validate and render any error message for arrays. Returns true if the value is invalid.2370*/2371function renderArrayValidations(2372dataElement: SettingsTreeSettingElement,2373template: ISettingListItemTemplate | ISettingObjectItemTemplate,2374value: string[] | Record<string, unknown> | undefined,2375calledOnStartup: boolean2376): boolean {2377template.containerElement.classList.add('invalid-input');2378if (dataElement.setting.validator) {2379const errMsg = dataElement.setting.validator(value);2380if (errMsg && errMsg !== '') {2381template.containerElement.classList.add('invalid-input');2382template.validationErrorMessageElement.innerText = errMsg;2383const validationError = localize('validationError', "Validation Error.");2384template.containerElement.setAttribute('aria-label', [dataElement.setting.key, validationError, errMsg].join(' '));2385if (!calledOnStartup) { aria.status(validationError + ' ' + errMsg); }2386return true;2387} else {2388template.containerElement.setAttribute('aria-label', dataElement.setting.key);2389template.containerElement.classList.remove('invalid-input');2390}2391}2392return false;2393}23942395function cleanRenderedMarkdown(element: Node): void {2396for (let i = 0; i < element.childNodes.length; i++) {2397const child = element.childNodes.item(i);23982399const tagName = (<Element>child).tagName && (<Element>child).tagName.toLowerCase();2400if (tagName === 'img') {2401child.remove();2402} else {2403cleanRenderedMarkdown(child);2404}2405}2406}24072408function fixSettingLinks(text: string, linkify = true): string {2409return text.replace(/`#([^#\s`]+)#`|'#([^#\s']+)#'/g, (match, backticksGroup, quotesGroup) => {2410const settingKey: string = backticksGroup ?? quotesGroup;2411const targetDisplayFormat = settingKeyToDisplayFormat(settingKey);2412const targetName = `${targetDisplayFormat.category}: ${targetDisplayFormat.label}`;2413return linkify ?2414`[${targetName}](#${settingKey} "${settingKey}")` :2415`"${targetName}"`;2416});2417}24182419function escapeInvisibleChars(enumValue: string): string {2420return enumValue && enumValue2421.replace(/\n/g, '\\n')2422.replace(/\r/g, '\\r');2423}242424252426export class SettingsTreeFilter implements ITreeFilter<SettingsTreeElement> {2427constructor(2428private viewState: ISettingsEditorViewState,2429private isFilteringGroups: boolean,2430@IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService,2431) { }24322433filter(element: SettingsTreeElement, parentVisibility: TreeVisibility): TreeFilterResult<void> {2434// Filter during search2435if (this.viewState.categoryFilter && element instanceof SettingsTreeSettingElement) {2436if (!this.settingContainedInGroup(element.setting, this.viewState.categoryFilter)) {2437return false;2438}2439}24402441// Non-user scope selected2442if (element instanceof SettingsTreeSettingElement && this.viewState.settingsTarget !== ConfigurationTarget.USER_LOCAL) {2443const isRemote = !!this.environmentService.remoteAuthority;2444if (!element.matchesScope(this.viewState.settingsTarget, isRemote)) {2445return false;2446}2447}24482449// Group with no visible children2450if (element instanceof SettingsTreeGroupElement) {2451// When filtering to a specific category, only show that category and its descendants2452if (this.isFilteringGroups && this.viewState.categoryFilter) {2453if (!this.groupIsRelatedToCategory(element, this.viewState.categoryFilter)) {2454return false;2455}2456// For groups related to the category, skip the count check and recurse2457// to let child settings be filtered2458return TreeVisibility.Recurse;2459}24602461if (typeof element.count === 'number') {2462return element.count > 0;2463}24642465return TreeVisibility.Recurse;2466}24672468// Filtered "new extensions" button2469if (element instanceof SettingsTreeNewExtensionsElement) {2470if (this.viewState.tagFilters?.size || this.viewState.categoryFilter) {2471return false;2472}2473}24742475return true;2476}24772478private settingContainedInGroup(setting: ISetting, group: SettingsTreeGroupElement): boolean {2479return group.children.some(child => {2480if (child instanceof SettingsTreeGroupElement) {2481return this.settingContainedInGroup(setting, child);2482} else if (child instanceof SettingsTreeSettingElement) {2483return child.setting.key === setting.key;2484} else {2485return false;2486}2487});2488}24892490/**2491* Checks if a group is related to the filtered category.2492* A group is related if it's the category itself, a descendant of it, or an ancestor of it.2493*/2494private groupIsRelatedToCategory(group: SettingsTreeGroupElement, category: SettingsTreeGroupElement): boolean {2495// Check if this group is the category itself2496if (group.id === category.id) {2497return true;2498}24992500// Check if this group is a descendant of the category2501let parent = group.parent;2502while (parent) {2503if (parent.id === category.id) {2504return true;2505}2506parent = parent.parent;2507}25082509// Check if this group is an ancestor of the category2510let categoryParent = category.parent;2511while (categoryParent) {2512if (categoryParent.id === group.id) {2513return true;2514}2515categoryParent = categoryParent.parent;2516}25172518return false;2519}2520}25212522class SettingsTreeDelegate extends CachedListVirtualDelegate<SettingsTreeGroupChild> {25232524getTemplateId(element: SettingsTreeGroupElement | SettingsTreeSettingElement | SettingsTreeNewExtensionsElement): string {2525if (element instanceof SettingsTreeGroupElement) {2526return SETTINGS_ELEMENT_TEMPLATE_ID;2527}25282529if (element instanceof SettingsTreeSettingElement) {2530if (element.valueType === SettingValueType.ExtensionToggle) {2531return SETTINGS_EXTENSION_TOGGLE_TEMPLATE_ID;2532}25332534const invalidTypeError = element.isConfigured && getInvalidTypeError(element.value, element.setting.type);2535if (invalidTypeError) {2536return SETTINGS_COMPLEX_TEMPLATE_ID;2537}25382539if (element.valueType === SettingValueType.Boolean) {2540return SETTINGS_BOOL_TEMPLATE_ID;2541}25422543if (element.valueType === SettingValueType.Integer ||2544element.valueType === SettingValueType.Number ||2545element.valueType === SettingValueType.NullableInteger ||2546element.valueType === SettingValueType.NullableNumber) {2547return SETTINGS_NUMBER_TEMPLATE_ID;2548}25492550if (element.valueType === SettingValueType.MultilineString) {2551return SETTINGS_MULTILINE_TEXT_TEMPLATE_ID;2552}25532554if (element.valueType === SettingValueType.String) {2555return SETTINGS_TEXT_TEMPLATE_ID;2556}25572558if (element.valueType === SettingValueType.Enum) {2559return SETTINGS_ENUM_TEMPLATE_ID;2560}25612562if (element.valueType === SettingValueType.Array) {2563return SETTINGS_ARRAY_TEMPLATE_ID;2564}25652566if (element.valueType === SettingValueType.Exclude) {2567return SETTINGS_EXCLUDE_TEMPLATE_ID;2568}25692570if (element.valueType === SettingValueType.Include) {2571return SETTINGS_INCLUDE_TEMPLATE_ID;2572}25732574if (element.valueType === SettingValueType.Object) {2575return SETTINGS_OBJECT_TEMPLATE_ID;2576}25772578if (element.valueType === SettingValueType.BooleanObject) {2579return SETTINGS_BOOL_OBJECT_TEMPLATE_ID;2580}25812582if (element.valueType === SettingValueType.ComplexObject) {2583return SETTINGS_COMPLEX_OBJECT_TEMPLATE_ID;2584}25852586if (element.valueType === SettingValueType.LanguageTag) {2587return SETTINGS_COMPLEX_TEMPLATE_ID;2588}25892590return SETTINGS_COMPLEX_TEMPLATE_ID;2591}25922593if (element instanceof SettingsTreeNewExtensionsElement) {2594return SETTINGS_NEW_EXTENSIONS_TEMPLATE_ID;2595}25962597throw new Error('unknown element type: ' + element);2598}25992600hasDynamicHeight(element: SettingsTreeGroupElement | SettingsTreeSettingElement | SettingsTreeNewExtensionsElement): boolean {2601return !(element instanceof SettingsTreeGroupElement);2602}26032604protected estimateHeight(element: SettingsTreeGroupChild): number {2605if (element instanceof SettingsTreeGroupElement) {2606return 42;2607}26082609return element instanceof SettingsTreeSettingElement && element.valueType === SettingValueType.Boolean ? 78 : 104;2610}2611}26122613export class NonCollapsibleObjectTreeModel<T> extends ObjectTreeModel<T> {2614override isCollapsible(element: T): boolean {2615return false;2616}26172618override setCollapsed(element: T, collapsed?: boolean, recursive?: boolean): boolean {2619return false;2620}2621}26222623class SettingsTreeAccessibilityProvider implements IListAccessibilityProvider<SettingsTreeElement> {2624constructor(private readonly configurationService: IWorkbenchConfigurationService, private readonly languageService: ILanguageService, private readonly userDataProfilesService: IUserDataProfilesService) {2625}26262627getAriaLabel(element: SettingsTreeElement) {2628if (element instanceof SettingsTreeSettingElement) {2629const ariaLabelSections: string[] = [];2630ariaLabelSections.push(`${element.displayCategory} ${element.displayLabel}.`);26312632if (element.isConfigured) {2633const modifiedText = localize('settings.Modified', 'Modified.');2634ariaLabelSections.push(modifiedText);2635}26362637const indicatorsLabelAriaLabel = getIndicatorsLabelAriaLabel(element, this.configurationService, this.userDataProfilesService, this.languageService);2638if (indicatorsLabelAriaLabel.length) {2639ariaLabelSections.push(`${indicatorsLabelAriaLabel}.`);2640}26412642const descriptionWithoutSettingLinks = renderAsPlaintext({ value: fixSettingLinks(element.description, false) });2643if (descriptionWithoutSettingLinks.length) {2644ariaLabelSections.push(descriptionWithoutSettingLinks);2645}2646return ariaLabelSections.join(' ');2647} else if (element instanceof SettingsTreeGroupElement) {2648return element.label;2649} else {2650return element.id;2651}2652}26532654getWidgetAriaLabel() {2655return localize('settings', "Settings");2656}2657}26582659export class SettingsTree extends WorkbenchObjectTree<SettingsTreeElement> {2660constructor(2661container: HTMLElement,2662viewState: ISettingsEditorViewState,2663renderers: ITreeRenderer<any, void, any>[],2664@IContextKeyService contextKeyService: IContextKeyService,2665@IListService listService: IListService,2666@IWorkbenchConfigurationService configurationService: IWorkbenchConfigurationService,2667@IInstantiationService instantiationService: IInstantiationService,2668@ILanguageService languageService: ILanguageService,2669@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService2670) {2671super('SettingsTree', container,2672new SettingsTreeDelegate(),2673renderers,2674{2675horizontalScrolling: false,2676supportDynamicHeights: true,2677scrollToActiveElement: true,2678identityProvider: {2679getId(e) {2680return e.id;2681}2682},2683accessibilityProvider: new SettingsTreeAccessibilityProvider(configurationService, languageService, userDataProfilesService),2684styleController: id => new DefaultStyleController(domStylesheetsJs.createStyleSheet(container), id),2685filter: instantiationService.createInstance(SettingsTreeFilter, viewState, true),2686smoothScrolling: configurationService.getValue<boolean>('workbench.list.smoothScrolling'),2687multipleSelectionSupport: false,2688findWidgetEnabled: false,2689renderIndentGuides: RenderIndentGuides.None,2690transformOptimization: false // Disable transform optimization #1774702691},2692instantiationService,2693contextKeyService,2694listService,2695configurationService,2696);26972698this.getHTMLElement().classList.add('settings-editor-tree');26992700this.style(getListStyles({2701listBackground: editorBackground,2702listActiveSelectionBackground: editorBackground,2703listActiveSelectionForeground: foreground,2704listFocusAndSelectionBackground: editorBackground,2705listFocusAndSelectionForeground: foreground,2706listFocusBackground: editorBackground,2707listFocusForeground: foreground,2708listHoverForeground: foreground,2709listHoverBackground: editorBackground,2710listHoverOutline: editorBackground,2711listFocusOutline: editorBackground,2712listInactiveSelectionBackground: editorBackground,2713listInactiveSelectionForeground: foreground,2714listInactiveFocusBackground: editorBackground,2715listInactiveFocusOutline: editorBackground,2716treeIndentGuidesStroke: undefined,2717treeInactiveIndentGuidesStroke: undefined,2718}));27192720this.disposables.add(configurationService.onDidChangeConfiguration(e => {2721if (e.affectsConfiguration('workbench.list.smoothScrolling')) {2722this.updateOptions({2723smoothScrolling: configurationService.getValue<boolean>('workbench.list.smoothScrolling')2724});2725}2726}));2727}27282729protected override createModel(user: string, options: IObjectTreeOptions<SettingsTreeElement | null, void>): ITreeModel<SettingsTreeGroupChild | null, void, SettingsTreeGroupChild | null> {2730return new NonCollapsibleObjectTreeModel<SettingsTreeGroupChild>(user, options);2731}2732}27332734class CopySettingIdAction extends Action {2735static readonly ID = 'settings.copySettingId';2736static readonly LABEL = localize('copySettingIdLabel', "Copy Setting ID");27372738constructor(2739@IClipboardService private readonly clipboardService: IClipboardService2740) {2741super(CopySettingIdAction.ID, CopySettingIdAction.LABEL);2742}27432744override async run(context: SettingsTreeSettingElement): Promise<void> {2745if (context) {2746await this.clipboardService.writeText(context.setting.key);2747}27482749return Promise.resolve(undefined);2750}2751}27522753class CopySettingAsJSONAction extends Action {2754static readonly ID = 'settings.copySettingAsJSON';2755static readonly LABEL = localize('copySettingAsJSONLabel', "Copy Setting as JSON");27562757constructor(2758@IClipboardService private readonly clipboardService: IClipboardService2759) {2760super(CopySettingAsJSONAction.ID, CopySettingAsJSONAction.LABEL);2761}27622763override async run(context: SettingsTreeSettingElement): Promise<void> {2764if (context) {2765const jsonResult = `"${context.setting.key}": ${JSON.stringify(context.value, undefined, ' ')}`;2766await this.clipboardService.writeText(jsonResult);2767}27682769return Promise.resolve(undefined);2770}2771}27722773class CopySettingAsURLAction extends Action {2774static readonly ID = 'settings.copySettingAsURL';2775static readonly LABEL = localize('copySettingAsURLLabel', "Copy Setting as URL");27762777constructor(2778@IClipboardService private readonly clipboardService: IClipboardService,2779@IProductService private readonly productService: IProductService,2780) {2781super(CopySettingAsURLAction.ID, CopySettingAsURLAction.LABEL);2782}27832784override async run(context: SettingsTreeSettingElement): Promise<void> {2785if (context) {2786const settingKey = context.setting.key;2787const product = this.productService.urlProtocol;2788const uri = URI.from({ scheme: product, authority: SETTINGS_AUTHORITY, path: `/${settingKey}` }, true);2789await this.clipboardService.writeText(uri.toString());2790}27912792return Promise.resolve(undefined);2793}2794}27952796class SyncSettingAction extends Action {2797static readonly ID = 'settings.stopSyncingSetting';2798static readonly LABEL = localize('stopSyncingSetting', "Sync This Setting");27992800constructor(2801private readonly setting: ISetting,2802@IConfigurationService private readonly configService: IConfigurationService,2803) {2804super(SyncSettingAction.ID, SyncSettingAction.LABEL);2805this._register(Event.filter(configService.onDidChangeConfiguration, e => e.affectsConfiguration('settingsSync.ignoredSettings'))(() => this.update()));2806this.update();2807}28082809async update() {2810const ignoredSettings = getIgnoredSettings(getDefaultIgnoredSettings(), this.configService);2811this.checked = !ignoredSettings.includes(this.setting.key);2812}28132814override async run(): Promise<void> {2815// first remove the current setting completely from ignored settings2816let currentValue = [...this.configService.getValue<string[]>('settingsSync.ignoredSettings')];2817currentValue = currentValue.filter(v => v !== this.setting.key && v !== `-${this.setting.key}`);28182819const defaultIgnoredSettings = getDefaultIgnoredSettings();2820const isDefaultIgnored = defaultIgnoredSettings.includes(this.setting.key);2821const askedToSync = !this.checked;28222823// If asked to sync, then add only if it is ignored by default2824if (askedToSync && isDefaultIgnored) {2825currentValue.push(`-${this.setting.key}`);2826}28272828// If asked not to sync, then add only if it is not ignored by default2829if (!askedToSync && !isDefaultIgnored) {2830currentValue.push(this.setting.key);2831}28322833this.configService.updateValue('settingsSync.ignoredSettings', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER);28342835return Promise.resolve(undefined);2836}28372838}28392840class ApplySettingToAllProfilesAction extends Action {2841static readonly ID = 'settings.applyToAllProfiles';2842static readonly LABEL = localize('applyToAllProfiles', "Apply Setting to all Profiles");28432844constructor(2845private readonly setting: ISetting,2846@IWorkbenchConfigurationService private readonly configService: IWorkbenchConfigurationService,2847) {2848super(ApplySettingToAllProfilesAction.ID, ApplySettingToAllProfilesAction.LABEL);2849this._register(Event.filter(configService.onDidChangeConfiguration, e => e.affectsConfiguration(APPLY_ALL_PROFILES_SETTING))(() => this.update()));2850this.update();2851}28522853update() {2854const allProfilesSettings = this.configService.getValue<string[]>(APPLY_ALL_PROFILES_SETTING);2855this.checked = allProfilesSettings.includes(this.setting.key);2856}28572858override async run(): Promise<void> {2859// first remove the current setting completely from ignored settings2860const value = this.configService.getValue<string[]>(APPLY_ALL_PROFILES_SETTING) ?? [];28612862if (this.checked) {2863value.splice(value.indexOf(this.setting.key), 1);2864} else {2865value.push(this.setting.key);2866}28672868const newValue = distinct(value);2869if (this.checked) {2870await this.configService.updateValue(this.setting.key, this.configService.inspect(this.setting.key).application?.value, ConfigurationTarget.USER_LOCAL);2871await this.configService.updateValue(APPLY_ALL_PROFILES_SETTING, newValue.length ? newValue : undefined, ConfigurationTarget.USER_LOCAL);2872} else {2873await this.configService.updateValue(APPLY_ALL_PROFILES_SETTING, newValue.length ? newValue : undefined, ConfigurationTarget.USER_LOCAL);2874await this.configService.updateValue(this.setting.key, this.configService.inspect(this.setting.key).userLocal?.value, ConfigurationTarget.USER_LOCAL);2875}2876}28772878}287928802881