@@ -833,6 +833,14 @@ function buildOperandStartClass(grammar: CstGrammar, identToken: TokenDecl | und
833833 return `[[:alpha:][:digit:]${ cls } ]` ;
834834}
835835
836+ function notAfterValueWithOptionalWhitespace ( valueCharClass : string , maxWhitespace = 16 ) : string {
837+ const assertions : string [ ] = [ ] ;
838+ for ( let spaces = 0 ; spaces <= maxWhitespace ; spaces ++ ) {
839+ assertions . push ( `(?<![${ valueCharClass } ]${ '\\s' . repeat ( spaces ) } )` ) ;
840+ }
841+ return assertions . join ( '' ) ;
842+ }
843+
836844// ── JSX detection ──
837845
838846interface JsxInfo {
@@ -992,6 +1000,7 @@ interface JsxDisambigDelims {
9921000 topTypeParam : string ; // "is a type-param list" body: top-level comma OR constraint keyword
9931001 balancedAngles : string ; // recursive balanced `<…>` named group `(?<B>…)`
9941002 arrowParamShape : string ; // the arrow-shaped `(` confirm after `>`
1003+ close : string ; // the generic close delimiter (`>` for TS/TSX)
9951004 // Lookbehind body asserting the `>` just left closes a type-param LIST that carried a
9961005 // top-level comma or constraint keyword (`<T,>`, `<T extends X>`) — i.e. the SAME
9971006 // generic-arrow disambiguation signal as `topTypeParam`, but for matching a `(` that
@@ -1126,7 +1135,7 @@ function jsxDisambigDelims(grammar: CstGrammar, identRegex: string, separator: s
11261135 ? `|${ skip } \\b(?:${ constraintKeywords . map ( escapeRegex ) . join ( '|' ) } )\\b`
11271136 : '' ;
11281137 const typeParamCloseBehind = `${ escapeRegex ( open ) } (?:${ topComma } ${ behindKw } )${ skip } ${ escapeRegex ( close ) } ` ;
1129- return { topComma, topTypeParam, balancedAngles, arrowParamShape, typeParamCloseBehind } ;
1138+ return { topComma, topTypeParam, balancedAngles, arrowParamShape, close , typeParamCloseBehind } ;
11301139}
11311140
11321141/**
@@ -1916,12 +1925,12 @@ function generateTypeCastPattern(
19161925 const tpEnd = `punctuation.definition.typeparameters.end.${ langName } ` ;
19171926 // `<` only at expression-start. A prefix cast's `<` is never preceded by a value
19181927 // OPERAND; a comparison's `<` always is (`a < b`). Reject the cast when `<` is
1919- // preceded — across any whitespace — by an operand-ending char: an identifier
1928+ // preceded — across bounded whitespace — by an operand-ending char: an identifier
19201929 // char, `)`, `]`, a numeric/quote tail. This keeps `a < b > c`, `f() < g`,
1921- // `x] < y` as comparisons (variable-length lookbehind; Oniguruma supports it) .
1930+ // `x] < y` as comparisons while staying compatible with TextMate 2.0 Onigmo .
19221931 // Casts after a keyword that ends in a letter (`return <T>x`) stay a comparison
19231932 // here — rare, and never a regression (they were unhighlighted before too).
1924- const notAfter = `(?<![ \\w$)\\]]\\s*)` ;
1933+ const notAfter = notAfterValueWithOptionalWhitespace ( ' \\w$)\\]' ) ;
19251934 // Type-shaped, balanced-angle inner content (kept to type characters so an
19261935 // ordinary `a < b > c` comparison — whose operands are arbitrary expressions —
19271936 // is not swallowed). `\g<TC>` recurses for nested generics like `<Array<T>>`.
@@ -2445,26 +2454,7 @@ function generateRegexLiteralPatterns(
24452454 // Also match at start of line
24462455 const startOfLine = '(?<=^)' ;
24472456
2448- // Ambiguous postfix/prefix op chars (TS `!`): a `/` may follow one ONLY when the op-run is
2449- // the PREFIX form — i.e. the run is itself in a regex-start position (`= !/re/`, `!!/re/`,
2450- // `return !/x/`), NOT the postfix non-null form (`x! / y` → division). We can't decide that
2451- // from the single char before `/` (it's the op either way), so look back PAST the op-run and
2452- // re-apply the same regex-start test there. The inner context is the SAME char-class +
2453- // keywords + line-start used above, but un-wrapped (it sits inside this lookbehind), and the
2454- // op-run is `[ops](?:\s*[ops])*` (chained `!!` allowed). Because these chars were excluded
2455- // from `charLookbehind`, a postfix op (preceded by a value) matches NONE of the alternatives
2456- // → the `/` falls through to the division operator.
2457- const innerCtx = [
2458- charEsc ? `[${ charEsc } ]` : null ,
2459- ...info . preceedingKeywords . map ( kw => `\\b${ escapeRegex ( kw ) } ` ) ,
2460- '^' ,
2461- ] . filter ( Boolean ) . join ( '|' ) ;
2462- const opRun = info . postfixAmbiguousChars . map ( escapeRegex ) . join ( '' ) ;
2463- const postfixBangLookbehind = opRun
2464- ? `(?<=(?:${ innerCtx } )\\s*[${ opRun } ](?:\\s*[${ opRun } ])*)`
2465- : '' ;
2466-
2467- const lbAlts = [ charLookbehind , keywordLookbehinds , postfixBangLookbehind , startOfLine ]
2457+ const lbAlts = [ charLookbehind , keywordLookbehinds , startOfLine ]
24682458 . filter ( Boolean ) . join ( '|' ) ;
24692459 const fullLookbehind = `(?:${ lbAlts } )` ;
24702460
@@ -2491,6 +2481,31 @@ function generateRegexLiteralPatterns(
24912481 } ;
24922482 if ( commentBody ) beginCaptures [ '1' ] = { name : `comment.block.${ langName } ` } ;
24932483
2484+ // Ambiguous postfix/prefix op chars (TS `!`): a `/` may follow one ONLY when the op-run is
2485+ // the PREFIX form (`= !/re/`, `return !!/x/`), not postfix non-null (`x! / y`). TextMate 2.0's
2486+ // Onigmo rejects the old variable-length lookbehind that looked past the whole op-run, so this
2487+ // separate pattern anchors on the fixed-width expression-start context and consumes the op-run.
2488+ const prefixOpClass = info . postfixAmbiguousChars . map ( escapeForCharClass ) . join ( '' ) ;
2489+ if ( prefixOpClass ) {
2490+ const prefixSlashGroup = commentBody ? '3' : '2' ;
2491+ const prefixCaptures : Record < string , { name : string } > = {
2492+ '1' : { name : `keyword.operator.logical.prefix.${ langName } ` } ,
2493+ [ prefixSlashGroup ] : { name : `punctuation.definition.string.begin.regexp.${ langName } ` } ,
2494+ } ;
2495+ if ( commentBody ) prefixCaptures [ '2' ] = { name : `comment.block.${ langName } ` } ;
2496+ result [ 'regex-literal-prefix-ops' ] = {
2497+ name : `string.regexp.${ langName } ` ,
2498+ begin : `${ fullLookbehind } \s*([${ prefixOpClass } ](?:\s*[${ prefixOpClass } ])*)\s*${ commentPrefix } (/)${ commentExclude } ` ,
2499+ beginCaptures : prefixCaptures ,
2500+ end : `(/)(${ info . flagsPattern } )` ,
2501+ endCaptures : {
2502+ '1' : { name : `punctuation.definition.string.end.regexp.${ langName } ` } ,
2503+ '2' : { name : `keyword.other.regexp.${ langName } ` } ,
2504+ } ,
2505+ patterns : [ { include : '#regexp' } ] ,
2506+ } ;
2507+ }
2508+
24942509 result [ 'regex-literal' ] = {
24952510 name : `string.regexp.${ langName } ` ,
24962511 begin : `${ fullLookbehind } \\s*${ commentPrefix } (/)${ commentExclude } ` ,
@@ -4382,6 +4397,7 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
43824397 for ( const [ key , pattern ] of Object . entries ( rlPatterns ) ) {
43834398 repository [ key ] = pattern ;
43844399 }
4400+ if ( rlPatterns [ 'regex-literal-prefix-ops' ] ) topPatterns . push ( { include : '#regex-literal-prefix-ops' } ) ;
43854401 topPatterns . push ( { include : '#regex-literal' } ) ;
43864402 }
43874403
@@ -4937,7 +4953,7 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
49374953 // reference it via `{ include: '#type-inner' }`. No shared mutable array;
49384954 // later injections rebuild the patterns array non-destructively.
49394955 // Type operators are derived from @type rule literals.
4940- const typeInnerPats : ( TmPattern | { include : string } ) [ ] = [
4956+ const typeInnerPats : ( TmPattern | { include : string } ) [ ] = hasTypeAnnotations ? [
49414957 ...( repository [ 'generic-type' ] ? [ { include : '#generic-type' } ] : [ ] ) ,
49424958 ...( repository [ 'type-object-type' ] ? [ { include : '#type-object-type' } ] : [ ] ) ,
49434959 ...( repository [ 'type-paren' ] ? [ { include : '#type-paren' } ] : [ ] ) ,
@@ -4947,7 +4963,7 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
49474963 // swallowed by the surrounding type region's name.
49484964 ...literalTypeIncludes ,
49494965 { include : '#simple-type' } ,
4950- ] ;
4966+ ] : [ ] ;
49514967 // Union/intersection operators — only if present in @type rules
49524968 const typeUnionOps = [ '|' , '&' ] . filter ( op => typeLiterals . has ( op ) ) ;
49534969 if ( typeUnionOps . length > 0 ) {
@@ -5028,7 +5044,7 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
50285044 typeInnerPats . splice ( idx === - 1 ? typeInnerPats . length : idx , 0 , { include : '#type-conditional' } ) ;
50295045 }
50305046
5031- repository [ 'type-inner' ] = { patterns : typeInnerPats } ;
5047+ if ( hasTypeAnnotations ) repository [ 'type-inner' ] = { patterns : typeInnerPats } ;
50325048
50335049 // Wire up deferred type-paren pattern (basic wiring; patched after type injections)
50345050 if ( repository [ 'type-paren' ] ) {
@@ -5405,7 +5421,7 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
54055421 if ( angleBracket && angleDisambig ) {
54065422 const balancedAngles = angleDisambig . balancedAngles ;
54075423 const arrowParamShape = angleDisambig . arrowParamShape ;
5408- const arrowPos = `(?:(?<=\\basync\\s)|(?<![ \\w$)\\]}]\\s*) )` ;
5424+ const arrowPos = `(?:(?<=\\basync\\s)|${ notAfterValueWithOptionalWhitespace ( ' \\w$)\\]}' ) } )` ;
54095425 // JSX-dialect disambiguator: in a `.tsx`/`.jsx` grammar a bare `<Foo>(…`
54105426 // is a JSX element, so a generic-arrow type-param list is only recognised
54115427 // when it carries a TOP-LEVEL comma inside the `<…>` (`<T,>`, `<T = X,>`,
@@ -6366,7 +6382,7 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
63666382 if ( angleBracket && angleDisambig ) {
63676383 repository [ 'arrow-function-params-generic' ] = {
63686384 name : `meta.parameters.arrow.${ langName } ` ,
6369- begin : `(?<=${ angleDisambig . typeParamCloseBehind } )\\s*(\\()\\s*$` ,
6385+ begin : `(?<=${ escapeRegex ( angleDisambig . close ) } )\\s*(\\()\\s*$` ,
63706386 beginCaptures : {
63716387 '1' : { name : `punctuation.definition.parameters.begin.${ langName } ` } ,
63726388 } ,
0 commit comments