Skip to content

Commit 46d793b

Browse files
committed
Merge remote-tracking branch 'origin/main' into cm-improvements
2 parents 84a3ea8 + 149a99b commit 46d793b

File tree

7 files changed

+250
-55
lines changed

7 files changed

+250
-55
lines changed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,25 @@
109109
"dependencies": {
110110
"@codemirror/autocomplete": "^6.20.1",
111111
"@codemirror/commands": "^6.10.3",
112+
"@codemirror/lang-angular": "^0.1.4",
112113
"@codemirror/lang-cpp": "^6.0.3",
113114
"@codemirror/lang-css": "^6.3.1",
114115
"@codemirror/lang-go": "^6.0.1",
115116
"@codemirror/lang-html": "^6.4.11",
116117
"@codemirror/lang-java": "^6.0.2",
117118
"@codemirror/lang-javascript": "^6.2.5",
119+
"@codemirror/lang-jinja": "^6.0.0",
118120
"@codemirror/lang-json": "^6.0.2",
121+
"@codemirror/lang-less": "^6.0.2",
122+
"@codemirror/lang-liquid": "^6.3.2",
119123
"@codemirror/lang-markdown": "^6.5.0",
120124
"@codemirror/lang-php": "^6.0.2",
121125
"@codemirror/lang-python": "^6.2.1",
122126
"@codemirror/lang-rust": "^6.0.2",
123127
"@codemirror/lang-sass": "^6.0.2",
128+
"@codemirror/lang-sql": "^6.10.0",
124129
"@codemirror/lang-vue": "^0.1.3",
130+
"@codemirror/lang-wast": "^6.0.2",
125131
"@codemirror/lang-xml": "^6.1.0",
126132
"@codemirror/lang-yaml": "^6.1.2",
127133
"@codemirror/language": "^6.12.2",

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
/**

src/cm/supportedModes.ts

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,77 @@ import { languages } from "@codemirror/language-data";
22
import type { Extension } from "@codemirror/state";
33
import { addMode } from "./modelist";
44

5+
type FilenameMatcher = string | RegExp;
6+
57
interface LanguageDescription {
68
name?: string;
9+
alias?: readonly string[];
710
extensions?: readonly string[];
8-
filenames?: readonly string[];
9-
filename?: string;
11+
filenames?: readonly FilenameMatcher[];
12+
filename?: FilenameMatcher;
1013
load?: () => Promise<Extension>;
1114
}
1215

16+
function normalizeModeKey(value: string): string {
17+
return String(value ?? "")
18+
.trim()
19+
.toLowerCase();
20+
}
21+
22+
function isSafeModeId(value: string): boolean {
23+
return /^[a-z0-9][a-z0-9._-]*$/.test(value);
24+
}
25+
26+
function slugifyModeId(value: string): string {
27+
return normalizeModeKey(value)
28+
.replace(/\+\+/g, "pp")
29+
.replace(/#/g, "sharp")
30+
.replace(/&/g, "and")
31+
.replace(/[^a-z0-9._-]+/g, "-")
32+
.replace(/^-+|-+$/g, "");
33+
}
34+
35+
function collectAliases(
36+
name: string,
37+
aliases: readonly string[] | undefined,
38+
): string[] {
39+
return [
40+
...new Set(
41+
[name, ...(aliases || [])].map(normalizeModeKey).filter(Boolean),
42+
),
43+
];
44+
}
45+
46+
function getModeId(name: string, aliases: string[]): string {
47+
const normalizedName = normalizeModeKey(name);
48+
if (isSafeModeId(normalizedName)) return normalizedName;
49+
50+
const safeAlias = aliases.find(
51+
(alias) => alias !== normalizedName && isSafeModeId(alias),
52+
);
53+
return safeAlias || slugifyModeId(name) || normalizedName || "text";
54+
}
55+
56+
function escapeRegExp(value: string): string {
57+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58+
}
59+
1360
// 1) Always register a plain text fallback
1461
addMode("Text", "txt|text|log|plain", "Plain Text", () => []);
1562

1663
// 2) Register all languages provided by @codemirror/language-data
1764
// We convert extensions like [".js", ".mjs"] into a modelist pattern: "js|mjs"
18-
// and include anchored filename patterns like "^Dockerfile" when present.
65+
// and preserve aliases and filename regexes for languages like C++ and Dockerfile.
1966
for (const lang of languages as readonly LanguageDescription[]) {
2067
try {
2168
const name = String(lang?.name || "").trim();
2269
if (!name) continue;
2370

71+
const aliases = collectAliases(name, lang.alias);
72+
const modeId = getModeId(name, aliases);
2473
const parts: string[] = [];
74+
const filenameMatchers: RegExp[] = [];
75+
2576
// File extensions
2677
if (Array.isArray(lang.extensions)) {
2778
for (const e of lang.extensions) {
@@ -30,28 +81,37 @@ for (const lang of languages as readonly LanguageDescription[]) {
3081
if (cleaned) parts.push(cleaned);
3182
}
3283
}
33-
// Exact filenames (dockerfile, makefile, etc.)
84+
85+
// Exact filenames / filename regexes (Dockerfile, PKGBUILD, nginx*.conf, etc.)
3486
const filenames = Array.isArray(lang.filenames)
3587
? lang.filenames
3688
: lang.filename
3789
? [lang.filename]
3890
: [];
3991
for (const fn of filenames) {
40-
if (typeof fn !== "string") continue;
41-
const cleaned = fn.trim();
42-
if (cleaned) parts.push(`^${cleaned}`);
43-
}
92+
if (typeof fn === "string") {
93+
const cleaned = fn.trim();
94+
if (cleaned) {
95+
filenameMatchers.push(new RegExp(`^${escapeRegExp(cleaned)}$`, "i"));
96+
}
97+
continue;
98+
}
4499

45-
// Skip if we have no way to match the language
46-
if (parts.length === 0) continue;
100+
if (fn instanceof RegExp) {
101+
filenameMatchers.push(new RegExp(fn.source, fn.flags));
102+
}
103+
}
47104

48105
const pattern = parts.join("|");
49106

50107
// Wrap language-data loader as our modelist language provider
51108
// lang.load() returns a Promise<Extension>; we let the editor handle async loading
52109
const loader = typeof lang.load === "function" ? () => lang.load!() : null;
53110

54-
addMode(name, pattern, name, loader);
111+
addMode(modeId, pattern, name, loader, {
112+
aliases,
113+
filenameMatchers,
114+
});
55115
} catch (_) {
56116
// Ignore faulty entries to avoid breaking the whole registration
57117
}

0 commit comments

Comments
 (0)