Path: blob/main/src/vs/editor/common/services/languagesAssociations.ts
5250 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 { endsWithIgnoreCase, equals, 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 filepatternParsed?: ParsedPattern;26readonly filepatternOnPath?: boolean;27}2829let registeredAssociations: ILanguageAssociationItem[] = [];30let nonUserRegisteredAssociations: ILanguageAssociationItem[] = [];31let userRegisteredAssociations: ILanguageAssociationItem[] = [];3233/**34* Associate a language to the registry (platform).35* * **NOTE**: This association will lose over associations registered using `registerConfiguredLanguageAssociation`.36* * **NOTE**: Use `clearPlatformLanguageAssociations` to remove all associations registered using this function.37*/38export function registerPlatformLanguageAssociation(association: ILanguageAssociation, warnOnOverwrite = false): void {39_registerLanguageAssociation(association, false, warnOnOverwrite);40}4142/**43* Associate a language to the registry (configured).44* * **NOTE**: This association will win over associations registered using `registerPlatformLanguageAssociation`.45* * **NOTE**: Use `clearConfiguredLanguageAssociations` to remove all associations registered using this function.46*/47export function registerConfiguredLanguageAssociation(association: ILanguageAssociation): void {48_registerLanguageAssociation(association, true, false);49}5051function _registerLanguageAssociation(association: ILanguageAssociation, userConfigured: boolean, warnOnOverwrite: boolean): void {5253// Register54const associationItem = toLanguageAssociationItem(association, userConfigured);55registeredAssociations.push(associationItem);56if (!associationItem.userConfigured) {57nonUserRegisteredAssociations.push(associationItem);58} else {59userRegisteredAssociations.push(associationItem);60}6162// Check for conflicts unless this is a user configured association63if (warnOnOverwrite && !associationItem.userConfigured) {64registeredAssociations.forEach(a => {65if (a.mime === associationItem.mime || a.userConfigured) {66return; // same mime or userConfigured is ok67}6869if (associationItem.extension && a.extension === associationItem.extension) {70console.warn(`Overwriting extension <<${associationItem.extension}>> to now point to mime <<${associationItem.mime}>>`);71}7273if (associationItem.filename && a.filename === associationItem.filename) {74console.warn(`Overwriting filename <<${associationItem.filename}>> to now point to mime <<${associationItem.mime}>>`);75}7677if (associationItem.filepattern && a.filepattern === associationItem.filepattern) {78console.warn(`Overwriting filepattern <<${associationItem.filepattern}>> to now point to mime <<${associationItem.mime}>>`);79}8081if (associationItem.firstline && a.firstline === associationItem.firstline) {82console.warn(`Overwriting firstline <<${associationItem.firstline}>> to now point to mime <<${associationItem.mime}>>`);83}84});85}86}8788function toLanguageAssociationItem(association: ILanguageAssociation, userConfigured: boolean): ILanguageAssociationItem {89return {90id: association.id,91mime: association.mime,92filename: association.filename,93extension: association.extension,94filepattern: association.filepattern,95firstline: association.firstline,96userConfigured: userConfigured,97filepatternParsed: association.filepattern ? parse(association.filepattern, { ignoreCase: true }) : undefined,98filepatternOnPath: association.filepattern ? association.filepattern.indexOf(posix.sep) >= 0 : false99};100}101102/**103* Clear language associations from the registry (platform).104*/105export function clearPlatformLanguageAssociations(): void {106registeredAssociations = registeredAssociations.filter(a => a.userConfigured);107nonUserRegisteredAssociations = [];108}109110/**111* Clear language associations from the registry (configured).112*/113export function clearConfiguredLanguageAssociations(): void {114registeredAssociations = registeredAssociations.filter(a => !a.userConfigured);115userRegisteredAssociations = [];116}117118interface IdAndMime {119id: string;120mime: string;121}122123/**124* Given a file, return the best matching mime types for it125* based on the registered language associations.126*/127export function getMimeTypes(resource: URI | null, firstLine?: string): string[] {128return getAssociations(resource, firstLine).map(item => item.mime);129}130131/**132* @see `getMimeTypes`133*/134export function getLanguageIds(resource: URI | null, firstLine?: string): string[] {135return getAssociations(resource, firstLine).map(item => item.id);136}137138function getAssociations(resource: URI | null, firstLine?: string): IdAndMime[] {139let path: string | undefined;140if (resource) {141switch (resource.scheme) {142case Schemas.file:143path = resource.fsPath;144break;145case Schemas.data: {146const metadata = DataUri.parseMetaData(resource);147path = metadata.get(DataUri.META_DATA_LABEL);148break;149}150case Schemas.vscodeNotebookCell:151// File path not relevant for language detection of cell152path = undefined;153break;154default:155path = resource.path;156}157}158159if (!path) {160return [{ id: 'unknown', mime: Mimes.unknown }];161}162163path = path.toLowerCase();164165const filename = basename(path);166167// 1.) User configured mappings have highest priority168const configuredLanguage = getAssociationByPath(path, filename, userRegisteredAssociations);169if (configuredLanguage) {170return [configuredLanguage, { id: PLAINTEXT_LANGUAGE_ID, mime: Mimes.text }];171}172173// 2.) Registered mappings have middle priority174const registeredLanguage = getAssociationByPath(path, filename, nonUserRegisteredAssociations);175if (registeredLanguage) {176return [registeredLanguage, { id: PLAINTEXT_LANGUAGE_ID, mime: Mimes.text }];177}178179// 3.) Firstline has lowest priority180if (firstLine) {181const firstlineLanguage = getAssociationByFirstline(firstLine);182if (firstlineLanguage) {183return [firstlineLanguage, { id: PLAINTEXT_LANGUAGE_ID, mime: Mimes.text }];184}185}186187return [{ id: 'unknown', mime: Mimes.unknown }];188}189190function getAssociationByPath(path: string, filename: string, associations: ILanguageAssociationItem[]): ILanguageAssociationItem | undefined {191let filenameMatch: ILanguageAssociationItem | undefined = undefined;192let patternMatch: ILanguageAssociationItem | undefined = undefined;193let extensionMatch: ILanguageAssociationItem | undefined = undefined;194195// We want to prioritize associations based on the order they are registered so that the last registered196// association wins over all other. This is for https://github.com/microsoft/vscode/issues/20074197for (let i = associations.length - 1; i >= 0; i--) {198const association = associations[i];199200// First exact name match201if (equals(filename, association.filename, true)) {202filenameMatch = association;203break; // take it!204}205206// Longest pattern match207if (association.filepattern) {208if (!patternMatch || association.filepattern.length > patternMatch.filepattern!.length) {209const target = association.filepatternOnPath ? path : filename; // match on full path if pattern contains path separator210if (association.filepatternParsed?.(target)) {211patternMatch = association;212}213}214}215216// Longest extension match217if (association.extension) {218if (!extensionMatch || association.extension.length > extensionMatch.extension!.length) {219if (endsWithIgnoreCase(filename, association.extension)) {220extensionMatch = association;221}222}223}224}225226// 1.) Exact name match has second highest priority227if (filenameMatch) {228return filenameMatch;229}230231// 2.) Match on pattern232if (patternMatch) {233return patternMatch;234}235236// 3.) Match on extension comes next237if (extensionMatch) {238return extensionMatch;239}240241return undefined;242}243244function getAssociationByFirstline(firstLine: string): ILanguageAssociationItem | undefined {245if (startsWithUTF8BOM(firstLine)) {246firstLine = firstLine.substring(1);247}248249if (firstLine.length > 0) {250251// We want to prioritize associations based on the order they are registered so that the last registered252// association wins over all other. This is for https://github.com/microsoft/vscode/issues/20074253for (let i = registeredAssociations.length - 1; i >= 0; i--) {254const association = registeredAssociations[i];255if (!association.firstline) {256continue;257}258259const matches = firstLine.match(association.firstline);260if (matches && matches.length > 0) {261return association;262}263}264}265266return undefined;267}268269270