Skip to content

Commit 95031ba

Browse files
authored
fix(cm): preserve CodeMirror mode aliases and improve mode picker (#1974)
1 parent 8704e6e commit 95031ba

File tree

8 files changed

+271
-63
lines changed

8 files changed

+271
-63
lines changed

bun.lock

Lines changed: 19 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"lint": "biome lint --write",
1414
"format": "biome format --write",
1515
"check": "biome check --write",
16+
"list:modes": "node ./utils/listSupportedModes.mjs",
1617
"typecheck": "tsc --noEmit",
1718
"updateAce": "node ./utils/updateAce.js"
1819
},
@@ -107,21 +108,25 @@
107108
"webpack-cli": "^6.0.1"
108109
},
109110
"dependencies": {
110-
"@codemirror/autocomplete": "^6.20.0",
111-
"@codemirror/commands": "^6.10.0",
111+
"@codemirror/lang-angular": "^0.1.4",
112112
"@codemirror/lang-cpp": "^6.0.3",
113113
"@codemirror/lang-css": "^6.3.1",
114114
"@codemirror/lang-go": "^6.0.1",
115115
"@codemirror/lang-html": "^6.4.11",
116116
"@codemirror/lang-java": "^6.0.2",
117-
"@codemirror/lang-javascript": "^6.2.4",
117+
"@codemirror/lang-javascript": "^6.2.5",
118+
"@codemirror/lang-jinja": "^6.0.0",
118119
"@codemirror/lang-json": "^6.0.2",
120+
"@codemirror/lang-less": "^6.0.2",
121+
"@codemirror/lang-liquid": "^6.3.2",
119122
"@codemirror/lang-markdown": "^6.5.0",
120123
"@codemirror/lang-php": "^6.0.2",
121124
"@codemirror/lang-python": "^6.2.1",
122125
"@codemirror/lang-rust": "^6.0.2",
123126
"@codemirror/lang-sass": "^6.0.2",
127+
"@codemirror/lang-sql": "^6.10.0",
124128
"@codemirror/lang-vue": "^0.1.3",
129+
"@codemirror/lang-wast": "^6.0.2",
125130
"@codemirror/lang-xml": "^6.1.0",
126131
"@codemirror/lang-yaml": "^6.1.2",
127132
"@codemirror/language": "^6.11.3",

src/cm/modelist.ts

Lines changed: 122 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,38 @@ import type { Extension } from "@codemirror/state";
22

33
export type LanguageExtensionProvider = () => Extension | Promise<Extension>;
44

5+
export interface AddModeOptions {
6+
aliases?: string[];
7+
filenameMatchers?: RegExp[];
8+
}
9+
510
export interface ModesByName {
611
[name: string]: Mode;
712
}
813

914
const modesByName: ModesByName = {};
1015
const modes: Mode[] = [];
1116

17+
function normalizeModeKey(value: string): string {
18+
return String(value ?? "")
19+
.trim()
20+
.toLowerCase();
21+
}
22+
23+
function normalizeAliases(aliases: string[] = [], name: string): string[] {
24+
const normalized = new Set<string>();
25+
for (const alias of aliases) {
26+
const key = normalizeModeKey(alias);
27+
if (!key || key === name) continue;
28+
normalized.add(key);
29+
}
30+
return [...normalized];
31+
}
32+
33+
function escapeRegExp(value: string): string {
34+
return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35+
}
36+
1237
/**
1338
* Initialize CodeMirror mode list functionality
1439
*/
@@ -25,20 +50,43 @@ export function addMode(
2550
extensions: string | string[],
2651
caption?: string,
2752
languageExtension: LanguageExtensionProvider | null = null,
53+
options: AddModeOptions = {},
2854
): void {
29-
const filename = name.toLowerCase();
30-
const mode = new Mode(filename, caption, extensions, languageExtension);
55+
const filename = normalizeModeKey(name);
56+
const mode = new Mode(
57+
filename,
58+
caption,
59+
extensions,
60+
languageExtension,
61+
options,
62+
);
3163
modesByName[filename] = mode;
64+
mode.aliases.forEach((alias) => {
65+
if (!modesByName[alias]) {
66+
modesByName[alias] = mode;
67+
}
68+
});
3269
modes.push(mode);
3370
}
3471

3572
/**
3673
* Remove language mode from CodeMirror editor
3774
*/
3875
export function removeMode(name: string): void {
39-
const filename = name.toLowerCase();
40-
delete modesByName[filename];
41-
const modeIndex = modes.findIndex((mode) => mode.name === filename);
76+
const filename = normalizeModeKey(name);
77+
const mode = modesByName[filename];
78+
if (!mode) return;
79+
80+
delete modesByName[mode.name];
81+
mode.aliases.forEach((alias) => {
82+
if (modesByName[alias] === mode) {
83+
delete modesByName[alias];
84+
}
85+
});
86+
87+
const modeIndex = modes.findIndex(
88+
(registeredMode) => registeredMode === mode,
89+
);
4290
if (modeIndex >= 0) {
4391
modes.splice(modeIndex, 1);
4492
}
@@ -73,24 +121,32 @@ export function getModeForPath(path: string): Mode {
73121
*/
74122
function getModeSpecificityScore(modeInstance: Mode): number {
75123
const extensionsStr = modeInstance.extensions;
76-
if (!extensionsStr) return 0;
77-
78-
const patterns = extensionsStr.split("|");
79124
let maxScore = 0;
80125

81-
for (const pattern of patterns) {
82-
let currentScore = 0;
83-
if (pattern.startsWith("^")) {
84-
// Exact filename match or anchored pattern
85-
currentScore = 1000 + (pattern.length - 1); // Subtract 1 for '^'
86-
} else {
87-
// Extension match
88-
currentScore = pattern.length;
126+
if (extensionsStr) {
127+
const patterns = extensionsStr.split("|");
128+
for (const pattern of patterns) {
129+
let currentScore = 0;
130+
if (pattern.startsWith("^")) {
131+
// Exact filename match or anchored pattern
132+
currentScore = 1000 + (pattern.length - 1); // Subtract 1 for '^'
133+
} else {
134+
// Extension match
135+
currentScore = pattern.length;
136+
}
137+
if (currentScore > maxScore) {
138+
maxScore = currentScore;
139+
}
89140
}
90-
if (currentScore > maxScore) {
91-
maxScore = currentScore;
141+
}
142+
143+
for (const matcher of modeInstance.filenameMatchers) {
144+
const score = 1000 + matcher.source.length;
145+
if (score > maxScore) {
146+
maxScore = score;
92147
}
93148
}
149+
94150
return maxScore;
95151
}
96152

@@ -108,19 +164,26 @@ export function getModes(): Mode[] {
108164
return modes;
109165
}
110166

167+
export function getMode(name: string): Mode | null {
168+
return modesByName[normalizeModeKey(name)] || null;
169+
}
170+
111171
export class Mode {
112172
extensions: string;
113173
caption: string;
114174
name: string;
115175
mode: string;
116-
extRe: RegExp;
176+
aliases: string[];
177+
extRe: RegExp | null;
178+
filenameMatchers: RegExp[];
117179
languageExtension: LanguageExtensionProvider | null;
118180

119181
constructor(
120182
name: string,
121183
caption: string | undefined,
122184
extensions: string | string[],
123185
languageExtension: LanguageExtensionProvider | null = null,
186+
options: AddModeOptions = {},
124187
) {
125188
if (Array.isArray(extensions)) {
126189
extensions = extensions.join("|");
@@ -130,23 +193,53 @@ export class Mode {
130193
this.mode = name; // CodeMirror uses different mode naming
131194
this.extensions = extensions;
132195
this.caption = caption || this.name.replace(/_/g, " ");
196+
this.aliases = normalizeAliases(options.aliases, this.name);
197+
this.filenameMatchers = Array.isArray(options.filenameMatchers)
198+
? options.filenameMatchers.filter((matcher) => matcher instanceof RegExp)
199+
: [];
133200
this.languageExtension = languageExtension;
134-
let re: string;
135-
136-
if (/\^/.test(extensions)) {
137-
re =
138-
extensions.replace(/\|(\^)?/g, function (_a: string, b: string) {
139-
return "$|" + (b ? "^" : "^.*\\.");
140-
}) + "$";
141-
} else {
142-
re = "^.*\\.(" + extensions + ")$";
201+
let re = "";
202+
203+
if (!extensions) {
204+
this.extRe = null;
205+
return;
143206
}
144207

208+
const patterns = extensions
209+
.split("|")
210+
.map((pattern) => pattern.trim())
211+
.filter(Boolean);
212+
const filenamePatterns = patterns
213+
.filter((pattern) => pattern.startsWith("^"))
214+
.map((pattern) => `^${escapeRegExp(pattern.slice(1))}$`);
215+
const extensionPatterns = patterns
216+
.filter((pattern) => !pattern.startsWith("^"))
217+
.map((pattern) => escapeRegExp(pattern));
218+
const regexParts: string[] = [];
219+
220+
if (extensionPatterns.length) {
221+
regexParts.push(`^.*\\.(${extensionPatterns.join("|")})$`);
222+
}
223+
224+
regexParts.push(...filenamePatterns);
225+
226+
if (!regexParts.length) {
227+
this.extRe = null;
228+
return;
229+
}
230+
231+
re =
232+
regexParts.length === 1 ? regexParts[0] : `(?:${regexParts.join("|")})`;
145233
this.extRe = new RegExp(re, "i");
146234
}
147235

148236
supportsFile(filename: string): boolean {
149-
return this.extRe.test(filename);
237+
if (this.extRe?.test(filename)) return true;
238+
239+
return this.filenameMatchers.some((matcher) => {
240+
matcher.lastIndex = 0;
241+
return matcher.test(filename);
242+
});
150243
}
151244

152245
/**

0 commit comments

Comments
 (0)