Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/config/editorConfiguration.ts
5221 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 * as browser from '../../../base/browser/browser.js';
7
import * as arrays from '../../../base/common/arrays.js';
8
import { Emitter, Event } from '../../../base/common/event.js';
9
import { Disposable } from '../../../base/common/lifecycle.js';
10
import * as objects from '../../../base/common/objects.js';
11
import * as platform from '../../../base/common/platform.js';
12
import { ElementSizeObserver } from './elementSizeObserver.js';
13
import { FontMeasurements } from './fontMeasurements.js';
14
import { migrateOptions } from './migrateOptions.js';
15
import { TabFocus } from './tabFocus.js';
16
import { ComputeOptionsMemory, ConfigurationChangedEvent, EditorOption, editorOptionsRegistry, FindComputedEditorOptionValueById, IComputedEditorOptions, IEditorOptions, IEnvironmentalOptions } from '../../common/config/editorOptions.js';
17
import { EditorZoom } from '../../common/config/editorZoom.js';
18
import { BareFontInfo, FontInfo, IValidatedEditorOptions } from '../../common/config/fontInfo.js';
19
import { createBareFontInfoFromValidatedSettings } from '../../common/config/fontInfoFromSettings.js';
20
import { IDimension } from '../../common/core/2d/dimension.js';
21
import { IEditorConfiguration } from '../../common/config/editorConfiguration.js';
22
import { AccessibilitySupport, IAccessibilityService } from '../../../platform/accessibility/common/accessibility.js';
23
import { getWindow, getWindowById } from '../../../base/browser/dom.js';
24
import { PixelRatio } from '../../../base/browser/pixelRatio.js';
25
import { MenuId } from '../../../platform/actions/common/actions.js';
26
import { InputMode } from '../../common/inputMode.js';
27
28
export interface IEditorConstructionOptions extends IEditorOptions {
29
/**
30
* The initial editor dimension (to avoid measuring the container).
31
*/
32
dimension?: IDimension;
33
/**
34
* Place overflow widgets inside an external DOM node.
35
* Defaults to an internal DOM node.
36
*/
37
overflowWidgetsDomNode?: HTMLElement;
38
}
39
40
export class EditorConfiguration extends Disposable implements IEditorConfiguration {
41
42
private _onDidChange = this._register(new Emitter<ConfigurationChangedEvent>());
43
public readonly onDidChange: Event<ConfigurationChangedEvent> = this._onDidChange.event;
44
45
private _onDidChangeFast = this._register(new Emitter<ConfigurationChangedEvent>());
46
public readonly onDidChangeFast: Event<ConfigurationChangedEvent> = this._onDidChangeFast.event;
47
48
public readonly isSimpleWidget: boolean;
49
public readonly contextMenuId: MenuId;
50
private readonly _containerObserver: ElementSizeObserver;
51
52
private _isDominatedByLongLines: boolean = false;
53
private _viewLineCount: number = 1;
54
private _lineNumbersDigitCount: number = 1;
55
private _reservedHeight: number = 0;
56
private _glyphMarginDecorationLaneCount: number = 1;
57
private _targetWindowId: number;
58
59
private readonly _computeOptionsMemory: ComputeOptionsMemory = new ComputeOptionsMemory();
60
/**
61
* Raw options as they were passed in and merged with all calls to `updateOptions`.
62
*/
63
private readonly _rawOptions: IEditorOptions;
64
/**
65
* Validated version of `_rawOptions`.
66
*/
67
private _validatedOptions: ValidatedEditorOptions;
68
/**
69
* Complete options which are a combination of passed in options and env values.
70
*/
71
public options: ComputedEditorOptions;
72
73
constructor(
74
isSimpleWidget: boolean,
75
contextMenuId: MenuId,
76
options: Readonly<IEditorConstructionOptions>,
77
container: HTMLElement | null,
78
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService
79
) {
80
super();
81
this.isSimpleWidget = isSimpleWidget;
82
this.contextMenuId = contextMenuId;
83
this._containerObserver = this._register(new ElementSizeObserver(container, options.dimension));
84
this._targetWindowId = getWindow(container).vscodeWindowId;
85
86
this._rawOptions = deepCloneAndMigrateOptions(options);
87
this._validatedOptions = EditorOptionsUtil.validateOptions(this._rawOptions);
88
this.options = this._computeOptions();
89
90
if (this.options.get(EditorOption.automaticLayout)) {
91
this._containerObserver.startObserving();
92
}
93
94
this._register(EditorZoom.onDidChangeZoomLevel(() => this._recomputeOptions()));
95
this._register(TabFocus.onDidChangeTabFocus(() => this._recomputeOptions()));
96
this._register(this._containerObserver.onDidChange(() => this._recomputeOptions()));
97
this._register(FontMeasurements.onDidChange(() => this._recomputeOptions()));
98
this._register(PixelRatio.getInstance(getWindow(container)).onDidChange(() => this._recomputeOptions()));
99
this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => this._recomputeOptions()));
100
this._register(InputMode.onDidChangeInputMode(() => this._recomputeOptions()));
101
}
102
103
private _recomputeOptions(): void {
104
const newOptions = this._computeOptions();
105
const changeEvent = EditorOptionsUtil.checkEquals(this.options, newOptions);
106
if (changeEvent === null) {
107
// nothing changed!
108
return;
109
}
110
111
this.options = newOptions;
112
this._onDidChangeFast.fire(changeEvent);
113
this._onDidChange.fire(changeEvent);
114
}
115
116
private _computeOptions(): ComputedEditorOptions {
117
const partialEnv = this._readEnvConfiguration();
118
const bareFontInfo = createBareFontInfoFromValidatedSettings(this._validatedOptions, partialEnv.pixelRatio, this.isSimpleWidget);
119
const fontInfo = this._readFontInfo(bareFontInfo);
120
const env: IEnvironmentalOptions = {
121
memory: this._computeOptionsMemory,
122
outerWidth: partialEnv.outerWidth,
123
outerHeight: partialEnv.outerHeight - this._reservedHeight,
124
fontInfo: fontInfo,
125
extraEditorClassName: partialEnv.extraEditorClassName,
126
isDominatedByLongLines: this._isDominatedByLongLines,
127
viewLineCount: this._viewLineCount,
128
lineNumbersDigitCount: this._lineNumbersDigitCount,
129
emptySelectionClipboard: partialEnv.emptySelectionClipboard,
130
pixelRatio: partialEnv.pixelRatio,
131
tabFocusMode: this._validatedOptions.get(EditorOption.tabFocusMode) || TabFocus.getTabFocusMode(),
132
inputMode: InputMode.getInputMode(),
133
accessibilitySupport: partialEnv.accessibilitySupport,
134
glyphMarginDecorationLaneCount: this._glyphMarginDecorationLaneCount,
135
editContextSupported: partialEnv.editContextSupported
136
};
137
return EditorOptionsUtil.computeOptions(this._validatedOptions, env);
138
}
139
140
protected _readEnvConfiguration(): IEnvConfiguration {
141
return {
142
extraEditorClassName: getExtraEditorClassName(),
143
outerWidth: this._containerObserver.getWidth(),
144
outerHeight: this._containerObserver.getHeight(),
145
emptySelectionClipboard: browser.isWebKit || browser.isFirefox,
146
pixelRatio: PixelRatio.getInstance(getWindowById(this._targetWindowId, true).window).value,
147
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
148
editContextSupported: typeof (globalThis as any).EditContext === 'function',
149
accessibilitySupport: (
150
this._accessibilityService.isScreenReaderOptimized()
151
? AccessibilitySupport.Enabled
152
: this._accessibilityService.getAccessibilitySupport()
153
)
154
};
155
}
156
157
protected _readFontInfo(bareFontInfo: BareFontInfo): FontInfo {
158
return FontMeasurements.readFontInfo(getWindowById(this._targetWindowId, true).window, bareFontInfo);
159
}
160
161
public getRawOptions(): IEditorOptions {
162
return this._rawOptions;
163
}
164
165
public updateOptions(_newOptions: Readonly<IEditorOptions>): void {
166
const newOptions = deepCloneAndMigrateOptions(_newOptions);
167
168
const didChange = EditorOptionsUtil.applyUpdate(this._rawOptions, newOptions);
169
if (!didChange) {
170
return;
171
}
172
173
this._validatedOptions = EditorOptionsUtil.validateOptions(this._rawOptions);
174
this._recomputeOptions();
175
}
176
177
public observeContainer(dimension?: IDimension): void {
178
this._containerObserver.observe(dimension);
179
}
180
181
public setIsDominatedByLongLines(isDominatedByLongLines: boolean): void {
182
if (this._isDominatedByLongLines === isDominatedByLongLines) {
183
return;
184
}
185
this._isDominatedByLongLines = isDominatedByLongLines;
186
this._recomputeOptions();
187
}
188
189
public setModelLineCount(modelLineCount: number): void {
190
const lineNumbersDigitCount = digitCount(modelLineCount);
191
if (this._lineNumbersDigitCount === lineNumbersDigitCount) {
192
return;
193
}
194
this._lineNumbersDigitCount = lineNumbersDigitCount;
195
this._recomputeOptions();
196
}
197
198
public setViewLineCount(viewLineCount: number): void {
199
if (this._viewLineCount === viewLineCount) {
200
return;
201
}
202
this._viewLineCount = viewLineCount;
203
this._recomputeOptions();
204
}
205
206
public setReservedHeight(reservedHeight: number) {
207
if (this._reservedHeight === reservedHeight) {
208
return;
209
}
210
this._reservedHeight = reservedHeight;
211
this._recomputeOptions();
212
}
213
214
public setGlyphMarginDecorationLaneCount(decorationLaneCount: number): void {
215
if (this._glyphMarginDecorationLaneCount === decorationLaneCount) {
216
return;
217
}
218
this._glyphMarginDecorationLaneCount = decorationLaneCount;
219
this._recomputeOptions();
220
}
221
}
222
223
function digitCount(n: number): number {
224
let r = 0;
225
while (n) {
226
n = Math.floor(n / 10);
227
r++;
228
}
229
return r ? r : 1;
230
}
231
232
function getExtraEditorClassName(): string {
233
let extra = '';
234
if (browser.isSafari || browser.isWebkitWebView) {
235
// See https://github.com/microsoft/vscode/issues/108822
236
extra += 'no-minimap-shadow ';
237
extra += 'enable-user-select ';
238
} else {
239
// Use user-select: none in all browsers except Safari and native macOS WebView
240
extra += 'no-user-select ';
241
}
242
if (platform.isMacintosh) {
243
extra += 'mac ';
244
}
245
return extra;
246
}
247
248
export interface IEnvConfiguration {
249
extraEditorClassName: string;
250
outerWidth: number;
251
outerHeight: number;
252
emptySelectionClipboard: boolean;
253
pixelRatio: number;
254
accessibilitySupport: AccessibilitySupport;
255
editContextSupported: boolean;
256
}
257
258
class ValidatedEditorOptions implements IValidatedEditorOptions {
259
private readonly _values: unknown[] = [];
260
public _read<T>(option: EditorOption): T {
261
return this._values[option] as T;
262
}
263
public get<T extends EditorOption>(id: T): FindComputedEditorOptionValueById<T> {
264
return this._values[id] as FindComputedEditorOptionValueById<T>;
265
}
266
public _write<T>(option: EditorOption, value: T): void {
267
this._values[option] = value;
268
}
269
}
270
271
export class ComputedEditorOptions implements IComputedEditorOptions {
272
private readonly _values: unknown[] = [];
273
public _read<T>(id: EditorOption): T {
274
if (id >= this._values.length) {
275
throw new Error('Cannot read uninitialized value');
276
}
277
return this._values[id] as T;
278
}
279
public get<T extends EditorOption>(id: T): FindComputedEditorOptionValueById<T> {
280
return this._read(id);
281
}
282
public _write<T>(id: EditorOption, value: T): void {
283
this._values[id] = value;
284
}
285
}
286
287
class EditorOptionsUtil {
288
289
public static validateOptions(options: IEditorOptions): ValidatedEditorOptions {
290
const result = new ValidatedEditorOptions();
291
for (const editorOption of editorOptionsRegistry) {
292
const value = (editorOption.name === '_never_' ? undefined : (options as Record<string, unknown>)[editorOption.name]);
293
result._write(editorOption.id, editorOption.validate(value));
294
}
295
return result;
296
}
297
298
public static computeOptions(options: ValidatedEditorOptions, env: IEnvironmentalOptions): ComputedEditorOptions {
299
const result = new ComputedEditorOptions();
300
for (const editorOption of editorOptionsRegistry) {
301
result._write(editorOption.id, editorOption.compute(env, result, options._read(editorOption.id)));
302
}
303
return result;
304
}
305
306
private static _deepEquals<T>(a: T, b: T): boolean {
307
if (typeof a !== 'object' || typeof b !== 'object' || !a || !b) {
308
return a === b;
309
}
310
if (Array.isArray(a) || Array.isArray(b)) {
311
return (Array.isArray(a) && Array.isArray(b) ? arrays.equals(a, b) : false);
312
}
313
if (Object.keys(a as unknown as object).length !== Object.keys(b as unknown as object).length) {
314
return false;
315
}
316
for (const key in a) {
317
if (!EditorOptionsUtil._deepEquals(a[key], b[key])) {
318
return false;
319
}
320
}
321
return true;
322
}
323
324
public static checkEquals(a: ComputedEditorOptions, b: ComputedEditorOptions): ConfigurationChangedEvent | null {
325
const result: boolean[] = [];
326
let somethingChanged = false;
327
for (const editorOption of editorOptionsRegistry) {
328
const changed = !EditorOptionsUtil._deepEquals(a._read(editorOption.id), b._read(editorOption.id));
329
result[editorOption.id] = changed;
330
if (changed) {
331
somethingChanged = true;
332
}
333
}
334
return (somethingChanged ? new ConfigurationChangedEvent(result) : null);
335
}
336
337
/**
338
* Returns true if something changed.
339
* Modifies `options`.
340
*/
341
public static applyUpdate(options: IEditorOptions, update: Readonly<IEditorOptions>): boolean {
342
let changed = false;
343
for (const editorOption of editorOptionsRegistry) {
344
if (update.hasOwnProperty(editorOption.name)) {
345
const result = editorOption.applyUpdate((options as Record<string, unknown>)[editorOption.name], (update as Record<string, unknown>)[editorOption.name]);
346
(options as Record<string, unknown>)[editorOption.name] = result.newValue;
347
changed = changed || result.didChange;
348
}
349
}
350
return changed;
351
}
352
}
353
354
function deepCloneAndMigrateOptions(_options: Readonly<IEditorOptions>): IEditorOptions {
355
const options = objects.deepClone(_options);
356
migrateOptions(options);
357
return options;
358
}
359
360