-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
Copy pathmonaco-editor-preference-extractor.ts
274 lines (255 loc) · 14.4 KB
/
monaco-editor-preference-extractor.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
/********************************************************************************
* Copyright (C) 2022 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
/**
* The command contributed in this file allows us to generate a copy of the schema expected for editor preferences by Monaco,
* as well as an interface corresponding to those properties for use with our EditorPreferences PreferenceProxy.
* It examines the schemata registered with the Monaco `ConfigurationRegistry` and writes any configurations associated with the editor
* to a file in the `editor` package. It also generates an interface based on the types specified in the schema.
* The only manual work required during a Monaco uplift is to run the command and then update any fields of the interface where the
* schema type is `array` or `object`, since it is tricky to extract the type details for such fields automatically.
*/
import { ConfigurationScope, Extensions, IConfigurationRegistry } from '@theia/monaco-editor-core/esm/vs/platform/configuration/common/configurationRegistry';
import { Registry } from '@theia/monaco-editor-core/esm/vs/platform/registry/common/platform';
import { CommandContribution, CommandRegistry, MaybeArray, MessageService, nls } from '@theia/core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { PreferenceItem, PreferenceValidationService } from '@theia/core/lib/browser';
import { JSONValue } from '@theia/core/shared/@phosphor/coreutils';
import { JsonType } from '@theia/core/lib/common/json-schema';
import { editorOptionsRegistry } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
function generateContent(properties: string, interfaceEntries: string[]): string {
return `/********************************************************************************
* Copyright (C) 2022 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
import { isOSX, isWindows, nls } from '@theia/core';
import { PreferenceSchema } from '@theia/core/lib/browser';
/* eslint-disable @typescript-eslint/quotes,max-len,no-null/no-null */
/**
* Please do not modify this file by hand. It should be generated automatically
* during a Monaco uplift using the command registered by monaco-editor-preference-extractor.ts
* The only manual work required is fixing preferences with type 'array' or 'object'.
*/
export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] = ${properties};
export interface GeneratedEditorPreferences {
${interfaceEntries.join('\n ')}
}
`;
}
const dequoteMarker = '@#@';
// From src/vs/editor/common/config/editorOptions.ts
const DEFAULT_WINDOWS_FONT_FAMILY = "Consolas, \\'Courier New\\', monospace";
const DEFAULT_MAC_FONT_FAMILY = "Menlo, Monaco, \\'Courier New\\', monospace";
const DEFAULT_LINUX_FONT_FAMILY = "\\'Droid Sans Mono\\', \\'monospace\\', monospace";
const fontFamilyText = `${dequoteMarker}isOSX ? '${DEFAULT_MAC_FONT_FAMILY}' : isWindows ? '${DEFAULT_WINDOWS_FONT_FAMILY}' : '${DEFAULT_LINUX_FONT_FAMILY}'${dequoteMarker}`;
const fontSizeText = `${dequoteMarker}isOSX ? 12 : 14${dequoteMarker}`;
/**
* This class is intended for use when uplifting Monaco.
*/
@injectable()
export class MonacoEditorPreferenceSchemaExtractor implements CommandContribution {
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(MessageService) protected readonly messageService: MessageService;
@inject(FileService) protected readonly fileService: FileService;
@inject(PreferenceValidationService) protected readonly preferenceValidationService: PreferenceValidationService;
@inject(MonacoEditorProvider) protected readonly monacoEditorProvider: MonacoEditorProvider;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand({ id: 'check-for-unvalidated-editor-preferences', label: 'Check for unvalidated editor preferences in Monaco' }, {
execute: () => {
const firstRootUri = this.workspaceService.tryGetRoots()[0]?.resource;
if (firstRootUri) {
const validatedEditorPreferences = new Set(editorOptionsRegistry.map(validator => validator.name));
const allEditorPreferenceKeys = Object.keys(this.monacoEditorProvider['createOptions'](
this.monacoEditorProvider['preferencePrefixes'], firstRootUri.toString(), 'typescript'
));
const unvalidatedKeys = allEditorPreferenceKeys.filter(key => !validatedEditorPreferences.has(key));
console.log('Unvalidated keys are:', unvalidatedKeys);
}
}
});
commands.registerCommand({ id: 'extract-editor-preference-schema', label: 'Extract editor preference schema from Monaco' }, {
execute: async () => {
const roots = this.workspaceService.tryGetRoots();
if (roots.length !== 1 || !(roots[0].resource.path.toString() ?? '').includes('theia')) {
this.messageService.warn('This command should only be executed in the Theia workspace.');
}
const theiaRoot = roots[0];
const fileToWrite = theiaRoot.resource.resolve('packages/editor/src/browser/editor-generated-preference-schema.ts');
const properties = {};
Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurations().forEach(config => {
if (config.id === 'editor' && config.properties) {
Object.assign(properties, config.properties);
}
});
this.guaranteePlatformOptions(properties);
const interfaceEntries = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
for (const [name, description] of Object.entries(properties) as Array<[string, any]>) {
description.scope = this.getScope(description.scope);
delete description.defaultDefaultValue;
if (name === 'editor.fontSize') {
description.default = fontSizeText;
} else if (name === 'editor.fontFamily') {
description.default = fontFamilyText;
}
interfaceEntries.push(`'${name}': ${this.formatSchemaForInterface(description)};`);
}
const stringified = JSON.stringify(properties, this.codeSnippetReplacer(), 4);
const propertyList = this.dequoteCodeSnippets(stringified);
const content = generateContent(propertyList, interfaceEntries);
await this.fileService.write(fileToWrite, content);
}
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected codeSnippetReplacer(): (key: string, value: any) => any {
// JSON.stringify doesn't give back the whole context when serializing so we use state...
let lastPreferenceName: string;
return (key, value) => {
if (key.startsWith('editor.') || key.startsWith('diffEditor.')) {
lastPreferenceName = key;
}
if ((key === 'description' || key === 'markdownDescription') && typeof value === 'string') {
if (value.length === 0) {
return value;
}
const defaultKey = nls.getDefaultKey(value);
if (defaultKey) {
return `${dequoteMarker}nls.localizeByDefault(${dequoteMarker}"${value}${dequoteMarker}")${dequoteMarker}`;
} else {
const localizationKey = `${dequoteMarker}"theia/editor/${lastPreferenceName}${dequoteMarker}"`;
return `${dequoteMarker}nls.localize(${localizationKey}, ${dequoteMarker}"${value}${dequoteMarker}")${dequoteMarker}`;
}
}
if ((key === 'enumDescriptions' || key === 'markdownEnumDescriptions') && Array.isArray(value)) {
return value.map((description, i) => {
if (description.length === 0) {
return description;
}
const defaultKey = nls.getDefaultKey(description);
if (defaultKey) {
return `${dequoteMarker}nls.localizeByDefault(${dequoteMarker}"${description}${dequoteMarker}")${dequoteMarker}`;
} else {
const localizationKey = `${dequoteMarker}"theia/editor/${lastPreferenceName}${i}${dequoteMarker}"`;
return `${dequoteMarker}nls.localize(${localizationKey}, ${dequoteMarker}"${description}${dequoteMarker}")${dequoteMarker}`;
}
});
}
return value;
};
};
protected getScope(monacoScope: unknown): string | undefined {
switch (monacoScope) {
case ConfigurationScope.MACHINE_OVERRIDABLE:
case ConfigurationScope.WINDOW:
return 'window';
case ConfigurationScope.RESOURCE:
return 'resource';
case ConfigurationScope.LANGUAGE_OVERRIDABLE:
return 'language-overridable';
case ConfigurationScope.APPLICATION:
case ConfigurationScope.MACHINE:
return 'application';
}
return undefined;
}
protected formatSchemaForInterface(schema: PreferenceItem): string {
const defaultValue = schema.default !== undefined ? schema.default : schema.defaultValue;
// There are a few preferences for which VSCode uses defaults that do not match the schema. We have to handle those manually.
if (defaultValue !== undefined && this.preferenceValidationService.validateBySchema('any-preference', defaultValue, schema) !== defaultValue) {
return 'HelpBadDefaultValue';
}
const jsonType = schema.const !== undefined ? schema.const : (schema.enum ?? schema.type);
if (jsonType === undefined) {
const subschemata = schema.anyOf ?? schema.oneOf;
if (subschemata) {
const permittedTypes = [].concat.apply(subschemata.map(subschema => this.formatSchemaForInterface(subschema).split(' | ')));
return Array.from(new Set(permittedTypes)).join(' | ');
}
}
return this.formatTypeForInterface(jsonType);
}
protected formatTypeForInterface(jsonType?: MaybeArray<JsonType | JSONValue> | undefined): string {
if (Array.isArray(jsonType)) {
return jsonType.map(subtype => this.formatTypeForInterface(subtype)).join(' | ');
}
switch (jsonType) {
case 'boolean':
case 'number':
case 'string':
case 'true':
case 'false':
return jsonType;
case true:
case false:
case null: // eslint-disable-line no-null/no-null
return `${jsonType}`;
case 'integer':
return 'number';
case 'array':
case 'object':
case undefined:
// These have to be fixed manually, so we output a type that will cause a TS error.
return 'Help';
}
// Most of the rest are string literals.
return `'${jsonType}'`;
}
protected dequoteCodeSnippets(stringification: string): string {
return stringification
.replace(new RegExp(`${dequoteMarker}"|"${dequoteMarker}|${dequoteMarker}\\\\`, 'g'), '')
.replace(new RegExp(`\\\\"${dequoteMarker}`, 'g'), '"')
.replace(/\\\\'/g, "\\'");
}
/**
* Ensures that options that are only relevant on certain platforms are caught.
* Check for use of `platform` in src/vs/editor/common/config/editorOptions.ts
*/
protected guaranteePlatformOptions(properties: object): void {
Object.assign(properties, {
'editor.find.globalFindClipboard': {
type: 'boolean',
default: false,
description: 'Controls whether the Find Widget should read or modify the shared find clipboard on macOS.',
included: `${dequoteMarker}isOSX${dequoteMarker}`,
},
'editor.selectionClipboard': {
type: 'boolean',
default: true,
description: 'Controls whether the Linux primary clipboard should be supported.',
included: `${dequoteMarker}!isOSX && !isWindows${dequoteMarker}`
}
});
}
}
// Utility to assist with Monaco uplifts to generate preference schema. Not for regular use in the application.
export function bindMonacoPreferenceExtractor(bind: interfaces.Bind): void {
// bind(MonacoEditorPreferenceSchemaExtractor).toSelf().inSingletonScope();
// bind(CommandContribution).toService(MonacoEditorPreferenceSchemaExtractor);
}