@@ -2,13 +2,38 @@ import type { Extension } from "@codemirror/state";
22
33export type LanguageExtensionProvider = ( ) => Extension | Promise < Extension > ;
44
5+ export interface AddModeOptions {
6+ aliases ?: string [ ] ;
7+ filenameMatchers ?: RegExp [ ] ;
8+ }
9+
510export interface ModesByName {
611 [ name : string ] : Mode ;
712}
813
914const modesByName : ModesByName = { } ;
1015const 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 */
3875export 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 */
74122function 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+
111171export 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