1- import type { CstGrammar , RuleExpr } from './types.ts' ;
1+ import type { CstGrammar , RuleExpr , RuleDecl } from './types.ts' ;
22import { collectLiterals , isKeywordLiteral } from './grammar-utils.ts' ;
33
44interface TmPattern {
@@ -162,23 +162,62 @@ function inferIdentScope(keyword: string, scopeOverrides: Map<string, string[]>)
162162 if ( ! scope ) return null ;
163163 if ( scope . startsWith ( 'storage.type.function' ) ) return 'entity.name.function' ;
164164 if ( scope . startsWith ( 'storage.type.' ) && scope !== 'storage.type' ) return 'entity.name.type' ;
165+ // Heritage keyword (TextMate convention `*.extends`, e.g. `keyword.other.extends`
166+ // / `storage.modifier.extends`): the identifier it introduces names a superclass.
167+ // Scope-convention driven (like the storage.type.* mappings above), not keyed on
168+ // any specific word — a grammar that scopes its inheritance keyword `*.extends`
169+ // gets `entity.other.inherited-class` for the following identifier automatically.
170+ if ( / ( ^ | \. ) e x t e n d s $ / . test ( scope ) ) return 'entity.other.inherited-class' ;
165171 return null ;
166172}
167173
168- function findContextualPatterns ( expr : RuleExpr , tokenNames : Set < string > , scopeOverrides : Map < string , string [ ] > ) : ContextualPattern [ ] {
174+ /**
175+ * Does a rule, when expanded, have an alternative that begins with the given
176+ * identifier token? Used so a `keyword Ident` pattern is still detected when the
177+ * identifier is reached through a rule-ref (e.g. `extends ClassHeritage`, whose
178+ * base alternative is `Ident`). Bounded depth guards against ref cycles.
179+ */
180+ function ruleStartsWithIdent (
181+ refName : string ,
182+ identTokenName : string ,
183+ rules : RuleDecl [ ] ,
184+ seen : Set < string > = new Set ( ) ,
185+ ) : boolean {
186+ if ( refName === identTokenName ) return true ;
187+ if ( seen . has ( refName ) ) return false ;
188+ seen . add ( refName ) ;
189+ const rule = rules . find ( r => r . name === refName ) ;
190+ if ( ! rule ) return false ;
191+ for ( const alt of expandAlts ( rule . body ) ) {
192+ const head = alt [ 0 ] ;
193+ if ( ! head ) continue ;
194+ if ( head . type === 'ref' && ruleStartsWithIdent ( head . name , identTokenName , rules , seen ) ) return true ;
195+ }
196+ return false ;
197+ }
198+
199+ function findContextualPatterns (
200+ expr : RuleExpr ,
201+ tokenNames : Set < string > ,
202+ scopeOverrides : Map < string , string [ ] > ,
203+ rules : RuleDecl [ ] ,
204+ identTokenName : string | null ,
205+ ) : ContextualPattern [ ] {
169206 const patterns : ContextualPattern [ ] = [ ] ;
170207
171208 function walkSeq ( items : RuleExpr [ ] ) {
172209 for ( let i = 0 ; i < items . length - 1 ; i ++ ) {
173210 const a = items [ i ] ;
174211 const b = items [ i + 1 ] ;
175- if ( a . type === 'literal' && isKeywordLiteral ( a . value ) &&
176- b . type === 'ref' && tokenNames . has ( b . name ) ) {
177- const scope = inferIdentScope ( a . value , scopeOverrides ) ;
178- if ( scope ) {
179- patterns . push ( { keyword : a . value , identScope : scope } ) ;
180- }
181- }
212+ if ( a . type !== 'literal' || ! isKeywordLiteral ( a . value ) || b . type !== 'ref' ) continue ;
213+ const scope = inferIdentScope ( a . value , scopeOverrides ) ;
214+ if ( ! scope ) continue ;
215+ // Direct identifier-token adjacency, or the identifier reached through a
216+ // rule-ref whose base alternative starts with the identifier token.
217+ const adjacent =
218+ tokenNames . has ( b . name ) ||
219+ ( identTokenName !== null && ruleStartsWithIdent ( b . name , identTokenName , rules ) ) ;
220+ if ( adjacent ) patterns . push ( { keyword : a . value , identScope : scope } ) ;
182221 }
183222 }
184223
@@ -2026,7 +2065,7 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
20262065 // ── 4. Contextual patterns (keyword + Ident → entity.name.*) ──
20272066 const contextualPatterns : ContextualPattern [ ] = [ ] ;
20282067 for ( const rule of grammar . rules ) {
2029- contextualPatterns . push ( ...findContextualPatterns ( rule . body , tokenNames , scopeOverrides ) ) ;
2068+ contextualPatterns . push ( ...findContextualPatterns ( rule . body , tokenNames , scopeOverrides , grammar . rules , identToken ?. name ?? null ) ) ;
20302069 }
20312070
20322071 const seenContextual = new Set < string > ( ) ;
@@ -2048,6 +2087,68 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
20482087 topPatterns . push ( { include : `#${ key } ` } ) ;
20492088 }
20502089
2090+ // ── 4a. Import/export namespace `*` → constant.language.import-export-all ──
2091+ // A `*` directly after an import/export keyword (`import * as ns`, `export *`,
2092+ // `export * as ns`) names the whole module, not multiplication. Both the trigger
2093+ // keyword (scope `keyword.control.import`) and the `*` literal are read from the
2094+ // grammar/scope map; the rule fires only on the keyword→`*` adjacency that
2095+ // actually occurs in a rule, so an arithmetic `*` is never mis-scoped (import/
2096+ // export keywords are reserved and can never be a multiplication operand).
2097+ const importExportKws = new Set < string > ( ) ;
2098+ for ( const [ lit , scopes ] of scopeOverrides ) {
2099+ if ( scopes . some ( s => s . startsWith ( 'keyword.control.import' ) ) ) importExportKws . add ( lit ) ;
2100+ }
2101+ // Does a rule have an alternative whose first item is the `*` literal? (e.g.
2102+ // `import` → ImportClause, whose namespace branch begins `'*' 'as' Ident`.)
2103+ const ruleStartsWithStar = ( refName : string , seen : Set < string > = new Set ( ) ) : boolean => {
2104+ if ( seen . has ( refName ) ) return false ;
2105+ seen . add ( refName ) ;
2106+ const rule = grammar . rules . find ( r => r . name === refName ) ;
2107+ if ( ! rule ) return false ;
2108+ for ( const alt of expandAlts ( rule . body ) ) {
2109+ const head = alt [ 0 ] ;
2110+ if ( ! head ) continue ;
2111+ if ( head . type === 'literal' && head . value === '*' ) return true ;
2112+ if ( head . type === 'ref' && ruleStartsWithStar ( head . name , seen ) ) return true ;
2113+ }
2114+ return false ;
2115+ } ;
2116+ const starAllKws = new Set < string > ( ) ; // import/export keywords that introduce a namespace `*`
2117+ {
2118+ const walk = ( e : RuleExpr | undefined ) : void => {
2119+ if ( ! e ) return ;
2120+ for ( const alt of expandAlts ( e ) ) {
2121+ for ( let i = 0 ; i < alt . length - 1 ; i ++ ) {
2122+ const a = alt [ i ] , b = alt [ i + 1 ] ;
2123+ if ( a . type !== 'literal' || ! importExportKws . has ( a . value ) ) continue ;
2124+ // `*` directly after the keyword, or reached through the next rule-ref
2125+ // (e.g. `import ImportClause`, whose namespace branch starts with `*`).
2126+ if ( ( b . type === 'literal' && b . value === '*' ) || ( b . type === 'ref' && ruleStartsWithStar ( b . name ) ) ) {
2127+ starAllKws . add ( a . value ) ;
2128+ }
2129+ }
2130+ for ( const item of alt ) {
2131+ if ( item . type === 'quantifier' || item . type === 'group' ) walk ( item . body ) ;
2132+ else if ( item . type === 'sep' ) walk ( item . element ) ;
2133+ }
2134+ }
2135+ } ;
2136+ for ( const rule of grammar . rules ) walk ( rule . body ) ;
2137+ }
2138+ if ( starAllKws . size > 0 ) {
2139+ // Keyword keeps the scope it carries elsewhere (read from the scope map), so
2140+ // capture 1 is not hardcoded to a specific scope string.
2141+ const kwScope = getScope ( scopeOverrides , [ ...starAllKws ] [ 0 ] ) ?? 'keyword.control.import' ;
2142+ repository [ 'import-export-all' ] = {
2143+ match : `\\b(${ [ ...starAllKws ] . map ( escapeRegex ) . join ( '|' ) } )\\s+(\\*)` ,
2144+ captures : {
2145+ '1' : { name : `${ kwScope } .${ langName } ` } ,
2146+ '2' : { name : `constant.language.import-export-all.${ langName } ` } ,
2147+ } ,
2148+ } ;
2149+ topPatterns . push ( { include : '#import-export-all' } ) ;
2150+ }
2151+
20512152 // ── 4b. Function call detection ──
20522153 const hasCallExpr = detectCallExpression ( grammar ) ;
20532154 if ( hasCallExpr ) {
@@ -2706,6 +2807,9 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
27062807 if ( key === 'arrow-function-params' ) return 1.95 ;
27072808 if ( key === 'ternary-expression' ) return 1.97 ;
27082809 if ( key . endsWith ( '-declaration' ) || key . endsWith ( '-definition' ) || key . endsWith ( '-typekw' ) ) return 2 ;
2810+ // import/export namespace `*` must beat both the import/export keyword group
2811+ // (which would consume the keyword alone) and the arithmetic-operator match.
2812+ if ( key === 'import-export-all' ) return 2 ;
27092813 if ( scope . includes ( 'constant.numeric' ) ) return 3 ; // stable sort preserves DSL token order
27102814 if ( scope . includes ( 'keyword.operator' ) && key . startsWith ( 'scope-' ) ) return 4 ;
27112815 if ( scope . includes ( 'keyword.control' ) ) return 5 ;
0 commit comments