Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
7
import { IAction } from '../../../../base/common/actions.js';
8
import type { Tokens } from '../../../../base/common/marked/marked.js';
9
import { Schemas } from '../../../../base/common/network.js';
10
import { URI } from '../../../../base/common/uri.js';
11
import * as nls from '../../../../nls.js';
12
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
13
import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
14
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
15
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
16
import { IPreferencesService, ISetting } from '../../../services/preferences/common/preferences.js';
17
import { settingKeyToDisplayFormat } from '../../preferences/browser/settingsTreeModels.js';
18
19
export class SimpleSettingRenderer {
20
private readonly codeSettingAnchorRegex: RegExp;
21
private readonly codeSettingSimpleRegex: RegExp;
22
23
private _updatedSettings = new Map<string, unknown>(); // setting ID to user's original setting value
24
private _encounteredSettings = new Map<string, ISetting>(); // setting ID to setting
25
private _featuredSettings = new Map<string, unknown>(); // setting ID to feature value
26
27
constructor(
28
@IConfigurationService private readonly _configurationService: IConfigurationService,
29
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
30
@IPreferencesService private readonly _preferencesService: IPreferencesService,
31
@ITelemetryService private readonly _telemetryService: ITelemetryService,
32
@IClipboardService private readonly _clipboardService: IClipboardService,
33
) {
34
this.codeSettingAnchorRegex = new RegExp(`^<a (href)=".*code.*://settings/([^\\s"]+)"(?:\\s*codesetting="([^"]+)")?>`);
35
this.codeSettingSimpleRegex = new RegExp(`^setting\\(([^\\s:)]+)(?::([^)]+))?\\)$`);
36
}
37
38
get featuredSettingStates(): Map<string, boolean> {
39
const result = new Map<string, boolean>();
40
for (const [settingId, value] of this._featuredSettings) {
41
result.set(settingId, this._configurationService.getValue(settingId) === value);
42
}
43
return result;
44
}
45
46
private replaceAnchor(raw: string): string | undefined {
47
const match = this.codeSettingAnchorRegex.exec(raw);
48
if (match && match.length === 4) {
49
const settingId = match[2];
50
const rendered = this.render(settingId, match[3]);
51
if (rendered) {
52
return raw.replace(this.codeSettingAnchorRegex, rendered);
53
}
54
}
55
return undefined;
56
}
57
58
private replaceSimple(raw: string): string | undefined {
59
const match = this.codeSettingSimpleRegex.exec(raw);
60
if (match && match.length === 3) {
61
const settingId = match[1];
62
const rendered = this.render(settingId, match[2]);
63
if (rendered) {
64
return raw.replace(this.codeSettingSimpleRegex, rendered);
65
}
66
}
67
return undefined;
68
}
69
70
getHtmlRenderer(): (token: Tokens.HTML | Tokens.Tag) => string {
71
return ({ raw }: Tokens.HTML | Tokens.Tag): string => {
72
const replacedAnchor = this.replaceAnchor(raw);
73
if (replacedAnchor) {
74
raw = replacedAnchor;
75
}
76
return raw;
77
};
78
}
79
80
getCodeSpanRenderer(): (token: Tokens.Codespan) => string {
81
return ({ text }: Tokens.Codespan): string => {
82
const replacedSimple = this.replaceSimple(text);
83
if (replacedSimple) {
84
return replacedSimple;
85
}
86
return `<code>${text}</code>`;
87
};
88
}
89
90
settingToUriString(settingId: string, value?: unknown): string {
91
return `${Schemas.codeSetting}://${settingId}${value ? `/${value}` : ''}`;
92
}
93
94
private getSetting(settingId: string): ISetting | undefined {
95
if (this._encounteredSettings.has(settingId)) {
96
return this._encounteredSettings.get(settingId);
97
}
98
return this._preferencesService.getSetting(settingId);
99
}
100
101
parseValue(settingId: string, value: string) {
102
if (value === 'undefined' || value === '') {
103
return undefined;
104
}
105
const setting = this.getSetting(settingId);
106
if (!setting) {
107
return value;
108
}
109
110
switch (setting.type) {
111
case 'boolean':
112
return value === 'true';
113
case 'number':
114
return parseInt(value, 10);
115
case 'string':
116
default:
117
return value;
118
}
119
}
120
121
private render(settingId: string, newValue: string): string | undefined {
122
const setting = this.getSetting(settingId);
123
if (!setting) {
124
return `<code>${settingId}</code>`;
125
}
126
127
return this.renderSetting(setting, newValue);
128
}
129
130
private viewInSettingsMessage(settingId: string, alreadyDisplayed: boolean) {
131
if (alreadyDisplayed) {
132
return nls.localize('viewInSettings', "View in Settings");
133
} else {
134
const displayName = settingKeyToDisplayFormat(settingId);
135
return nls.localize('viewInSettingsDetailed', "View \"{0}: {1}\" in Settings", displayName.category, displayName.label);
136
}
137
}
138
139
private restorePreviousSettingMessage(settingId: string): string {
140
const displayName = settingKeyToDisplayFormat(settingId);
141
return nls.localize('restorePreviousValue', "Restore value of \"{0}: {1}\"", displayName.category, displayName.label);
142
}
143
144
private isAlreadySet(setting: ISetting, value: string | number | boolean): boolean {
145
const currentValue = this._configurationService.getValue<boolean>(setting.key);
146
return (currentValue === value || (currentValue === undefined && setting.value === value));
147
}
148
149
private booleanSettingMessage(setting: ISetting, booleanValue: boolean): string | undefined {
150
const displayName = settingKeyToDisplayFormat(setting.key);
151
if (this.isAlreadySet(setting, booleanValue)) {
152
if (booleanValue) {
153
return nls.localize('alreadysetBoolTrue', "\"{0}: {1}\" is already enabled", displayName.category, displayName.label);
154
} else {
155
return nls.localize('alreadysetBoolFalse', "\"{0}: {1}\" is already disabled", displayName.category, displayName.label);
156
}
157
}
158
159
if (booleanValue) {
160
return nls.localize('trueMessage', "Enable \"{0}: {1}\"", displayName.category, displayName.label);
161
} else {
162
return nls.localize('falseMessage', "Disable \"{0}: {1}\"", displayName.category, displayName.label);
163
}
164
}
165
166
private stringSettingMessage(setting: ISetting, stringValue: string): string | undefined {
167
const displayName = settingKeyToDisplayFormat(setting.key);
168
if (this.isAlreadySet(setting, stringValue)) {
169
return nls.localize('alreadysetString', "\"{0}: {1}\" is already set to \"{2}\"", displayName.category, displayName.label, stringValue);
170
}
171
172
return nls.localize('stringValue', "Set \"{0}: {1}\" to \"{2}\"", displayName.category, displayName.label, stringValue);
173
}
174
175
private numberSettingMessage(setting: ISetting, numberValue: number): string | undefined {
176
const displayName = settingKeyToDisplayFormat(setting.key);
177
if (this.isAlreadySet(setting, numberValue)) {
178
return nls.localize('alreadysetNum', "\"{0}: {1}\" is already set to {2}", displayName.category, displayName.label, numberValue);
179
}
180
181
return nls.localize('numberValue', "Set \"{0}: {1}\" to {2}", displayName.category, displayName.label, numberValue);
182
183
}
184
185
private renderSetting(setting: ISetting, newValue: string | undefined): string | undefined {
186
const href = this.settingToUriString(setting.key, newValue);
187
const title = nls.localize('changeSettingTitle', "View or change setting");
188
return `<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>
189
<span class="separator"></span>
190
<span class="setting-name">${setting.key}</span>
191
</a></code>`;
192
}
193
194
private getSettingMessage(setting: ISetting, newValue: boolean | string | number): string | undefined {
195
if (setting.type === 'boolean') {
196
return this.booleanSettingMessage(setting, newValue as boolean);
197
} else if (setting.type === 'string') {
198
return this.stringSettingMessage(setting, newValue as string);
199
} else if (setting.type === 'number') {
200
return this.numberSettingMessage(setting, newValue as number);
201
}
202
return undefined;
203
}
204
205
async restoreSetting(settingId: string): Promise<void> {
206
const userOriginalSettingValue = this._updatedSettings.get(settingId);
207
this._updatedSettings.delete(settingId);
208
return this._configurationService.updateValue(settingId, userOriginalSettingValue, ConfigurationTarget.USER);
209
}
210
211
async setSetting(settingId: string, currentSettingValue: unknown, newSettingValue: unknown): Promise<void> {
212
this._updatedSettings.set(settingId, currentSettingValue);
213
return this._configurationService.updateValue(settingId, newSettingValue, ConfigurationTarget.USER);
214
}
215
216
getActions(uri: URI) {
217
if (uri.scheme !== Schemas.codeSetting) {
218
return;
219
}
220
221
const actions: IAction[] = [];
222
223
const settingId = uri.authority;
224
const newSettingValue = this.parseValue(uri.authority, uri.path.substring(1));
225
const currentSettingValue = this._configurationService.inspect(settingId).userValue;
226
227
if ((newSettingValue !== undefined) && newSettingValue === currentSettingValue && this._updatedSettings.has(settingId)) {
228
const restoreMessage = this.restorePreviousSettingMessage(settingId);
229
actions.push({
230
class: undefined,
231
id: 'restoreSetting',
232
enabled: true,
233
tooltip: restoreMessage,
234
label: restoreMessage,
235
run: () => {
236
return this.restoreSetting(settingId);
237
}
238
});
239
} else if (newSettingValue !== undefined) {
240
const setting = this.getSetting(settingId);
241
const trySettingMessage = setting ? this.getSettingMessage(setting, newSettingValue) : undefined;
242
243
if (setting && trySettingMessage) {
244
actions.push({
245
class: undefined,
246
id: 'trySetting',
247
enabled: !this.isAlreadySet(setting, newSettingValue),
248
tooltip: trySettingMessage,
249
label: trySettingMessage,
250
run: () => {
251
this.setSetting(settingId, currentSettingValue, newSettingValue);
252
}
253
});
254
}
255
}
256
257
const viewInSettingsMessage = this.viewInSettingsMessage(settingId, actions.length > 0);
258
actions.push({
259
class: undefined,
260
enabled: true,
261
id: 'viewInSettings',
262
tooltip: viewInSettingsMessage,
263
label: viewInSettingsMessage,
264
run: () => {
265
return this._preferencesService.openApplicationSettings({ query: `@id:${settingId}` });
266
}
267
});
268
269
actions.push({
270
class: undefined,
271
enabled: true,
272
id: 'copySettingId',
273
tooltip: nls.localize('copySettingId', "Copy Setting ID"),
274
label: nls.localize('copySettingId', "Copy Setting ID"),
275
run: () => {
276
this._clipboardService.writeText(settingId);
277
}
278
});
279
280
return actions;
281
}
282
283
private showContextMenu(uri: URI, x: number, y: number) {
284
const actions = this.getActions(uri);
285
if (!actions) {
286
return;
287
}
288
289
this._contextMenuService.showContextMenu({
290
getAnchor: () => ({ x, y }),
291
getActions: () => actions,
292
getActionViewItem: (action) => {
293
return new ActionViewItem(action, action, { label: true });
294
},
295
});
296
}
297
298
async updateSetting(uri: URI, x: number, y: number) {
299
if (uri.scheme === Schemas.codeSetting) {
300
type ReleaseNotesSettingUsedClassification = {
301
owner: 'alexr00';
302
comment: 'Used to understand if the action to update settings from the release notes is used.';
303
settingId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the setting that was clicked on in the release notes' };
304
};
305
type ReleaseNotesSettingUsed = {
306
settingId: string;
307
};
308
this._telemetryService.publicLog2<ReleaseNotesSettingUsed, ReleaseNotesSettingUsedClassification>('releaseNotesSettingAction', {
309
settingId: uri.authority
310
});
311
return this.showContextMenu(uri, x, y);
312
}
313
}
314
}
315
316