Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/language/common/languageService.ts
3296 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 { localize } from '../../../../nls.js';
7
import { clearConfiguredLanguageAssociations, registerConfiguredLanguageAssociation } from '../../../../editor/common/services/languagesAssociations.js';
8
import { joinPath } from '../../../../base/common/resources.js';
9
import { URI } from '../../../../base/common/uri.js';
10
import { ILanguageExtensionPoint, ILanguageService } from '../../../../editor/common/languages/language.js';
11
import { LanguageService } from '../../../../editor/common/services/languageService.js';
12
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
13
import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';
14
import { FILES_ASSOCIATIONS_CONFIG, IFilesConfiguration } from '../../../../platform/files/common/files.js';
15
import { IExtensionService } from '../../extensions/common/extensions.js';
16
import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from '../../extensions/common/extensionsRegistry.js';
17
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
18
import { IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';
19
import { ILogService } from '../../../../platform/log/common/log.js';
20
import { Disposable } from '../../../../base/common/lifecycle.js';
21
import { Extensions, IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData } from '../../extensionManagement/common/extensionFeatures.js';
22
import { Registry } from '../../../../platform/registry/common/platform.js';
23
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
24
import { index } from '../../../../base/common/arrays.js';
25
import { MarkdownString } from '../../../../base/common/htmlContent.js';
26
import { isString } from '../../../../base/common/types.js';
27
28
export interface IRawLanguageExtensionPoint {
29
id: string;
30
extensions: string[];
31
filenames: string[];
32
filenamePatterns: string[];
33
firstLine: string;
34
aliases: string[];
35
mimetypes: string[];
36
configuration: string;
37
icon: { light: string; dark: string };
38
}
39
40
export const languagesExtPoint: IExtensionPoint<IRawLanguageExtensionPoint[]> = ExtensionsRegistry.registerExtensionPoint<IRawLanguageExtensionPoint[]>({
41
extensionPoint: 'languages',
42
jsonSchema: {
43
description: localize('vscode.extension.contributes.languages', 'Contributes language declarations.'),
44
type: 'array',
45
items: {
46
type: 'object',
47
defaultSnippets: [{ body: { id: '${1:languageId}', aliases: ['${2:label}'], extensions: ['${3:extension}'], configuration: './language-configuration.json' } }],
48
properties: {
49
id: {
50
description: localize('vscode.extension.contributes.languages.id', 'ID of the language.'),
51
type: 'string'
52
},
53
aliases: {
54
description: localize('vscode.extension.contributes.languages.aliases', 'Name aliases for the language.'),
55
type: 'array',
56
items: {
57
type: 'string'
58
}
59
},
60
extensions: {
61
description: localize('vscode.extension.contributes.languages.extensions', 'File extensions associated to the language.'),
62
default: ['.foo'],
63
type: 'array',
64
items: {
65
type: 'string'
66
}
67
},
68
filenames: {
69
description: localize('vscode.extension.contributes.languages.filenames', 'File names associated to the language.'),
70
type: 'array',
71
items: {
72
type: 'string'
73
}
74
},
75
filenamePatterns: {
76
description: localize('vscode.extension.contributes.languages.filenamePatterns', 'File name glob patterns associated to the language.'),
77
type: 'array',
78
items: {
79
type: 'string'
80
}
81
},
82
mimetypes: {
83
description: localize('vscode.extension.contributes.languages.mimetypes', 'Mime types associated to the language.'),
84
type: 'array',
85
items: {
86
type: 'string'
87
}
88
},
89
firstLine: {
90
description: localize('vscode.extension.contributes.languages.firstLine', 'A regular expression matching the first line of a file of the language.'),
91
type: 'string'
92
},
93
configuration: {
94
description: localize('vscode.extension.contributes.languages.configuration', 'A relative path to a file containing configuration options for the language.'),
95
type: 'string',
96
default: './language-configuration.json'
97
},
98
icon: {
99
type: 'object',
100
description: localize('vscode.extension.contributes.languages.icon', 'A icon to use as file icon, if no icon theme provides one for the language.'),
101
properties: {
102
light: {
103
description: localize('vscode.extension.contributes.languages.icon.light', 'Icon path when a light theme is used'),
104
type: 'string'
105
},
106
dark: {
107
description: localize('vscode.extension.contributes.languages.icon.dark', 'Icon path when a dark theme is used'),
108
type: 'string'
109
}
110
}
111
}
112
}
113
}
114
},
115
activationEventsGenerator: (languageContributions, result) => {
116
for (const languageContribution of languageContributions) {
117
if (languageContribution.id && languageContribution.configuration) {
118
result.push(`onLanguage:${languageContribution.id}`);
119
}
120
}
121
}
122
});
123
124
class LanguageTableRenderer extends Disposable implements IExtensionFeatureTableRenderer {
125
126
readonly type = 'table';
127
128
shouldRender(manifest: IExtensionManifest): boolean {
129
return !!manifest.contributes?.languages;
130
}
131
132
render(manifest: IExtensionManifest): IRenderedData<ITableData> {
133
const contributes = manifest.contributes;
134
const rawLanguages = contributes?.languages || [];
135
const languages: { id: string; name: string; extensions: string[]; hasGrammar: boolean; hasSnippets: boolean }[] = [];
136
for (const l of rawLanguages) {
137
if (isValidLanguageExtensionPoint(l)) {
138
languages.push({
139
id: l.id,
140
name: (l.aliases || [])[0] || l.id,
141
extensions: l.extensions || [],
142
hasGrammar: false,
143
hasSnippets: false
144
});
145
}
146
}
147
const byId = index(languages, l => l.id);
148
149
const grammars = contributes?.grammars || [];
150
grammars.forEach(grammar => {
151
if (!isString(grammar.language)) {
152
// ignore the grammars that are only used as includes in other grammars
153
return;
154
}
155
let language = byId[grammar.language];
156
157
if (language) {
158
language.hasGrammar = true;
159
} else {
160
language = { id: grammar.language, name: grammar.language, extensions: [], hasGrammar: true, hasSnippets: false };
161
byId[language.id] = language;
162
languages.push(language);
163
}
164
});
165
166
const snippets = contributes?.snippets || [];
167
snippets.forEach(snippet => {
168
if (!isString(snippet.language)) {
169
// ignore invalid snippets
170
return;
171
}
172
let language = byId[snippet.language];
173
174
if (language) {
175
language.hasSnippets = true;
176
} else {
177
language = { id: snippet.language, name: snippet.language, extensions: [], hasGrammar: false, hasSnippets: true };
178
byId[language.id] = language;
179
languages.push(language);
180
}
181
});
182
183
if (!languages.length) {
184
return { data: { headers: [], rows: [] }, dispose: () => { } };
185
}
186
187
const headers = [
188
localize('language id', "ID"),
189
localize('language name', "Name"),
190
localize('file extensions', "File Extensions"),
191
localize('grammar', "Grammar"),
192
localize('snippets', "Snippets")
193
];
194
const rows: IRowData[][] = languages.sort((a, b) => a.id.localeCompare(b.id))
195
.map(l => {
196
return [
197
l.id, l.name,
198
new MarkdownString().appendMarkdown(`${l.extensions.map(e => `\`${e}\``).join('&nbsp;')}`),
199
l.hasGrammar ? '✔︎' : '\u2014',
200
l.hasSnippets ? '✔︎' : '\u2014'
201
];
202
});
203
204
return {
205
data: {
206
headers,
207
rows
208
},
209
dispose: () => { }
210
};
211
}
212
}
213
214
Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({
215
id: 'languages',
216
label: localize('languages', "Programming Languages"),
217
access: {
218
canToggle: false
219
},
220
renderer: new SyncDescriptor(LanguageTableRenderer),
221
});
222
223
export class WorkbenchLanguageService extends LanguageService {
224
private _configurationService: IConfigurationService;
225
private _extensionService: IExtensionService;
226
227
constructor(
228
@IExtensionService extensionService: IExtensionService,
229
@IConfigurationService configurationService: IConfigurationService,
230
@IEnvironmentService environmentService: IEnvironmentService,
231
@ILogService private readonly logService: ILogService
232
) {
233
super(environmentService.verbose || environmentService.isExtensionDevelopment || !environmentService.isBuilt);
234
this._configurationService = configurationService;
235
this._extensionService = extensionService;
236
237
languagesExtPoint.setHandler((extensions: readonly IExtensionPointUser<IRawLanguageExtensionPoint[]>[]) => {
238
const allValidLanguages: ILanguageExtensionPoint[] = [];
239
240
for (let i = 0, len = extensions.length; i < len; i++) {
241
const extension = extensions[i];
242
243
if (!Array.isArray(extension.value)) {
244
extension.collector.error(localize('invalid', "Invalid `contributes.{0}`. Expected an array.", languagesExtPoint.name));
245
continue;
246
}
247
248
for (let j = 0, lenJ = extension.value.length; j < lenJ; j++) {
249
const ext = extension.value[j];
250
if (isValidLanguageExtensionPoint(ext, extension.collector)) {
251
let configuration: URI | undefined = undefined;
252
if (ext.configuration) {
253
configuration = joinPath(extension.description.extensionLocation, ext.configuration);
254
}
255
allValidLanguages.push({
256
id: ext.id,
257
extensions: ext.extensions,
258
filenames: ext.filenames,
259
filenamePatterns: ext.filenamePatterns,
260
firstLine: ext.firstLine,
261
aliases: ext.aliases,
262
mimetypes: ext.mimetypes,
263
configuration: configuration,
264
icon: ext.icon && {
265
light: joinPath(extension.description.extensionLocation, ext.icon.light),
266
dark: joinPath(extension.description.extensionLocation, ext.icon.dark)
267
}
268
});
269
}
270
}
271
}
272
273
this._registry.setDynamicLanguages(allValidLanguages);
274
275
});
276
277
this.updateMime();
278
this._register(this._configurationService.onDidChangeConfiguration(e => {
279
if (e.affectsConfiguration(FILES_ASSOCIATIONS_CONFIG)) {
280
this.updateMime();
281
}
282
}));
283
this._extensionService.whenInstalledExtensionsRegistered().then(() => {
284
this.updateMime();
285
});
286
287
this._register(this.onDidRequestRichLanguageFeatures((languageId) => {
288
// extension activation
289
this._extensionService.activateByEvent(`onLanguage:${languageId}`);
290
this._extensionService.activateByEvent(`onLanguage`);
291
}));
292
}
293
294
private updateMime(): void {
295
const configuration = this._configurationService.getValue<IFilesConfiguration>();
296
297
// Clear user configured mime associations
298
clearConfiguredLanguageAssociations();
299
300
// Register based on settings
301
if (configuration.files?.associations) {
302
Object.keys(configuration.files.associations).forEach(pattern => {
303
const langId = configuration.files!.associations[pattern];
304
if (typeof langId !== 'string') {
305
this.logService.warn(`Ignoring configured 'files.associations' for '${pattern}' because its type is not a string but '${typeof langId}'`);
306
307
return; // https://github.com/microsoft/vscode/issues/147284
308
}
309
310
const mimeType = this.getMimeType(langId) || `text/x-${langId}`;
311
312
registerConfiguredLanguageAssociation({ id: langId, mime: mimeType, filepattern: pattern });
313
});
314
}
315
316
this._onDidChange.fire();
317
}
318
}
319
320
function isUndefinedOrStringArray(value: string[]): boolean {
321
if (typeof value === 'undefined') {
322
return true;
323
}
324
if (!Array.isArray(value)) {
325
return false;
326
}
327
return value.every(item => typeof item === 'string');
328
}
329
330
function isValidLanguageExtensionPoint(value: any, collector?: ExtensionMessageCollector): value is IRawLanguageExtensionPoint {
331
if (!value) {
332
collector?.error(localize('invalid.empty', "Empty value for `contributes.{0}`", languagesExtPoint.name));
333
return false;
334
}
335
if (typeof value.id !== 'string') {
336
collector?.error(localize('require.id', "property `{0}` is mandatory and must be of type `string`", 'id'));
337
return false;
338
}
339
if (!isUndefinedOrStringArray(value.extensions)) {
340
collector?.error(localize('opt.extensions', "property `{0}` can be omitted and must be of type `string[]`", 'extensions'));
341
return false;
342
}
343
if (!isUndefinedOrStringArray(value.filenames)) {
344
collector?.error(localize('opt.filenames', "property `{0}` can be omitted and must be of type `string[]`", 'filenames'));
345
return false;
346
}
347
if (typeof value.firstLine !== 'undefined' && typeof value.firstLine !== 'string') {
348
collector?.error(localize('opt.firstLine', "property `{0}` can be omitted and must be of type `string`", 'firstLine'));
349
return false;
350
}
351
if (typeof value.configuration !== 'undefined' && typeof value.configuration !== 'string') {
352
collector?.error(localize('opt.configuration', "property `{0}` can be omitted and must be of type `string`", 'configuration'));
353
return false;
354
}
355
if (!isUndefinedOrStringArray(value.aliases)) {
356
collector?.error(localize('opt.aliases', "property `{0}` can be omitted and must be of type `string[]`", 'aliases'));
357
return false;
358
}
359
if (!isUndefinedOrStringArray(value.mimetypes)) {
360
collector?.error(localize('opt.mimetypes', "property `{0}` can be omitted and must be of type `string[]`", 'mimetypes'));
361
return false;
362
}
363
if (typeof value.icon !== 'undefined') {
364
if (typeof value.icon !== 'object' || typeof value.icon.light !== 'string' || typeof value.icon.dark !== 'string') {
365
collector?.error(localize('opt.icon', "property `{0}` can be omitted and must be of type `object` with properties `{1}` and `{2}` of type `string`", 'icon', 'light', 'dark'));
366
return false;
367
}
368
}
369
return true;
370
}
371
372
registerSingleton(ILanguageService, WorkbenchLanguageService, InstantiationType.Eager);
373
374