Skip to content

Commit

Permalink
Add url handling for settings and adopt in release notes (microsoft#2…
Browse files Browse the repository at this point in the history
…19702)

* Add url handling for settings and adopt in release notes
Fixes microsoft#212079

* Update regex to use product service, fix test

* Incorporate feedback

* Check value of setting before passing to settings editor

* Fix test
  • Loading branch information
alexr00 authored and aaronchucarroll committed Jul 10, 2024
1 parent ed55b99 commit 51ce580
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@
*--------------------------------------------------------------------------------------------*/

import * as nls from 'vs/nls';
import { IPreferencesService, ISetting, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences';
import { IPreferencesService, ISetting } 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';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { IAction } from 'vs/base/common/actions';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';

const codeSettingRegex = /^<code (codesetting)="([^\s"\:]+)(?::([^"]+))?">/;
import { Schemas } from 'vs/base/common/network';

export class SimpleSettingRenderer {
private _defaultSettings: DefaultSettings;
private readonly codeSettingRegex: RegExp;

private _updatedSettings = new Map<string, any>(); // setting ID to user's original setting value
private _encounteredSettings = new Map<string, ISetting>(); // setting ID to setting
private _featuredSettings = new Map<string, any>(); // setting ID to feature value
Expand All @@ -29,9 +27,9 @@ export class SimpleSettingRenderer {
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
@IPreferencesService private readonly _preferencesService: IPreferencesService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IClipboardService private readonly _clipboardService: IClipboardService
@IClipboardService private readonly _clipboardService: IClipboardService,
) {
this._defaultSettings = new DefaultSettings([], ConfigurationTarget.USER);
this.codeSettingRegex = new RegExp(`^<a (href)=".*code.*://settings/([^\\s"]+)"(?:\\s*codesetting="([^"]+)")?>`);
}

get featuredSettingStates(): Map<string, boolean> {
Expand All @@ -44,12 +42,12 @@ export class SimpleSettingRenderer {

getHtmlRenderer(): (html: string) => string {
return (html): string => {
const match = codeSettingRegex.exec(html);
const match = this.codeSettingRegex.exec(html);
if (match && match.length === 4) {
const settingId = match[2];
const rendered = this.render(settingId, match[3]);
if (rendered) {
html = html.replace(codeSettingRegex, rendered);
html = html.replace(this.codeSettingRegex, rendered);
}
}
return html;
Expand All @@ -60,25 +58,11 @@ export class SimpleSettingRenderer {
return `${Schemas.codeSetting}://${settingId}${value ? `/${value}` : ''}`;
}

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;
return this._preferencesService.getSetting(settingId);
}

parseValue(settingId: string, value: string): any {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,15 @@ suite('Markdown Setting Renderer Test', () => {

suiteSetup(() => {
configurationService = new MarkdownConfigurationService();
preferencesService = <IPreferencesService>{};
preferencesService = <IPreferencesService>{
getSetting: (setting) => {
let type = 'boolean';
if (setting.includes('string')) {
type = 'string';
}
return { type, key: setting };
}
};
contextMenuService = <IContextMenuService>{};
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration(configuration);
settingRenderer = new SimpleSettingRenderer(configurationService, contextMenuService, preferencesService, { publicLog2: () => { } } as any, { writeText: async () => { } } as any);
Expand All @@ -70,10 +78,10 @@ suite('Markdown Setting Renderer Test', () => {

test('render code setting button with value', () => {
const htmlRenderer = settingRenderer.getHtmlRenderer();
const htmlNoValue = '<code codesetting="example.booleanSetting">';
const htmlNoValue = '<a href="code-oss://settings/example.booleanSetting" codesetting="true">';
const renderedHtmlNoValue = htmlRenderer(htmlNoValue);
assert.strictEqual(renderedHtmlNoValue,
`<code tabindex="0"><a href="code-setting://example.booleanSetting" class="codesetting" title="View or change setting" 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>
`<code tabindex="0"><a href="code-setting://example.booleanSetting/true" class="codesetting" title="View or change setting" 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>
<span class="separator"></span>
<span class="setting-name">example.booleanSetting</span>
</a></code><code>`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export class ShowCurrentReleaseNotesFromCurrentFileAction extends Action2 {
},
category: localize2('developerCategory', "Developer"),
f1: true,
precondition: RELEASE_NOTES_URL
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { IJSONEditingService } from 'vs/workbench/services/configuration/common/
import { GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService';
import { KeybindingsEditorInput } from 'vs/workbench/services/preferences/browser/keybindingsEditorInput';
import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, IKeybindingsEditorOptions, IKeybindingsEditorPane, IOpenSettingsOptions, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, USE_SPLIT_JSON_SETTING, validateSettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences';
import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, IKeybindingsEditorOptions, IKeybindingsEditorPane, IOpenSettingsOptions, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, ISettingsGroup, SETTINGS_AUTHORITY, USE_SPLIT_JSON_SETTING, validateSettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences';
import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
import { defaultKeybindingsContents, DefaultKeybindingsEditorModel, DefaultRawSettingsEditorModel, DefaultSettings, DefaultSettingsEditorModel, Settings2EditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
Expand All @@ -45,6 +45,7 @@ import { isObject } from 'vs/base/common/types';
import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
import { IURLService } from 'vs/platform/url/common/url';

const emptyEditableSettingsContent = '{\n}';

Expand All @@ -58,6 +59,9 @@ export class PreferencesService extends Disposable implements IPreferencesServic
private _defaultWorkspaceSettingsContentModel: DefaultSettings | undefined;
private _defaultFolderSettingsContentModel: DefaultSettings | undefined;

private _settingsGroups: ISettingsGroup[] | undefined = undefined;
private _defaultSettings: DefaultSettings | undefined = undefined;

constructor(
@IEditorService private readonly editorService: IEditorService,
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
Expand All @@ -75,7 +79,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic
@ILanguageService private readonly languageService: ILanguageService,
@ILabelService private readonly labelService: ILabelService,
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
@ITextEditorService private readonly textEditorService: ITextEditorService
@ITextEditorService private readonly textEditorService: ITextEditorService,
@IURLService urlService: IURLService
) {
super();
// The default keybindings.json updates based on keyboard layouts, so here we make sure
Expand All @@ -88,6 +93,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic
}
modelService.updateModel(model, defaultKeybindingsContents(keybindingService));
}));

this._register(urlService.registerHandler(this));
}

readonly defaultKeybindingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/keybindings.json' });
Expand Down Expand Up @@ -593,6 +600,53 @@ export class PreferencesService extends Disposable implements IPreferencesServic
return position;
}

private get defaultSettings(): DefaultSettings {
if (!this._defaultSettings) {
this._defaultSettings = new DefaultSettings([], ConfigurationTarget.USER);
}
return this._defaultSettings;
}

getSetting(settingId: string): ISetting | undefined {
if (!this._settingsGroups) {
this._settingsGroups = this.defaultSettings.getSettingsGroups();
}

for (const group of this._settingsGroups) {
for (const section of group.sections) {
for (const setting of section.settings) {
if (setting.key === settingId) {
return setting;
}
}
}
}
return undefined;
}

/**
* Should be of the format:
* code://settings/settingName
* Examples:
* code://settings/files.autoSave
*
*/
async handleURL(uri: URI): Promise<boolean> {
if (uri.authority !== SETTINGS_AUTHORITY) {
return false;
}

const openSettingsOptions: IOpenSettingsOptions = {};
const settingInfo = uri.path.split('/').filter(part => !!part);
if ((settingInfo.length === 0) || !this.getSetting(settingInfo[0])) {
return false;
}

openSettingsOptions.query = settingInfo[0];
this.openSettings(openSettingsOptions);
return true;
}

public override dispose(): void {
this._onDispose.fire();
super.dispose();
Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/services/preferences/common/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ export interface IPreferencesService {
openDefaultKeybindingsFile(): Promise<IEditorPane | undefined>;
openLanguageSpecificSettings(languageId: string, options?: IOpenSettingsOptions): Promise<IEditorPane | undefined>;
getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): Promise<URI | null>;
getSetting(settingId: string): ISetting | undefined;

createSplitJsonEditorInput(configurationTarget: ConfigurationTarget, resource: URI): EditorInput;
}
Expand Down Expand Up @@ -329,3 +330,5 @@ export interface IDefineKeybindingEditorContribution extends IEditorContribution
export const FOLDER_SETTINGS_PATH = '.vscode/settings.json';
export const DEFAULT_SETTINGS_EDITOR_SETTING = 'workbench.settings.openDefaultSettings';
export const USE_SPLIT_JSON_SETTING = 'workbench.settings.useSplitJSON';

export const SETTINGS_AUTHORITY = 'settings';
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TestCommandService } from 'vs/editor/test/browser/editorTestServices';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IURLService } from 'vs/platform/url/common/url';
import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor';
import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
import { TestJSONEditingService } from 'vs/workbench/services/configuration/test/common/testServices';
Expand All @@ -32,6 +33,7 @@ suite('PreferencesService', () => {
testInstantiationService.stub(IJSONEditingService, TestJSONEditingService);
testInstantiationService.stub(IRemoteAgentService, TestRemoteAgentService);
testInstantiationService.stub(ICommandService, TestCommandService);
testInstantiationService.stub(IURLService, { registerHandler: () => { } });

// PreferencesService creates a PreferencesEditorInput which depends on IPreferencesService, add the real one, not a stub
const collection = new ServiceCollection();
Expand Down

0 comments on commit 51ce580

Please sign in to comment.