Path: blob/main/src/vs/editor/common/services/languagesAssociations.ts
3295 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 { ParsedPattern, parse } from '../../../base/common/glob.js';6import { Mimes } from '../../../base/common/mime.js';7import { Schemas } from '../../../base/common/network.js';8import { basename, posix } from '../../../base/common/path.js';9import { DataUri } from '../../../base/common/resources.js';10import { startsWithUTF8BOM } from '../../../base/common/strings.js';11import { URI } from '../../../base/common/uri.js';12import { PLAINTEXT_LANGUAGE_ID } from '../languages/modesRegistry.js';1314export interface ILanguageAssociation {15readonly id: string;16readonly mime: string;17readonly filename?: string;18readonly extension?: string;19readonly filepattern?: string;20readonly firstline?: RegExp;21}2223interface ILanguageAssociationItem extends ILanguageAssociation {24readonly userConfigured: boolean;25readonly filenameLowercase?: string;26readonly extensionLowercase?: string;27readonly filepatternLowercase?: ParsedPattern;28readonly filepatternOnPath?: boolean;29}3031let registeredAssociations: ILanguageAssociationItem[] = [];32let nonUserRegisteredAssociations: ILanguageAssociationItem[] = [];33let userRegisteredAssociations: ILanguageAssociationItem[] = [];3435/**36* Associate a language to the registry (platform).37* * **NOTE**: This association will lose over associations registered using `registerConfiguredLanguageAssociation`.38* * **NOTE**: Use `clearPlatformLanguageAssociations` to remove all associations registered using this function.39*/40export function registerPlatformLanguageAssociation(association: ILanguageAssociation, warnOnOverwrite = false): void {41_registerLanguageAssociation(association, false, warnOnOverwrite);42}4344/**45* Associate a language to the registry (configured).46* * **NOTE**: This association will win over associations registered using `registerPlatformLanguageAssociation`.47* * **NOTE**: Use `clearConfiguredLanguageAssociations` to remove all associations registered using this function.48*/49export function registerConfiguredLanguageAssociation(association: ILanguageAssociation): void {50_registerLanguageAssociation(association, true, false);51}5253function _registerLanguageAssociation(association: ILanguageAssociation, userConfigured: boolean, warnOnOverwrite: boolean): void {5455// Register56const associationItem = toLanguageAssociationItem(association, userConfigured);57registeredAssociations.push(associationItem);58if (!associationItem.userConfigured) {59nonUserRegisteredAssociations.push(associationItem);60} else {61userRegisteredAssociations.push(associationItem);62}6364// Check for conflicts unless this is a user configured association65if (warnOnOverwrite && !associationItem.userConfigured) {66registeredAssociations.forEach(a => {67if (a.mime === associationItem.mime || a.userConfigured) {68return; // same mime or userConfigured is ok69}7071if (associationItem.extension && a.extension === associationItem.extension) {72console.warn(`Overwriting extension <<${associationItem.extension}>> to now point to mime <<${associationItem.mime}>>`);73}7475if (associationItem.filename && a.filename === associationItem.filename) {76console.warn(`Overwriting filename <<${associationItem.filename}>> to now point to mime <<${associationItem.mime}>>`);77}7879if (associationItem.filepattern && a.filepattern === associationItem.filepattern) {80console.warn(`Overwriting filepattern <<${associationItem.filepattern}>> to now point to mime <<${associationItem.mime}>>`);81}8283if (associationItem.firstline && a.firstline === associationItem.firstline) {84console.warn(`Overwriting firstline <<${associationItem.firstline}>> to now point to mime <<${associationItem.mime}>>`);85}86});87}88}8990function toLanguageAssociationItem(association: ILanguageAssociation, userConfigured: boolean): ILanguageAssociationItem {91return {92id: association.id,93mime: association.mime,94filename: association.filename,95extension: association.extension,96filepattern: association.filepattern,97firstline: association.firstline,98userConfigured: userConfigured,99filenameLowercase: association.filename ? association.filename.toLowerCase() : undefined,100extensionLowercase: association.extension ? association.extension.toLowerCase() : undefined,101filepatternLowercase: association.filepattern ? parse(association.filepattern.toLowerCase()) : undefined,102filepatternOnPath: association.filepattern ? association.filepattern.indexOf(posix.sep) >= 0 : false103};104}105106/**107* Clear language associations from the registry (platform).108*/109export function clearPlatformLanguageAssociations(): void {110registeredAssociations = registeredAssociations.filter(a => a.userConfigured);111nonUserRegisteredAssociations = [];112}113114/**115* Clear language associations from the registry (configured).116*/117export function clearConfiguredLanguageAssociations(): void {118registeredAssociations = registeredAssociations.filter(a => !a.userConfigured);119userRegisteredAssociations = [];120}121122interface IdAndMime {123id: string;124mime: string;125}126127/**128* Given a file, return the best matching mime types for it129* based on the registered language associations.130*/131export function getMimeTypes(resource: URI | null, firstLine?: string): string[] {132return getAssociations(resource, firstLine).map(item => item.mime);133}134135/**136* @see `getMimeTypes`137*/138export function getLanguageIds(resource: URI | null, firstLine?: string): string[] {139return getAssociations(resource, firstLine).map(item => item.id);140}141142function getAssociations(resource: URI | null, firstLine?: string): IdAndMime[] {143let path: string | undefined;144if (resource) {145switch (resource.scheme) {146case Schemas.file:147path = resource.fsPath;148break;149case Schemas.data: {150const metadata = DataUri.parseMetaData(resource);151path = metadata.get(DataUri.META_DATA_LABEL);152break;153}154case Schemas.vscodeNotebookCell:155// File path not relevant for language detection of cell156path = undefined;157break;158default:159path = resource.path;160}161}162163if (!path) {164return [{ id: 'unknown', mime: Mimes.unknown }];165}166167path = path.toLowerCase();168169const filename = basename(path);170171// 1.) User configured mappings have highest priority172const configuredLanguage = getAssociationByPath(path, filename, userRegisteredAssociations);173if (configuredLanguage) {174return [configuredLanguage, { id: PLAINTEXT_LANGUAGE_ID, mime: Mimes.text }];175}176177// 2.) Registered mappings have middle priority178const registeredLanguage = getAssociationByPath(path, filename, nonUserRegisteredAssociations);179if (registeredLanguage) {180return [registeredLanguage, { id: PLAINTEXT_LANGUAGE_ID, mime: Mimes.text }];181}182183// 3.) Firstline has lowest priority184if (firstLine) {185const firstlineLanguage = getAssociationByFirstline(firstLine);186if (firstlineLanguage) {187return [firstlineLanguage, { id: PLAINTEXT_LANGUAGE_ID, mime: Mimes.text }];188}189}190191return [{ id: 'unknown', mime: Mimes.unknown }];192}193194function getAssociationByPath(path: string, filename: string, associations: ILanguageAssociationItem[]): ILanguageAssociationItem | undefined {195let filenameMatch: ILanguageAssociationItem | undefined = undefined;196let patternMatch: ILanguageAssociationItem | undefined = undefined;197let extensionMatch: ILanguageAssociationItem | undefined = undefined;198199// We want to prioritize associations based on the order they are registered so that the last registered200// association wins over all other. This is for https://github.com/microsoft/vscode/issues/20074201for (let i = associations.length - 1; i >= 0; i--) {202const association = associations[i];203204// First exact name match205if (filename === association.filenameLowercase) {206filenameMatch = association;207break; // take it!208}209210// Longest pattern match211if (association.filepattern) {212if (!patternMatch || association.filepattern.length > patternMatch.filepattern!.length) {213const target = association.filepatternOnPath ? path : filename; // match on full path if pattern contains path separator214if (association.filepatternLowercase?.(target)) {215patternMatch = association;216}217}218}219220// Longest extension match221if (association.extension) {222if (!extensionMatch || association.extension.length > extensionMatch.extension!.length) {223if (filename.endsWith(association.extensionLowercase!)) {224extensionMatch = association;225}226}227}228}229230// 1.) Exact name match has second highest priority231if (filenameMatch) {232return filenameMatch;233}234235// 2.) Match on pattern236if (patternMatch) {237return patternMatch;238}239240// 3.) Match on extension comes next241if (extensionMatch) {242return extensionMatch;243}244245return undefined;246}247248function getAssociationByFirstline(firstLine: string): ILanguageAssociationItem | undefined {249if (startsWithUTF8BOM(firstLine)) {250firstLine = firstLine.substr(1);251}252253if (firstLine.length > 0) {254255// We want to prioritize associations based on the order they are registered so that the last registered256// association wins over all other. This is for https://github.com/microsoft/vscode/issues/20074257for (let i = registeredAssociations.length - 1; i >= 0; i--) {258const association = registeredAssociations[i];259if (!association.firstline) {260continue;261}262263const matches = firstLine.match(association.firstline);264if (matches && matches.length > 0) {265return association;266}267}268}269270return undefined;271}272273274