Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/services/languagesAssociations.ts
3295 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 { ParsedPattern, parse } from '../../../base/common/glob.js';
7
import { Mimes } from '../../../base/common/mime.js';
8
import { Schemas } from '../../../base/common/network.js';
9
import { basename, posix } from '../../../base/common/path.js';
10
import { DataUri } from '../../../base/common/resources.js';
11
import { startsWithUTF8BOM } from '../../../base/common/strings.js';
12
import { URI } from '../../../base/common/uri.js';
13
import { PLAINTEXT_LANGUAGE_ID } from '../languages/modesRegistry.js';
14
15
export interface ILanguageAssociation {
16
readonly id: string;
17
readonly mime: string;
18
readonly filename?: string;
19
readonly extension?: string;
20
readonly filepattern?: string;
21
readonly firstline?: RegExp;
22
}
23
24
interface ILanguageAssociationItem extends ILanguageAssociation {
25
readonly userConfigured: boolean;
26
readonly filenameLowercase?: string;
27
readonly extensionLowercase?: string;
28
readonly filepatternLowercase?: ParsedPattern;
29
readonly filepatternOnPath?: boolean;
30
}
31
32
let registeredAssociations: ILanguageAssociationItem[] = [];
33
let nonUserRegisteredAssociations: ILanguageAssociationItem[] = [];
34
let userRegisteredAssociations: ILanguageAssociationItem[] = [];
35
36
/**
37
* Associate a language to the registry (platform).
38
* * **NOTE**: This association will lose over associations registered using `registerConfiguredLanguageAssociation`.
39
* * **NOTE**: Use `clearPlatformLanguageAssociations` to remove all associations registered using this function.
40
*/
41
export function registerPlatformLanguageAssociation(association: ILanguageAssociation, warnOnOverwrite = false): void {
42
_registerLanguageAssociation(association, false, warnOnOverwrite);
43
}
44
45
/**
46
* Associate a language to the registry (configured).
47
* * **NOTE**: This association will win over associations registered using `registerPlatformLanguageAssociation`.
48
* * **NOTE**: Use `clearConfiguredLanguageAssociations` to remove all associations registered using this function.
49
*/
50
export function registerConfiguredLanguageAssociation(association: ILanguageAssociation): void {
51
_registerLanguageAssociation(association, true, false);
52
}
53
54
function _registerLanguageAssociation(association: ILanguageAssociation, userConfigured: boolean, warnOnOverwrite: boolean): void {
55
56
// Register
57
const associationItem = toLanguageAssociationItem(association, userConfigured);
58
registeredAssociations.push(associationItem);
59
if (!associationItem.userConfigured) {
60
nonUserRegisteredAssociations.push(associationItem);
61
} else {
62
userRegisteredAssociations.push(associationItem);
63
}
64
65
// Check for conflicts unless this is a user configured association
66
if (warnOnOverwrite && !associationItem.userConfigured) {
67
registeredAssociations.forEach(a => {
68
if (a.mime === associationItem.mime || a.userConfigured) {
69
return; // same mime or userConfigured is ok
70
}
71
72
if (associationItem.extension && a.extension === associationItem.extension) {
73
console.warn(`Overwriting extension <<${associationItem.extension}>> to now point to mime <<${associationItem.mime}>>`);
74
}
75
76
if (associationItem.filename && a.filename === associationItem.filename) {
77
console.warn(`Overwriting filename <<${associationItem.filename}>> to now point to mime <<${associationItem.mime}>>`);
78
}
79
80
if (associationItem.filepattern && a.filepattern === associationItem.filepattern) {
81
console.warn(`Overwriting filepattern <<${associationItem.filepattern}>> to now point to mime <<${associationItem.mime}>>`);
82
}
83
84
if (associationItem.firstline && a.firstline === associationItem.firstline) {
85
console.warn(`Overwriting firstline <<${associationItem.firstline}>> to now point to mime <<${associationItem.mime}>>`);
86
}
87
});
88
}
89
}
90
91
function toLanguageAssociationItem(association: ILanguageAssociation, userConfigured: boolean): ILanguageAssociationItem {
92
return {
93
id: association.id,
94
mime: association.mime,
95
filename: association.filename,
96
extension: association.extension,
97
filepattern: association.filepattern,
98
firstline: association.firstline,
99
userConfigured: userConfigured,
100
filenameLowercase: association.filename ? association.filename.toLowerCase() : undefined,
101
extensionLowercase: association.extension ? association.extension.toLowerCase() : undefined,
102
filepatternLowercase: association.filepattern ? parse(association.filepattern.toLowerCase()) : undefined,
103
filepatternOnPath: association.filepattern ? association.filepattern.indexOf(posix.sep) >= 0 : false
104
};
105
}
106
107
/**
108
* Clear language associations from the registry (platform).
109
*/
110
export function clearPlatformLanguageAssociations(): void {
111
registeredAssociations = registeredAssociations.filter(a => a.userConfigured);
112
nonUserRegisteredAssociations = [];
113
}
114
115
/**
116
* Clear language associations from the registry (configured).
117
*/
118
export function clearConfiguredLanguageAssociations(): void {
119
registeredAssociations = registeredAssociations.filter(a => !a.userConfigured);
120
userRegisteredAssociations = [];
121
}
122
123
interface IdAndMime {
124
id: string;
125
mime: string;
126
}
127
128
/**
129
* Given a file, return the best matching mime types for it
130
* based on the registered language associations.
131
*/
132
export function getMimeTypes(resource: URI | null, firstLine?: string): string[] {
133
return getAssociations(resource, firstLine).map(item => item.mime);
134
}
135
136
/**
137
* @see `getMimeTypes`
138
*/
139
export function getLanguageIds(resource: URI | null, firstLine?: string): string[] {
140
return getAssociations(resource, firstLine).map(item => item.id);
141
}
142
143
function getAssociations(resource: URI | null, firstLine?: string): IdAndMime[] {
144
let path: string | undefined;
145
if (resource) {
146
switch (resource.scheme) {
147
case Schemas.file:
148
path = resource.fsPath;
149
break;
150
case Schemas.data: {
151
const metadata = DataUri.parseMetaData(resource);
152
path = metadata.get(DataUri.META_DATA_LABEL);
153
break;
154
}
155
case Schemas.vscodeNotebookCell:
156
// File path not relevant for language detection of cell
157
path = undefined;
158
break;
159
default:
160
path = resource.path;
161
}
162
}
163
164
if (!path) {
165
return [{ id: 'unknown', mime: Mimes.unknown }];
166
}
167
168
path = path.toLowerCase();
169
170
const filename = basename(path);
171
172
// 1.) User configured mappings have highest priority
173
const configuredLanguage = getAssociationByPath(path, filename, userRegisteredAssociations);
174
if (configuredLanguage) {
175
return [configuredLanguage, { id: PLAINTEXT_LANGUAGE_ID, mime: Mimes.text }];
176
}
177
178
// 2.) Registered mappings have middle priority
179
const registeredLanguage = getAssociationByPath(path, filename, nonUserRegisteredAssociations);
180
if (registeredLanguage) {
181
return [registeredLanguage, { id: PLAINTEXT_LANGUAGE_ID, mime: Mimes.text }];
182
}
183
184
// 3.) Firstline has lowest priority
185
if (firstLine) {
186
const firstlineLanguage = getAssociationByFirstline(firstLine);
187
if (firstlineLanguage) {
188
return [firstlineLanguage, { id: PLAINTEXT_LANGUAGE_ID, mime: Mimes.text }];
189
}
190
}
191
192
return [{ id: 'unknown', mime: Mimes.unknown }];
193
}
194
195
function getAssociationByPath(path: string, filename: string, associations: ILanguageAssociationItem[]): ILanguageAssociationItem | undefined {
196
let filenameMatch: ILanguageAssociationItem | undefined = undefined;
197
let patternMatch: ILanguageAssociationItem | undefined = undefined;
198
let extensionMatch: ILanguageAssociationItem | undefined = undefined;
199
200
// We want to prioritize associations based on the order they are registered so that the last registered
201
// association wins over all other. This is for https://github.com/microsoft/vscode/issues/20074
202
for (let i = associations.length - 1; i >= 0; i--) {
203
const association = associations[i];
204
205
// First exact name match
206
if (filename === association.filenameLowercase) {
207
filenameMatch = association;
208
break; // take it!
209
}
210
211
// Longest pattern match
212
if (association.filepattern) {
213
if (!patternMatch || association.filepattern.length > patternMatch.filepattern!.length) {
214
const target = association.filepatternOnPath ? path : filename; // match on full path if pattern contains path separator
215
if (association.filepatternLowercase?.(target)) {
216
patternMatch = association;
217
}
218
}
219
}
220
221
// Longest extension match
222
if (association.extension) {
223
if (!extensionMatch || association.extension.length > extensionMatch.extension!.length) {
224
if (filename.endsWith(association.extensionLowercase!)) {
225
extensionMatch = association;
226
}
227
}
228
}
229
}
230
231
// 1.) Exact name match has second highest priority
232
if (filenameMatch) {
233
return filenameMatch;
234
}
235
236
// 2.) Match on pattern
237
if (patternMatch) {
238
return patternMatch;
239
}
240
241
// 3.) Match on extension comes next
242
if (extensionMatch) {
243
return extensionMatch;
244
}
245
246
return undefined;
247
}
248
249
function getAssociationByFirstline(firstLine: string): ILanguageAssociationItem | undefined {
250
if (startsWithUTF8BOM(firstLine)) {
251
firstLine = firstLine.substr(1);
252
}
253
254
if (firstLine.length > 0) {
255
256
// We want to prioritize associations based on the order they are registered so that the last registered
257
// association wins over all other. This is for https://github.com/microsoft/vscode/issues/20074
258
for (let i = registeredAssociations.length - 1; i >= 0; i--) {
259
const association = registeredAssociations[i];
260
if (!association.firstline) {
261
continue;
262
}
263
264
const matches = firstLine.match(association.firstline);
265
if (matches && matches.length > 0) {
266
return association;
267
}
268
}
269
}
270
271
return undefined;
272
}
273
274