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