Skip to content

Commit

Permalink
editors - validate editor options (#196232)
Browse files Browse the repository at this point in the history
* editors - validate editor options

* add more tests
  • Loading branch information
bpasero committed Oct 23, 2023
1 parent b16c487 commit 5d9da91
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 18 deletions.
33 changes: 32 additions & 1 deletion src/vs/base/common/objects.ts
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { isTypedArray, isObject, isUndefinedOrNull } from 'vs/base/common/types';
import { isTypedArray, isObject, isUndefinedOrNull, OptionalBooleanKey, OptionalNumberKey, OptionalStringKey } from 'vs/base/common/types';

export function deepClone<T>(obj: T): T {
if (!obj || typeof obj !== 'object') {
Expand Down Expand Up @@ -261,3 +261,34 @@ export function createProxyObject<T extends object>(methodNames: string[], invok
}
return result;
}

export function ensureOptionalBooleanValue<T extends object>(obj: T, key: OptionalBooleanKey<T>, defaultValue: boolean | undefined): void {
if (typeof key !== 'string') {
return;
}

if (obj[key] !== undefined && typeof obj[key] !== 'boolean') {
obj[key] = defaultValue as any;
}
}

export function ensureOptionalNumberValue<T extends object>(obj: T, key: OptionalNumberKey<T>, defaultValue: number | undefined): void {
if (typeof key !== 'string') {
return;
}

if (obj[key] !== undefined && typeof obj[key] !== 'number') {
obj[key] = defaultValue as any;
}
}

export function ensureOptionalStringValue<T extends object>(obj: T, key: OptionalStringKey<T>, allowed: string[], defaultValue: string | undefined): void {
if (typeof key !== 'string') {
return;
}

const value = obj[key];
if (value !== undefined && (typeof value !== 'string' || !allowed.includes(value))) {
obj[key] = defaultValue as any;
}
}
12 changes: 12 additions & 0 deletions src/vs/base/common/types.ts
Expand Up @@ -227,3 +227,15 @@ export type Mutable<T> = {
* A single object or an array of the objects.
*/
export type SingleOrMany<T> = T | T[];

export type OptionalBooleanKey<T> = {
[K in keyof T]: T[K] extends boolean | undefined ? K : never;
}[keyof T];

export type OptionalNumberKey<T> = {
[K in keyof T]: T[K] extends number | undefined ? K : never;
}[keyof T];

export type OptionalStringKey<T> = {
[K in keyof T]: T[K] extends string | undefined ? K : never;
}[keyof T];
80 changes: 80 additions & 0 deletions src/vs/base/test/common/objects.test.ts
Expand Up @@ -227,4 +227,84 @@ suite('Objects', () => {
assert.strictEqual(obj1.mIxEdCaSe, objects.getCaseInsensitive(obj1, 'MIXEDCASE'));
assert.strictEqual(obj1.mIxEdCaSe, objects.getCaseInsensitive(obj1, 'mixedcase'));
});

test('ensureOptionalBooleanValue', () => {
const obj: any = {
a: true,
b: false,
c: undefined,
d: 5,
e: 'foo'
};

objects.ensureOptionalBooleanValue(obj, 'a', false);
assert.strictEqual(obj.a, true);

objects.ensureOptionalBooleanValue(obj, 'b', true);
assert.strictEqual(obj.b, false);

objects.ensureOptionalBooleanValue(obj, 'c', true);
assert.strictEqual(obj.c, undefined);

objects.ensureOptionalBooleanValue(obj, 'd', true);
assert.strictEqual(obj.d, true);

objects.ensureOptionalBooleanValue(obj, 'e', true);
assert.strictEqual(obj.e, true);
});

test('ensureOptionalNumberValue', () => {
const obj: any = {
a: 1,
b: 0,
c: undefined,
d: true,
e: 'foo'
};

objects.ensureOptionalNumberValue(obj, 'a', 0);
assert.strictEqual(obj.a, 1);

objects.ensureOptionalNumberValue(obj, 'b', 1);
assert.strictEqual(obj.b, 0);

objects.ensureOptionalNumberValue(obj, 'c', 1);
assert.strictEqual(obj.c, undefined);

objects.ensureOptionalNumberValue(obj, 'd', 1);
assert.strictEqual(obj.d, 1);

objects.ensureOptionalNumberValue(obj, 'e', 1);
assert.strictEqual(obj.e, 1);
});

test('ensureOptionalStringValue', () => {
const obj: any = {
a: 'hello',
b: 'world',
c: undefined,
d: 'earth',
e: 5,
f: true
};

objects.ensureOptionalStringValue(obj, 'a', ['hello', 'world'], 'world');
assert.strictEqual(obj.a, 'hello');

objects.ensureOptionalStringValue(obj, 'b', ['hello', 'world'], 'hello');
assert.strictEqual(obj.b, 'world');

objects.ensureOptionalStringValue(obj, 'c', ['hello', 'world'], 'world');
assert.strictEqual(obj.c, undefined);

objects.ensureOptionalStringValue(obj, 'd', ['hello', 'world'], 'world');
assert.strictEqual(obj.d, 'world');

objects.ensureOptionalStringValue(obj, 'e', ['hello', 'world'], 'world');
assert.strictEqual(obj.e, 'world');

objects.ensureOptionalStringValue(obj, 'f', ['hello', 'world'], 'world');
assert.strictEqual(obj.f, 'world');

});
});
104 changes: 97 additions & 7 deletions src/vs/workbench/browser/parts/editor/editor.ts
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, SideBySideEditor, EditorCloseContext, IEditorPane } from 'vs/workbench/common/editor';
import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, SideBySideEditor, EditorCloseContext, IEditorPane, IEditorPartLimitConfiguration, IEditorPartDecorationsConfiguration } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { IEditorGroup, GroupDirection, IMergeGroupOptions, GroupsOrder, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IDisposable } from 'vs/base/common/lifecycle';
Expand All @@ -13,9 +13,10 @@ import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/co
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ISerializableView } from 'vs/base/browser/ui/grid/grid';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { isObject } from 'vs/base/common/types';
import { OptionalBooleanKey, OptionalNumberKey, OptionalStringKey, isObject } from 'vs/base/common/types';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { IWindowsConfiguration } from 'vs/platform/window/common/window';
import { ensureOptionalBooleanValue, ensureOptionalNumberValue, ensureOptionalStringValue } from 'vs/base/common/objects';

export interface IEditorPartCreationOptions {
readonly restorePreviousState: boolean;
Expand Down Expand Up @@ -90,13 +91,102 @@ export function getEditorPartOptions(configurationService: IConfigurationService
return options;
}

function validateEditorPartOptions(options: IEditorPartOptions) {
// showTabs ensure correct enum value
function validateEditorPartOptions(options: IEditorPartOptions): void {

// Migrate: Show tabs (config migration kicks in very late and can cause flicker otherwise)
if (typeof options.showTabs === 'boolean') {
// Migration service kicks in very late and can cause a flicker otherwise
options.showTabs = options.showTabs ? 'multiple' : 'single';
} else if (options.showTabs !== 'multiple' && options.showTabs !== 'single' && options.showTabs !== 'none') {
options.showTabs = 'multiple';
}

// Boolean options
const booleanOptions: Array<OptionalBooleanKey<IEditorPartOptions>> = [
'wrapTabs',
'scrollToSwitchTabs',
'highlightModifiedTabs',
'pinnedTabsOnSeparateRow',
'focusRecentEditorAfterClose',
'showIcons',
'enablePreview',
'enablePreviewFromQuickOpen',
'enablePreviewFromCodeNavigation',
'closeOnFileDelete',
'closeEmptyGroups',
'revealIfOpen',
'mouseBackForwardToNavigate',
'restoreViewState',
'splitOnDragAndDrop',
'centeredLayoutFixedWidth',
'doubleClickTabToToggleEditorGroupSizes'
];
for (const option of booleanOptions) {
if (typeof option === 'string') {
ensureOptionalBooleanValue(options, option, Boolean(DEFAULT_EDITOR_PART_OPTIONS[option]));
}
}

// Number options
const numberOptions: Array<OptionalNumberKey<IEditorPartOptions>> = [
'tabSizingFixedMinWidth',
'tabSizingFixedMaxWidth'
];
for (const option of numberOptions) {
if (typeof option === 'string') {
ensureOptionalNumberValue(options, option, Number(DEFAULT_EDITOR_PART_OPTIONS[option]));
}
}

// String options
const stringOptions: Array<[OptionalStringKey<IEditorPartOptions>, Array<string>]> = [
['showTabs', ['multiple', 'single', 'none']],
['tabCloseButton', ['left', 'right', 'off']],
['tabSizing', ['fit', 'shrink', 'fixed']],
['pinnedTabSizing', ['normal', 'compact', 'shrink']],
['tabHeight', ['default', 'compact']],
['preventPinnedEditorClose', ['keyboardAndMouse', 'keyboard', 'mouse', 'never']],
['titleScrollbarSizing', ['default', 'large']],
['openPositioning', ['left', 'right', 'first', 'last']],
['openSideBySideDirection', ['right', 'down']],
['labelFormat', ['default', 'short', 'medium', 'long']],
['splitInGroupLayout', ['vertical', 'horizontal']],
['splitSizing', ['distribute', 'split', 'auto']],
];
for (const [option, allowed] of stringOptions) {
if (typeof option === 'string') {
ensureOptionalStringValue(options, option, allowed, String(DEFAULT_EDITOR_PART_OPTIONS[option]));
}
}

// Complex options
if (options.autoLockGroups && !(options.autoLockGroups instanceof Set)) {
options.autoLockGroups = undefined;
}
if (options.limit && !isObject(options.limit)) {
options.limit = undefined;
} else if (options.limit) {
const booleanLimitOptions: Array<OptionalBooleanKey<IEditorPartLimitConfiguration>> = [
'enabled',
'excludeDirty',
'perEditorGroup'
];
for (const option of booleanLimitOptions) {
if (typeof option === 'string') {
ensureOptionalBooleanValue(options.limit, option, undefined);
}
}
ensureOptionalNumberValue(options.limit, 'value', undefined);
}
if (options.decorations && !isObject(options.decorations)) {
options.decorations = undefined;
} else if (options.decorations) {
const booleanDecorationOptions: Array<OptionalBooleanKey<IEditorPartDecorationsConfiguration>> = [
'badges',
'colors'
];
for (const option of booleanDecorationOptions) {
if (typeof option === 'string') {
ensureOptionalBooleanValue(options.decorations, option, undefined);
}
}
}
}

Expand Down
24 changes: 14 additions & 10 deletions src/vs/workbench/common/editor.ts
Expand Up @@ -1095,6 +1095,18 @@ export interface IWorkbenchEditorConfiguration {
};
}

export interface IEditorPartLimitConfiguration {
enabled?: boolean;
excludeDirty?: boolean;
value?: number;
perEditorGroup?: boolean;
}

export interface IEditorPartDecorationsConfiguration {
badges?: boolean;
colors?: boolean;
}

interface IEditorPartConfiguration {
showTabs?: 'multiple' | 'single' | 'none';
wrapTabs?: boolean;
Expand Down Expand Up @@ -1128,16 +1140,8 @@ interface IEditorPartConfiguration {
splitOnDragAndDrop?: boolean;
centeredLayoutFixedWidth?: boolean;
doubleClickTabToToggleEditorGroupSizes?: boolean;
limit?: {
enabled?: boolean;
excludeDirty?: boolean;
value?: number;
perEditorGroup?: boolean;
};
decorations?: {
badges?: boolean;
colors?: boolean;
};
limit?: IEditorPartLimitConfiguration;
decorations?: IEditorPartDecorationsConfiguration;
}

export interface IEditorPartOptions extends IEditorPartConfiguration {
Expand Down

0 comments on commit 5d9da91

Please sign in to comment.