Path: blob/main/src/vs/workbench/services/language/common/languageService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { localize } from '../../../../nls.js';6import { clearConfiguredLanguageAssociations, registerConfiguredLanguageAssociation } from '../../../../editor/common/services/languagesAssociations.js';7import { joinPath } from '../../../../base/common/resources.js';8import { URI } from '../../../../base/common/uri.js';9import { ILanguageExtensionPoint, ILanguageService } from '../../../../editor/common/languages/language.js';10import { LanguageService } from '../../../../editor/common/services/languageService.js';11import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';12import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';13import { FILES_ASSOCIATIONS_CONFIG, IFilesConfiguration } from '../../../../platform/files/common/files.js';14import { IExtensionService } from '../../extensions/common/extensions.js';15import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from '../../extensions/common/extensionsRegistry.js';16import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';17import { IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';18import { ILogService } from '../../../../platform/log/common/log.js';19import { Disposable } from '../../../../base/common/lifecycle.js';20import { Extensions, IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData } from '../../extensionManagement/common/extensionFeatures.js';21import { Registry } from '../../../../platform/registry/common/platform.js';22import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';23import { index } from '../../../../base/common/arrays.js';24import { MarkdownString } from '../../../../base/common/htmlContent.js';25import { isString } from '../../../../base/common/types.js';2627export interface IRawLanguageExtensionPoint {28id: string;29extensions: string[];30filenames: string[];31filenamePatterns: string[];32firstLine: string;33aliases: string[];34mimetypes: string[];35configuration: string;36icon: { light: string; dark: string };37}3839export const languagesExtPoint: IExtensionPoint<IRawLanguageExtensionPoint[]> = ExtensionsRegistry.registerExtensionPoint<IRawLanguageExtensionPoint[]>({40extensionPoint: 'languages',41jsonSchema: {42description: localize('vscode.extension.contributes.languages', 'Contributes language declarations.'),43type: 'array',44items: {45type: 'object',46defaultSnippets: [{ body: { id: '${1:languageId}', aliases: ['${2:label}'], extensions: ['${3:extension}'], configuration: './language-configuration.json' } }],47properties: {48id: {49description: localize('vscode.extension.contributes.languages.id', 'ID of the language.'),50type: 'string'51},52aliases: {53description: localize('vscode.extension.contributes.languages.aliases', 'Name aliases for the language.'),54type: 'array',55items: {56type: 'string'57}58},59extensions: {60description: localize('vscode.extension.contributes.languages.extensions', 'File extensions associated to the language.'),61default: ['.foo'],62type: 'array',63items: {64type: 'string'65}66},67filenames: {68description: localize('vscode.extension.contributes.languages.filenames', 'File names associated to the language.'),69type: 'array',70items: {71type: 'string'72}73},74filenamePatterns: {75description: localize('vscode.extension.contributes.languages.filenamePatterns', 'File name glob patterns associated to the language.'),76type: 'array',77items: {78type: 'string'79}80},81mimetypes: {82description: localize('vscode.extension.contributes.languages.mimetypes', 'Mime types associated to the language.'),83type: 'array',84items: {85type: 'string'86}87},88firstLine: {89description: localize('vscode.extension.contributes.languages.firstLine', 'A regular expression matching the first line of a file of the language.'),90type: 'string'91},92configuration: {93description: localize('vscode.extension.contributes.languages.configuration', 'A relative path to a file containing configuration options for the language.'),94type: 'string',95default: './language-configuration.json'96},97icon: {98type: 'object',99description: localize('vscode.extension.contributes.languages.icon', 'A icon to use as file icon, if no icon theme provides one for the language.'),100properties: {101light: {102description: localize('vscode.extension.contributes.languages.icon.light', 'Icon path when a light theme is used'),103type: 'string'104},105dark: {106description: localize('vscode.extension.contributes.languages.icon.dark', 'Icon path when a dark theme is used'),107type: 'string'108}109}110}111}112}113},114activationEventsGenerator: (languageContributions, result) => {115for (const languageContribution of languageContributions) {116if (languageContribution.id && languageContribution.configuration) {117result.push(`onLanguage:${languageContribution.id}`);118}119}120}121});122123class LanguageTableRenderer extends Disposable implements IExtensionFeatureTableRenderer {124125readonly type = 'table';126127shouldRender(manifest: IExtensionManifest): boolean {128return !!manifest.contributes?.languages;129}130131render(manifest: IExtensionManifest): IRenderedData<ITableData> {132const contributes = manifest.contributes;133const rawLanguages = contributes?.languages || [];134const languages: { id: string; name: string; extensions: string[]; hasGrammar: boolean; hasSnippets: boolean }[] = [];135for (const l of rawLanguages) {136if (isValidLanguageExtensionPoint(l)) {137languages.push({138id: l.id,139name: (l.aliases || [])[0] || l.id,140extensions: l.extensions || [],141hasGrammar: false,142hasSnippets: false143});144}145}146const byId = index(languages, l => l.id);147148const grammars = contributes?.grammars || [];149grammars.forEach(grammar => {150if (!isString(grammar.language)) {151// ignore the grammars that are only used as includes in other grammars152return;153}154let language = byId[grammar.language];155156if (language) {157language.hasGrammar = true;158} else {159language = { id: grammar.language, name: grammar.language, extensions: [], hasGrammar: true, hasSnippets: false };160byId[language.id] = language;161languages.push(language);162}163});164165const snippets = contributes?.snippets || [];166snippets.forEach(snippet => {167if (!isString(snippet.language)) {168// ignore invalid snippets169return;170}171let language = byId[snippet.language];172173if (language) {174language.hasSnippets = true;175} else {176language = { id: snippet.language, name: snippet.language, extensions: [], hasGrammar: false, hasSnippets: true };177byId[language.id] = language;178languages.push(language);179}180});181182if (!languages.length) {183return { data: { headers: [], rows: [] }, dispose: () => { } };184}185186const headers = [187localize('language id', "ID"),188localize('language name', "Name"),189localize('file extensions', "File Extensions"),190localize('grammar', "Grammar"),191localize('snippets', "Snippets")192];193const rows: IRowData[][] = languages.sort((a, b) => a.id.localeCompare(b.id))194.map(l => {195return [196l.id, l.name,197new MarkdownString().appendMarkdown(`${l.extensions.map(e => `\`${e}\``).join(' ')}`),198l.hasGrammar ? '✔︎' : '\u2014',199l.hasSnippets ? '✔︎' : '\u2014'200];201});202203return {204data: {205headers,206rows207},208dispose: () => { }209};210}211}212213Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({214id: 'languages',215label: localize('languages', "Programming Languages"),216access: {217canToggle: false218},219renderer: new SyncDescriptor(LanguageTableRenderer),220});221222export class WorkbenchLanguageService extends LanguageService {223private _configurationService: IConfigurationService;224private _extensionService: IExtensionService;225226constructor(227@IExtensionService extensionService: IExtensionService,228@IConfigurationService configurationService: IConfigurationService,229@IEnvironmentService environmentService: IEnvironmentService,230@ILogService private readonly logService: ILogService231) {232super(environmentService.verbose || environmentService.isExtensionDevelopment || !environmentService.isBuilt);233this._configurationService = configurationService;234this._extensionService = extensionService;235236languagesExtPoint.setHandler((extensions: readonly IExtensionPointUser<IRawLanguageExtensionPoint[]>[]) => {237const allValidLanguages: ILanguageExtensionPoint[] = [];238239for (let i = 0, len = extensions.length; i < len; i++) {240const extension = extensions[i];241242if (!Array.isArray(extension.value)) {243extension.collector.error(localize('invalid', "Invalid `contributes.{0}`. Expected an array.", languagesExtPoint.name));244continue;245}246247for (let j = 0, lenJ = extension.value.length; j < lenJ; j++) {248const ext = extension.value[j];249if (isValidLanguageExtensionPoint(ext, extension.collector)) {250let configuration: URI | undefined = undefined;251if (ext.configuration) {252configuration = joinPath(extension.description.extensionLocation, ext.configuration);253}254allValidLanguages.push({255id: ext.id,256extensions: ext.extensions,257filenames: ext.filenames,258filenamePatterns: ext.filenamePatterns,259firstLine: ext.firstLine,260aliases: ext.aliases,261mimetypes: ext.mimetypes,262configuration: configuration,263icon: ext.icon && {264light: joinPath(extension.description.extensionLocation, ext.icon.light),265dark: joinPath(extension.description.extensionLocation, ext.icon.dark)266}267});268}269}270}271272this._registry.setDynamicLanguages(allValidLanguages);273274});275276this.updateMime();277this._register(this._configurationService.onDidChangeConfiguration(e => {278if (e.affectsConfiguration(FILES_ASSOCIATIONS_CONFIG)) {279this.updateMime();280}281}));282this._extensionService.whenInstalledExtensionsRegistered().then(() => {283this.updateMime();284});285286this._register(this.onDidRequestRichLanguageFeatures((languageId) => {287// extension activation288this._extensionService.activateByEvent(`onLanguage:${languageId}`);289this._extensionService.activateByEvent(`onLanguage`);290}));291}292293private updateMime(): void {294const configuration = this._configurationService.getValue<IFilesConfiguration>();295296// Clear user configured mime associations297clearConfiguredLanguageAssociations();298299// Register based on settings300if (configuration.files?.associations) {301Object.keys(configuration.files.associations).forEach(pattern => {302const langId = configuration.files!.associations[pattern];303if (typeof langId !== 'string') {304this.logService.warn(`Ignoring configured 'files.associations' for '${pattern}' because its type is not a string but '${typeof langId}'`);305306return; // https://github.com/microsoft/vscode/issues/147284307}308309const mimeType = this.getMimeType(langId) || `text/x-${langId}`;310311registerConfiguredLanguageAssociation({ id: langId, mime: mimeType, filepattern: pattern });312});313}314315this._onDidChange.fire();316}317}318319function isUndefinedOrStringArray(value: string[]): boolean {320if (typeof value === 'undefined') {321return true;322}323if (!Array.isArray(value)) {324return false;325}326return value.every(item => typeof item === 'string');327}328329function isValidLanguageExtensionPoint(value: any, collector?: ExtensionMessageCollector): value is IRawLanguageExtensionPoint {330if (!value) {331collector?.error(localize('invalid.empty', "Empty value for `contributes.{0}`", languagesExtPoint.name));332return false;333}334if (typeof value.id !== 'string') {335collector?.error(localize('require.id', "property `{0}` is mandatory and must be of type `string`", 'id'));336return false;337}338if (!isUndefinedOrStringArray(value.extensions)) {339collector?.error(localize('opt.extensions', "property `{0}` can be omitted and must be of type `string[]`", 'extensions'));340return false;341}342if (!isUndefinedOrStringArray(value.filenames)) {343collector?.error(localize('opt.filenames', "property `{0}` can be omitted and must be of type `string[]`", 'filenames'));344return false;345}346if (typeof value.firstLine !== 'undefined' && typeof value.firstLine !== 'string') {347collector?.error(localize('opt.firstLine', "property `{0}` can be omitted and must be of type `string`", 'firstLine'));348return false;349}350if (typeof value.configuration !== 'undefined' && typeof value.configuration !== 'string') {351collector?.error(localize('opt.configuration', "property `{0}` can be omitted and must be of type `string`", 'configuration'));352return false;353}354if (!isUndefinedOrStringArray(value.aliases)) {355collector?.error(localize('opt.aliases', "property `{0}` can be omitted and must be of type `string[]`", 'aliases'));356return false;357}358if (!isUndefinedOrStringArray(value.mimetypes)) {359collector?.error(localize('opt.mimetypes', "property `{0}` can be omitted and must be of type `string[]`", 'mimetypes'));360return false;361}362if (typeof value.icon !== 'undefined') {363if (typeof value.icon !== 'object' || typeof value.icon.light !== 'string' || typeof value.icon.dark !== 'string') {364collector?.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'));365return false;366}367}368return true;369}370371registerSingleton(ILanguageService, WorkbenchLanguageService, InstantiationType.Eager);372373374