Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set settings directly from the release notes #204832

Merged
merged 3 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build/lib/i18n.resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
"name": "vs/workbench/contrib/mappedEdits",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/markdown",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/comments",
"project": "vscode-workbench"
Expand Down
5 changes: 5 additions & 0 deletions src/vs/base/common/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ export namespace Schemas {
* Scheme used for the Source Control commit input's text document
*/
export const vscodeSourceControl = 'vscode-scm';

/**
* Scheme used for special rendering of settings in the release notes
*/
export const codeSetting = 'code-setting';
}

export function matchesScheme(target: URI | string, scheme: string): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ILanguageService } from 'vs/editor/common/languages/language';
import { tokenizeToString } from 'vs/editor/common/languages/textToHtmlTokenizer';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { escape } from 'vs/base/common/strings';
import { SimpleSettingRenderer } from 'vs/workbench/contrib/markdown/browser/markdownSettingRenderer';

export const DEFAULT_MARKDOWN_STYLES = `
body {
Expand Down Expand Up @@ -195,6 +196,7 @@ export async function renderMarkdownDocument(
shouldSanitize: boolean = true,
allowUnknownProtocols: boolean = false,
token?: CancellationToken,
settingRenderer?: SimpleSettingRenderer
): Promise<string> {

const highlight = (code: string, lang: string | undefined, callback: ((error: any, code: string) => void) | undefined): any => {
Expand All @@ -220,8 +222,13 @@ export async function renderMarkdownDocument(
return '';
};

const renderer = new marked.Renderer();
if (settingRenderer) {
renderer.html = settingRenderer.getHtmlRenderer();
}

return new Promise<string>((resolve, reject) => {
marked(text, { highlight }, (err, value) => err ? reject(err) : resolve(value));
marked(text, { highlight, renderer }, (err, value) => err ? reject(err) : resolve(value));
}).then(raw => {
if (shouldSanitize) {
return sanitize(raw, allowUnknownProtocols);
Expand Down
187 changes: 187 additions & 0 deletions src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as nls from 'vs/nls';
import { ISetting, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences';
import { settingKeyToDisplayFormat } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { DefaultSettings } from 'vs/workbench/services/preferences/common/preferencesModels';

const codeSettingRegex = /^<span codesetting="([^\s"\:]+)(?::([^\s"]+))?">/;

export class SimpleSettingRenderer {
private defaultSettings: DefaultSettings;
private updatedSettings = new Map<string, any>(); // setting ID to user's original setting value
private encounteredSettings = new Map<string, ISetting>(); // setting ID to setting

constructor(
@IConfigurationService private readonly configurationService: IConfigurationService
) {
this.defaultSettings = new DefaultSettings([], ConfigurationTarget.USER);
}

getHtmlRenderer(): (html: string) => string {
return (html): string => {
const match = codeSettingRegex.exec(html);
if (match && match.length === 3) {
const settingId = match[1];
const rendered = this.render(settingId, match[2]);
if (rendered) {
html = html.replace(codeSettingRegex, rendered);
}
}
return html;
};
}

private settingsGroups: ISettingsGroup[] | undefined = undefined;
private getSetting(settingId: string): ISetting | undefined {
if (!this.settingsGroups) {
this.settingsGroups = this.defaultSettings.getSettingsGroups();
}
if (this.encounteredSettings.has(settingId)) {
return this.encounteredSettings.get(settingId);
}
for (const group of this.settingsGroups) {
for (const section of group.sections) {
for (const setting of section.settings) {
if (setting.key === settingId) {
this.encounteredSettings.set(settingId, setting);
return setting;
}
}
}
}
return undefined;
}

parseValue(settingId: string, value: string): any {
if (value === 'undefined') {
return undefined;
}
const setting = this.getSetting(settingId);
if (!setting) {
return value;
}

switch (setting.type) {
case 'boolean':
return value === 'true';
case 'number':
return parseInt(value, 10);
case 'string':
default:
return value;
}
}

private render(settingId: string, newValue: string): string | undefined {
const setting = this.getSetting(settingId);
if (!setting) {
return '';
}

return this.renderSetting(setting, newValue);
}

private viewInSettings(settingId: string, alreadySet: boolean): string {
let message: string;
if (alreadySet) {
const displayName = settingKeyToDisplayFormat(settingId);
message = nls.localize('viewInSettingsDetailed', "View \"{0}: {1}\" in Settings", displayName.category, displayName.label);
} else {
message = nls.localize('viewInSettings', "View in Settings");
}
return `<a href="${URI.parse(`command:workbench.action.openSettings?${encodeURIComponent(JSON.stringify([`@id:${settingId}`]))}`)}">${message}</a>`;
}

private renderRestorePreviousSetting(settingId: string): string {
const displayName = settingKeyToDisplayFormat(settingId);
const value = this.updatedSettings.get(settingId);
const message = nls.localize('restorePreviousValue', "Restore value of \"{0}: {1}\"", displayName.category, displayName.label);
return `<a href="${Schemas.codeSetting}://${settingId}/${value}">${message}</a>`;
}

private renderBooleanSetting(setting: ISetting, value: string): string | undefined {
const booleanValue: boolean = value === 'true' ? true : false;
const currentValue = this.configurationService.getValue<boolean>(setting.key);
if (currentValue === booleanValue || (currentValue === undefined && setting.value === booleanValue)) {
return undefined;
}

const displayName = settingKeyToDisplayFormat(setting.key);
let message: string;
if (booleanValue) {
message = nls.localize('trueMessage', "Enable \"{0}: {1}\" now", displayName.category, displayName.label);
} else {
message = nls.localize('falseMessage', "Disable \"{0}: {1}\" now", displayName.category, displayName.label);
}
return `<a href="${Schemas.codeSetting}://${setting.key}/${value}">${message}</a>`;
}

private renderStringSetting(setting: ISetting, value: string): string | undefined {
const currentValue = this.configurationService.getValue<string>(setting.key);
if (currentValue === value || (currentValue === undefined && setting.value === value)) {
return undefined;
}

const displayName = settingKeyToDisplayFormat(setting.key);
const message = nls.localize('stringValue', "Set \"{0}: {1}\" to \"{2}\" now", displayName.category, displayName.label, value);
return `<a href="${Schemas.codeSetting}://${setting.key}/${value}">${message}</a>`;
}

private renderNumberSetting(setting: ISetting, value: string): string | undefined {
const numberValue: number = parseInt(value, 10);
const currentValue = this.configurationService.getValue<number>(setting.key);
if (currentValue === numberValue || (currentValue === undefined && setting.value === numberValue)) {
return undefined;
}

const displayName = settingKeyToDisplayFormat(setting.key);
const message = nls.localize('numberValue', "Set \"{0}: {1}\" to {2} now", displayName.category, displayName.label, numberValue);
return `<a href="${Schemas.codeSetting}://${setting.key}/${value}">${message}</a>`;

}

private renderSetting(setting: ISetting, newValue: string | undefined): string | undefined {
let renderedSetting: string | undefined;

if (newValue !== undefined) {
if (this.updatedSettings.has(setting.key)) {
renderedSetting = this.renderRestorePreviousSetting(setting.key);
} else if (setting.type === 'boolean') {
renderedSetting = this.renderBooleanSetting(setting, newValue);
} else if (setting.type === 'string') {
renderedSetting = this.renderStringSetting(setting, newValue);
} else if (setting.type === 'number') {
renderedSetting = this.renderNumberSetting(setting, newValue);
}
}

if (!renderedSetting) {
return `(${this.viewInSettings(setting.key, true)})`;
}

return nls.localize({ key: 'fullRenderedSetting', comment: ['A pair of already localized links. The first argument is a link to change a setting, the second is a link to view the setting.'] },
"({0} | {1})", renderedSetting, this.viewInSettings(setting.key, false),);
}

async updateSettingValue(uri: URI) {
if (uri.scheme !== Schemas.codeSetting) {
return;
}
const settingId = uri.authority;
const newSettingValue = this.parseValue(uri.authority, uri.path.substring(1));
const oldSettingValue = this.configurationService.inspect(settingId).userValue;
if (newSettingValue === this.updatedSettings.get(settingId)) {
this.updatedSettings.delete(settingId);
} else {
this.updatedSettings.set(settingId, oldSettingValue);
}
await this.configurationService.updateValue(settingId, newSettingValue, ConfigurationTarget.USER);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { Registry } from 'vs/platform/registry/common/platform';
import { SimpleSettingRenderer } from 'vs/workbench/contrib/markdown/browser/markdownSettingRenderer';

const configuration: IConfigurationNode = {
'id': 'examples',
'title': 'Examples',
'type': 'object',
'properties': {
'example.booleanSetting': {
'type': 'boolean',
'default': false,
'scope': ConfigurationScope.APPLICATION
},
'example.booleanSetting2': {
'type': 'boolean',
'default': true,
'scope': ConfigurationScope.APPLICATION
},
'example.stringSetting': {
'type': 'string',
'default': 'one',
'scope': ConfigurationScope.APPLICATION
},
'example.numberSetting': {
'type': 'number',
'default': 3,
'scope': ConfigurationScope.APPLICATION
}
}
};

class MarkdownConfigurationService extends TestConfigurationService {
override async updateValue(key: string, value: any): Promise<void> {
const [section, setting] = key.split('.');
return this.setUserConfiguration(section, { [setting]: value });
}
}

suite('Markdown Setting Renderer Test', () => {
ensureNoDisposablesAreLeakedInTestSuite();

let configurationService: TestConfigurationService;
let settingRenderer: SimpleSettingRenderer;

suiteSetup(() => {
configurationService = new MarkdownConfigurationService();
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration(configuration);
settingRenderer = new SimpleSettingRenderer(configurationService);
});

suiteTeardown(() => {
Registry.as<IConfigurationRegistry>(Extensions.Configuration).deregisterConfigurations([configuration]);
});

test('render boolean setting', () => {
const htmlRenderer = settingRenderer.getHtmlRenderer();
const htmlNoValue = '<span codesetting="example.booleanSetting">';
const renderedHtmlNoValue = htmlRenderer(htmlNoValue);
assert.equal(renderedHtmlNoValue,
`(<a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.booleanSetting%22%5D">View "Example: Boolean Setting" in Settings</a>)`);

const htmlWithValue = '<span codesetting="example.booleanSetting:true">';
const renderedHtmlWithValue = htmlRenderer(htmlWithValue);
assert.equal(renderedHtmlWithValue,
`(<a href="code-setting://example.booleanSetting/true">Enable "Example: Boolean Setting" now</a> | <a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.booleanSetting%22%5D">View in Settings</a>)`);

const htmlWithValueSetToFalse = '<span codesetting="example.booleanSetting2:false">';
const renderedHtmlWithValueSetToFalse = htmlRenderer(htmlWithValueSetToFalse);
assert.equal(renderedHtmlWithValueSetToFalse,
`(<a href="code-setting://example.booleanSetting2/false">Disable "Example: Boolean Setting2" now</a> | <a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.booleanSetting2%22%5D">View in Settings</a>)`);

const htmlSameValue = '<span codesetting="example.booleanSetting:false">';
const renderedHtmlSameValue = htmlRenderer(htmlSameValue);
assert.equal(renderedHtmlSameValue,
`(<a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.booleanSetting%22%5D">View "Example: Boolean Setting" in Settings</a>)`);
});

test('render string setting', () => {
const htmlRenderer = settingRenderer.getHtmlRenderer();
const htmlNoValue = '<span codesetting="example.stringSetting">';
const renderedHtmlNoValue = htmlRenderer(htmlNoValue);
assert.equal(renderedHtmlNoValue,
`(<a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.stringSetting%22%5D">View "Example: String Setting" in Settings</a>)`);

const htmlWithValue = '<span codesetting="example.stringSetting:two">';
const renderedHtmlWithValue = htmlRenderer(htmlWithValue);
assert.equal(renderedHtmlWithValue,
`(<a href="code-setting://example.stringSetting/two">Set "Example: String Setting" to "two" now</a> | <a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.stringSetting%22%5D">View in Settings</a>)`);

const htmlSameValue = '<span codesetting="example.stringSetting:one">';
const renderedHtmlSameValue = htmlRenderer(htmlSameValue);
assert.equal(renderedHtmlSameValue,
`(<a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.stringSetting%22%5D">View "Example: String Setting" in Settings</a>)`);
});

test('render number setting', () => {
const htmlRenderer = settingRenderer.getHtmlRenderer();
const htmlNoValue = '<span codesetting="example.numberSetting">';
const renderedHtmlNoValue = htmlRenderer(htmlNoValue);
assert.equal(renderedHtmlNoValue,
`(<a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.numberSetting%22%5D">View "Example: Number Setting" in Settings</a>)`);

const htmlWithValue = '<span codesetting="example.numberSetting:2">';
const renderedHtmlWithValue = htmlRenderer(htmlWithValue);
assert.equal(renderedHtmlWithValue,
`(<a href="code-setting://example.numberSetting/2">Set "Example: Number Setting" to 2 now</a> | <a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.numberSetting%22%5D">View in Settings</a>)`);

const htmlSameValue = '<span codesetting="example.numberSetting:3">';
const renderedHtmlSameValue = htmlRenderer(htmlSameValue);
assert.equal(renderedHtmlSameValue,
`(<a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.numberSetting%22%5D">View "Example: Number Setting" in Settings</a>)`);
});

test('updating and restoring the setting through the renderer changes what is rendered', async () => {
await configurationService.setUserConfiguration('example', { stringSetting: 'two' });
const htmlRenderer = settingRenderer.getHtmlRenderer();
const htmlWithValue = '<span codesetting="example.stringSetting:three">';
const renderedHtmlWithValue = htmlRenderer(htmlWithValue);
assert.equal(renderedHtmlWithValue,
`(<a href="code-setting://example.stringSetting/three">Set "Example: String Setting" to "three" now</a> | <a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.stringSetting%22%5D">View in Settings</a>)`);
assert.equal(configurationService.getValue('example.stringSetting'), 'two');

// Update the value
await settingRenderer.updateSettingValue(URI.parse(`${Schemas.codeSetting}://example.stringSetting/three`));
assert.equal(configurationService.getValue('example.stringSetting'), 'three');
const renderedHtmlWithValueAfterUpdate = htmlRenderer(htmlWithValue);
assert.equal(renderedHtmlWithValueAfterUpdate,
`(<a href="code-setting://example.stringSetting/two">Restore value of "Example: String Setting"</a> | <a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.stringSetting%22%5D">View in Settings</a>)`);

// Restore the value
await settingRenderer.updateSettingValue(URI.parse(`${Schemas.codeSetting}://example.stringSetting/two`));
assert.equal(configurationService.getValue('example.stringSetting'), 'two');
const renderedHtmlWithValueAfterRestore = htmlRenderer(htmlWithValue);
assert.equal(renderedHtmlWithValueAfterRestore,
`(<a href="code-setting://example.stringSetting/three">Set "Example: String Setting" to "three" now</a> | <a href="command:workbench.action.openSettings?%5B%22%40id%3Aexample.stringSetting%22%5D">View in Settings</a>)`);
});
});
Loading
Loading