Skip to content

Commit f2ac88a

Browse files
committed
refactor: migrate CodeMirror mode management to TypeScript
1 parent aaf2989 commit f2ac88a

File tree

2 files changed

+54
-48
lines changed

2 files changed

+54
-48
lines changed
Lines changed: 42 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
1-
const modesByName = {};
2-
const modes = [];
1+
import type { Extension } from "@codemirror/state";
2+
3+
export type LanguageExtensionProvider = () => Extension | Promise<Extension>;
4+
5+
export interface ModesByName {
6+
[name: string]: Mode;
7+
}
8+
9+
const modesByName: ModesByName = {};
10+
const modes: Mode[] = [];
311

412
/**
513
* Initialize CodeMirror mode list functionality
614
*/
7-
export function initModes() {
15+
export function initModes(): void {
816
// CodeMirror modes don't need the same ace.define wrapper
917
// but we maintain the same API structure for compatibility
1018
}
1119

1220
/**
1321
* Add language mode to CodeMirror editor
14-
* @param {string} name name of the mode
15-
* @param {string|Array<string>} extensions extensions of the mode
16-
* @param {string} [caption] display name of the mode
17-
* @param {Function} [languageExtension] CodeMirror language provider function.
18-
* This function may return an Extension synchronously or a Promise resolving
19-
* to an Extension.
2022
*/
21-
export function addMode(name, extensions, caption, languageExtension = null) {
23+
export function addMode(
24+
name: string,
25+
extensions: string | string[],
26+
caption?: string,
27+
languageExtension: LanguageExtensionProvider | null = null,
28+
): void {
2229
const filename = name.toLowerCase();
2330
const mode = new Mode(filename, caption, extensions, languageExtension);
2431
modesByName[filename] = mode;
@@ -27,9 +34,8 @@ export function addMode(name, extensions, caption, languageExtension = null) {
2734

2835
/**
2936
* Remove language mode from CodeMirror editor
30-
* @param {string} name
3137
*/
32-
export function removeMode(name) {
38+
export function removeMode(name: string): void {
3339
const filename = name.toLowerCase();
3440
delete modesByName[filename];
3541
const modeIndex = modes.findIndex((mode) => mode.name === filename);
@@ -40,12 +46,10 @@ export function removeMode(name) {
4046

4147
/**
4248
* Get mode for file path
43-
* @param {string} path
44-
* @returns {Mode}
4549
*/
46-
export function getModeForPath(path) {
50+
export function getModeForPath(path: string): Mode {
4751
let mode = modesByName.text;
48-
let fileName = path.split(/[\/\\]/).pop();
52+
const fileName = path.split(/[/\\]/).pop() || "";
4953

5054
// Sort modes by specificity (descending) to check most specific first
5155
const sortedModes = [...modes].sort((a, b) => {
@@ -67,7 +71,7 @@ export function getModeForPath(path) {
6771
* - Anchored patterns (e.g., "^Dockerfile") get a base score of 1000.
6872
* - Non-anchored patterns (extensions) are scored by length.
6973
*/
70-
function getModeSpecificityScore(modeInstance) {
74+
function getModeSpecificityScore(modeInstance: Mode): number {
7175
const extensionsStr = modeInstance.extensions;
7276
if (!extensionsStr) return 0;
7377

@@ -92,36 +96,32 @@ function getModeSpecificityScore(modeInstance) {
9296

9397
/**
9498
* Get all modes by name
95-
* @returns {Object}
9699
*/
97-
export function getModesByName() {
100+
export function getModesByName(): ModesByName {
98101
return modesByName;
99102
}
100103

101104
/**
102105
* Get all modes array
103-
* @returns {Array}
104106
*/
105-
export function getModes() {
107+
export function getModes(): Mode[] {
106108
return modes;
107109
}
108110

109-
class Mode {
110-
extensions;
111-
displayName;
112-
name;
113-
mode;
114-
extRe;
115-
languageExtension;
116-
117-
/**
118-
* Create a new mode
119-
* @param {string} name
120-
* @param {string} caption
121-
* @param {string|Array<string>} extensions
122-
* @param {Function} languageExtension - CodeMirror language extension function
123-
*/
124-
constructor(name, caption, extensions, languageExtension = null) {
111+
export class Mode {
112+
extensions: string;
113+
caption: string;
114+
name: string;
115+
mode: string;
116+
extRe: RegExp;
117+
languageExtension: LanguageExtensionProvider | null;
118+
119+
constructor(
120+
name: string,
121+
caption: string | undefined,
122+
extensions: string | string[],
123+
languageExtension: LanguageExtensionProvider | null = null,
124+
) {
125125
if (Array.isArray(extensions)) {
126126
extensions = extensions.join("|");
127127
}
@@ -131,11 +131,11 @@ class Mode {
131131
this.extensions = extensions;
132132
this.caption = caption || this.name.replace(/_/g, " ");
133133
this.languageExtension = languageExtension;
134-
let re;
134+
let re: string;
135135

136136
if (/\^/.test(extensions)) {
137137
re =
138-
extensions.replace(/\|(\^)?/g, function (a, b) {
138+
extensions.replace(/\|(\^)?/g, function (_a: string, b: string) {
139139
return "$|" + (b ? "^" : "^.*\\.");
140140
}) + "$";
141141
} else {
@@ -145,23 +145,21 @@ class Mode {
145145
this.extRe = new RegExp(re, "i");
146146
}
147147

148-
supportsFile(filename) {
148+
supportsFile(filename: string): boolean {
149149
return this.extRe.test(filename);
150150
}
151151

152152
/**
153153
* Get the CodeMirror language extension
154-
* @returns {Function|null} The language provider function or null if not available
155154
*/
156-
getExtension() {
155+
getExtension(): LanguageExtensionProvider | null {
157156
return this.languageExtension;
158157
}
159158

160159
/**
161160
* Check if the language extension is available (loaded)
162-
* @returns {boolean}
163161
*/
164-
isAvailable() {
162+
isAvailable(): boolean {
165163
return this.languageExtension !== null;
166164
}
167165
}
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
import { languages } from "@codemirror/language-data";
2+
import type { Extension } from "@codemirror/state";
23
import { addMode } from "./modelist";
34

5+
interface LanguageDescription {
6+
name?: string;
7+
extensions?: readonly string[];
8+
filenames?: readonly string[];
9+
filename?: string;
10+
load?: () => Promise<Extension>;
11+
}
12+
413
// 1) Always register a plain text fallback
514
addMode("Text", "txt|text|log|plain", "Plain Text", () => []);
615

716
// 2) Register all languages provided by @codemirror/language-data
817
// We convert extensions like [".js", ".mjs"] into a modelist pattern: "js|mjs"
918
// and include anchored filename patterns like "^Dockerfile" when present.
10-
for (const lang of languages) {
19+
for (const lang of languages as readonly LanguageDescription[]) {
1120
try {
1221
const name = String(lang?.name || "").trim();
1322
if (!name) continue;
1423

15-
/** @type {string[]} */
16-
const parts = [];
24+
const parts: string[] = [];
1725
// File extensions
1826
if (Array.isArray(lang.extensions)) {
1927
for (const e of lang.extensions) {
@@ -41,7 +49,7 @@ for (const lang of languages) {
4149

4250
// Wrap language-data loader as our modelist language provider
4351
// lang.load() returns a Promise<Extension>; we let the editor handle async loading
44-
const loader = typeof lang.load === "function" ? () => lang.load() : null;
52+
const loader = typeof lang.load === "function" ? () => lang.load!() : null;
4553

4654
addMode(name, pattern, name, loader);
4755
} catch (_) {

0 commit comments

Comments
 (0)