Path: blob/main/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts
4780 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 { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';6import { IAction } from '../../../../base/common/actions.js';7import type { Tokens } from '../../../../base/common/marked/marked.js';8import { Schemas } from '../../../../base/common/network.js';9import { URI } from '../../../../base/common/uri.js';10import * as nls from '../../../../nls.js';11import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';12import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';13import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';14import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';15import { IPreferencesService, ISetting } from '../../../services/preferences/common/preferences.js';16import { settingKeyToDisplayFormat } from '../../preferences/browser/settingsTreeModels.js';1718export class SimpleSettingRenderer {19private readonly codeSettingAnchorRegex: RegExp;20private readonly codeSettingSimpleRegex: RegExp;2122private _updatedSettings = new Map<string, unknown>(); // setting ID to user's original setting value23private _encounteredSettings = new Map<string, ISetting>(); // setting ID to setting24private _featuredSettings = new Map<string, unknown>(); // setting ID to feature value2526constructor(27@IConfigurationService private readonly _configurationService: IConfigurationService,28@IContextMenuService private readonly _contextMenuService: IContextMenuService,29@IPreferencesService private readonly _preferencesService: IPreferencesService,30@ITelemetryService private readonly _telemetryService: ITelemetryService,31@IClipboardService private readonly _clipboardService: IClipboardService,32) {33this.codeSettingAnchorRegex = new RegExp(`^<a (href)=".*code.*://settings/([^\\s"]+)"(?:\\s*codesetting="([^"]+)")?>`);34this.codeSettingSimpleRegex = new RegExp(`^setting\\(([^\\s:)]+)(?::([^)]+))?\\)$`);35}3637get featuredSettingStates(): Map<string, boolean> {38const result = new Map<string, boolean>();39for (const [settingId, value] of this._featuredSettings) {40result.set(settingId, this._configurationService.getValue(settingId) === value);41}42return result;43}4445private replaceAnchor(raw: string): string | undefined {46const match = this.codeSettingAnchorRegex.exec(raw);47if (match && match.length === 4) {48const settingId = match[2];49const rendered = this.render(settingId, match[3]);50if (rendered) {51return raw.replace(this.codeSettingAnchorRegex, rendered);52}53}54return undefined;55}5657private replaceSimple(raw: string): string | undefined {58const match = this.codeSettingSimpleRegex.exec(raw);59if (match && match.length === 3) {60const settingId = match[1];61const rendered = this.render(settingId, match[2]);62if (rendered) {63return raw.replace(this.codeSettingSimpleRegex, rendered);64}65}66return undefined;67}6869getHtmlRenderer(): (token: Tokens.HTML | Tokens.Tag) => string {70return ({ raw }: Tokens.HTML | Tokens.Tag): string => {71const replacedAnchor = this.replaceAnchor(raw);72if (replacedAnchor) {73raw = replacedAnchor;74}75return raw;76};77}7879getCodeSpanRenderer(): (token: Tokens.Codespan) => string {80return ({ text }: Tokens.Codespan): string => {81const replacedSimple = this.replaceSimple(text);82if (replacedSimple) {83return replacedSimple;84}85return `<code>${text}</code>`;86};87}8889settingToUriString(settingId: string, value?: unknown): string {90return `${Schemas.codeSetting}://${settingId}${value ? `/${value}` : ''}`;91}9293private getSetting(settingId: string): ISetting | undefined {94if (this._encounteredSettings.has(settingId)) {95return this._encounteredSettings.get(settingId);96}97return this._preferencesService.getSetting(settingId);98}99100parseValue(settingId: string, value: string) {101if (value === 'undefined' || value === '') {102return undefined;103}104const setting = this.getSetting(settingId);105if (!setting) {106return value;107}108109switch (setting.type) {110case 'boolean':111return value === 'true';112case 'number':113return parseInt(value, 10);114case 'string':115default:116return value;117}118}119120private render(settingId: string, newValue: string): string | undefined {121const setting = this.getSetting(settingId);122if (!setting) {123return `<code>${settingId}</code>`;124}125126return this.renderSetting(setting, newValue);127}128129private viewInSettingsMessage(settingId: string, alreadyDisplayed: boolean) {130if (alreadyDisplayed) {131return nls.localize('viewInSettings', "View in Settings");132} else {133const displayName = settingKeyToDisplayFormat(settingId);134return nls.localize('viewInSettingsDetailed', "View \"{0}: {1}\" in Settings", displayName.category, displayName.label);135}136}137138private restorePreviousSettingMessage(settingId: string): string {139const displayName = settingKeyToDisplayFormat(settingId);140return nls.localize('restorePreviousValue', "Restore value of \"{0}: {1}\"", displayName.category, displayName.label);141}142143private isAlreadySet(setting: ISetting, value: string | number | boolean): boolean {144const currentValue = this._configurationService.getValue<boolean>(setting.key);145return (currentValue === value || (currentValue === undefined && setting.value === value));146}147148private booleanSettingMessage(setting: ISetting, booleanValue: boolean): string | undefined {149const displayName = settingKeyToDisplayFormat(setting.key);150if (this.isAlreadySet(setting, booleanValue)) {151if (booleanValue) {152return nls.localize('alreadysetBoolTrue', "\"{0}: {1}\" is already enabled", displayName.category, displayName.label);153} else {154return nls.localize('alreadysetBoolFalse', "\"{0}: {1}\" is already disabled", displayName.category, displayName.label);155}156}157158if (booleanValue) {159return nls.localize('trueMessage', "Enable \"{0}: {1}\"", displayName.category, displayName.label);160} else {161return nls.localize('falseMessage', "Disable \"{0}: {1}\"", displayName.category, displayName.label);162}163}164165private stringSettingMessage(setting: ISetting, stringValue: string): string | undefined {166const displayName = settingKeyToDisplayFormat(setting.key);167if (this.isAlreadySet(setting, stringValue)) {168return nls.localize('alreadysetString', "\"{0}: {1}\" is already set to \"{2}\"", displayName.category, displayName.label, stringValue);169}170171return nls.localize('stringValue', "Set \"{0}: {1}\" to \"{2}\"", displayName.category, displayName.label, stringValue);172}173174private numberSettingMessage(setting: ISetting, numberValue: number): string | undefined {175const displayName = settingKeyToDisplayFormat(setting.key);176if (this.isAlreadySet(setting, numberValue)) {177return nls.localize('alreadysetNum', "\"{0}: {1}\" is already set to {2}", displayName.category, displayName.label, numberValue);178}179180return nls.localize('numberValue', "Set \"{0}: {1}\" to {2}", displayName.category, displayName.label, numberValue);181182}183184private renderSetting(setting: ISetting, newValue: string | undefined): string | undefined {185const href = this.settingToUriString(setting.key, newValue);186const title = nls.localize('changeSettingTitle', "View or change setting");187return `<code tabindex="0"><a href="${href}" class="codesetting" title="${title}" aria-role="button"><svg width="14" height="14" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M9.1 4.4L8.6 2H7.4l-.5 2.4-.7.3-2-1.3-.9.8 1.3 2-.2.7-2.4.5v1.2l2.4.5.3.8-1.3 2 .8.8 2-1.3.8.3.4 2.3h1.2l.5-2.4.8-.3 2 1.3.8-.8-1.3-2 .3-.8 2.3-.4V7.4l-2.4-.5-.3-.8 1.3-2-.8-.8-2 1.3-.7-.2zM9.4 1l.5 2.4L12 2.1l2 2-1.4 2.1 2.4.4v2.8l-2.4.5L14 12l-2 2-2.1-1.4-.5 2.4H6.6l-.5-2.4L4 13.9l-2-2 1.4-2.1L1 9.4V6.6l2.4-.5L2.1 4l2-2 2.1 1.4.4-2.4h2.8zm.6 7c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zM8 9c.6 0 1-.4 1-1s-.4-1-1-1-1 .4-1 1 .4 1 1 1z"/></svg>188<span class="separator"></span>189<span class="setting-name">${setting.key}</span>190</a></code>`;191}192193private getSettingMessage(setting: ISetting, newValue: boolean | string | number): string | undefined {194if (setting.type === 'boolean') {195return this.booleanSettingMessage(setting, newValue as boolean);196} else if (setting.type === 'string') {197return this.stringSettingMessage(setting, newValue as string);198} else if (setting.type === 'number') {199return this.numberSettingMessage(setting, newValue as number);200}201return undefined;202}203204async restoreSetting(settingId: string): Promise<void> {205const userOriginalSettingValue = this._updatedSettings.get(settingId);206this._updatedSettings.delete(settingId);207return this._configurationService.updateValue(settingId, userOriginalSettingValue, ConfigurationTarget.USER);208}209210async setSetting(settingId: string, currentSettingValue: unknown, newSettingValue: unknown): Promise<void> {211this._updatedSettings.set(settingId, currentSettingValue);212return this._configurationService.updateValue(settingId, newSettingValue, ConfigurationTarget.USER);213}214215getActions(uri: URI) {216if (uri.scheme !== Schemas.codeSetting) {217return;218}219220const actions: IAction[] = [];221222const settingId = uri.authority;223const newSettingValue = this.parseValue(uri.authority, uri.path.substring(1));224const currentSettingValue = this._configurationService.inspect(settingId).userValue;225226if ((newSettingValue !== undefined) && newSettingValue === currentSettingValue && this._updatedSettings.has(settingId)) {227const restoreMessage = this.restorePreviousSettingMessage(settingId);228actions.push({229class: undefined,230id: 'restoreSetting',231enabled: true,232tooltip: restoreMessage,233label: restoreMessage,234run: () => {235return this.restoreSetting(settingId);236}237});238} else if (newSettingValue !== undefined) {239const setting = this.getSetting(settingId);240const trySettingMessage = setting ? this.getSettingMessage(setting, newSettingValue) : undefined;241242if (setting && trySettingMessage) {243actions.push({244class: undefined,245id: 'trySetting',246enabled: !this.isAlreadySet(setting, newSettingValue),247tooltip: trySettingMessage,248label: trySettingMessage,249run: () => {250this.setSetting(settingId, currentSettingValue, newSettingValue);251}252});253}254}255256const viewInSettingsMessage = this.viewInSettingsMessage(settingId, actions.length > 0);257actions.push({258class: undefined,259enabled: true,260id: 'viewInSettings',261tooltip: viewInSettingsMessage,262label: viewInSettingsMessage,263run: () => {264return this._preferencesService.openApplicationSettings({ query: `@id:${settingId}` });265}266});267268actions.push({269class: undefined,270enabled: true,271id: 'copySettingId',272tooltip: nls.localize('copySettingId', "Copy Setting ID"),273label: nls.localize('copySettingId', "Copy Setting ID"),274run: () => {275this._clipboardService.writeText(settingId);276}277});278279return actions;280}281282private showContextMenu(uri: URI, x: number, y: number) {283const actions = this.getActions(uri);284if (!actions) {285return;286}287288this._contextMenuService.showContextMenu({289getAnchor: () => ({ x, y }),290getActions: () => actions,291getActionViewItem: (action) => {292return new ActionViewItem(action, action, { label: true });293},294});295}296297async updateSetting(uri: URI, x: number, y: number) {298if (uri.scheme === Schemas.codeSetting) {299type ReleaseNotesSettingUsedClassification = {300owner: 'alexr00';301comment: 'Used to understand if the action to update settings from the release notes is used.';302settingId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the setting that was clicked on in the release notes' };303};304type ReleaseNotesSettingUsed = {305settingId: string;306};307this._telemetryService.publicLog2<ReleaseNotesSettingUsed, ReleaseNotesSettingUsedClassification>('releaseNotesSettingAction', {308settingId: uri.authority309});310return this.showContextMenu(uri, x, y);311}312}313}314315316