@@ -3,6 +3,7 @@ import type {
33 RegexEngineString ,
44} from '@shikijs/types'
55import type { IOnigMatch } from '@shikijs/vscode-textmate'
6+ import type { JavaScriptEngineString } from './string'
67
78const MAX = 4294967295
89
@@ -27,9 +28,48 @@ export interface JavaScriptRegexScannerOptions {
2728 regexConstructor ?: ( pattern : string ) => RegExp
2829}
2930
31+ interface LastSearch {
32+ strId : number
33+ searchedFrom : number
34+ matchIndex : number // -1 for no match
35+ indices : RegExpIndicesArray | null
36+ }
37+
38+ // Sticky regexps and `EmulatedRegExp`s with a search strategy (how `oniguruma-to-es` emulates `\G`)
39+ // depend on the exact start position, so their searches can't be reused
40+ function isSearchCacheable ( regexp : RegExp ) : boolean {
41+ return ! regexp . sticky && ! ( regexp as { rawOptions ?: { strategy ?: string | null } } ) . rawOptions ?. strategy
42+ }
43+
44+ function toResult ( index : number , indices : RegExpIndicesArray ) : IOnigMatch {
45+ return {
46+ index,
47+ captureIndices : indices . map ( ( indice ) => {
48+ if ( indice == null ) {
49+ return {
50+ start : MAX ,
51+ end : MAX ,
52+ length : 0 ,
53+ }
54+ }
55+ return {
56+ start : indice [ 0 ] ,
57+ end : indice [ 1 ] ,
58+ length : indice [ 1 ] - indice [ 0 ] ,
59+ }
60+ } ) ,
61+ }
62+ }
63+
3064export class JavaScriptScanner implements PatternScanner {
3165 regexps : ( RegExp | null ) [ ]
3266
67+ /**
68+ * Last search of each regexp on the current string, reused the way `vscode-oniguruma` does.
69+ * `null` for regexps whose searches can't be reused.
70+ */
71+ private lastSearches : ( LastSearch | null ) [ ]
72+
3373 constructor (
3474 public patterns : ( string | RegExp ) [ ] ,
3575 public options : JavaScriptRegexScannerOptions = { } ,
@@ -71,51 +111,66 @@ export class JavaScriptScanner implements PatternScanner {
71111 throw e
72112 }
73113 } )
114+
115+ this . lastSearches = this . regexps . map ( regexp => regexp && isSearchCacheable ( regexp )
116+ ? { strId : 0 , searchedFrom : 0 , matchIndex : - 1 , indices : null }
117+ : null ,
118+ )
74119 }
75120
76121 findNextMatchSync ( string : string | RegexEngineString , startPosition : number , _options : number ) : IOnigMatch | null {
77122 const str = typeof string === 'string'
78123 ? string
79124 : string . content
80- const pending : [ index : number , match : RegExpExecArray , offset : number ] [ ] = [ ]
125+ // No id (plain string or a string from another engine): never reuse
126+ const strId = typeof string === 'string'
127+ ? 0
128+ : ( string as Partial < JavaScriptEngineString > ) . id || 0
81129
82- function toResult ( index : number , match : RegExpExecArray , offset = 0 ) : IOnigMatch {
83- return {
84- index,
85- captureIndices : match . indices ! . map ( ( indice ) => {
86- if ( indice == null ) {
87- return {
88- start : MAX ,
89- end : MAX ,
90- length : 0 ,
91- }
92- }
93- return {
94- start : indice [ 0 ] + offset ,
95- end : indice [ 1 ] + offset ,
96- length : indice [ 1 ] - indice [ 0 ] ,
97- }
98- } ) ,
99- }
100- }
130+ let bestIndex = - 1
131+ let bestMatchIndex = 0
132+ let bestIndices : RegExpIndicesArray | null = null
101133
102134 for ( let i = 0 ; i < this . regexps . length ; i ++ ) {
103135 const regexp = this . regexps [ i ]
104136 if ( ! regexp )
105137 continue
106138 try {
107- regexp . lastIndex = startPosition
108- const match = regexp . exec ( str )
139+ const last = strId ? this . lastSearches [ i ] : null
140+ let matchIndex : number
141+ let indices : RegExpIndicesArray | null
142+
143+ // Still valid if it found nothing, or a match at or after the start position
144+ if ( last && last . strId === strId && last . searchedFrom <= startPosition && ( last . matchIndex === - 1 || last . matchIndex >= startPosition ) ) {
145+ matchIndex = last . matchIndex
146+ indices = last . indices
147+ }
148+ else {
149+ regexp . lastIndex = startPosition
150+ const match = regexp . exec ( str )
151+ matchIndex = match ? match . index : - 1
152+ indices = match ? match . indices ! : null
153+ if ( last ) {
154+ last . strId = strId
155+ last . searchedFrom = startPosition
156+ last . matchIndex = matchIndex
157+ last . indices = indices
158+ }
159+ }
109160
110- if ( ! match )
161+ if ( matchIndex === - 1 )
111162 continue
112163
113164 // If the match is at the start position, return it immediately
114- if ( match . index === startPosition ) {
115- return toResult ( i , match , 0 )
165+ if ( matchIndex === startPosition ) {
166+ return toResult ( i , indices ! )
167+ }
168+ // Otherwise, keep the closest one
169+ if ( bestIndex === - 1 || matchIndex < bestMatchIndex ) {
170+ bestIndex = i
171+ bestMatchIndex = matchIndex
172+ bestIndices = indices
116173 }
117- // Otherwise, store it for later
118- pending . push ( [ i , match , 0 ] )
119174 }
120175 catch ( e ) {
121176 if ( this . options . forgiving )
@@ -124,16 +179,8 @@ export class JavaScriptScanner implements PatternScanner {
124179 }
125180 }
126181
127- // Find the closest match to the start position
128- if ( pending . length ) {
129- const minIndex = Math . min ( ...pending . map ( m => m [ 1 ] . index ) )
130- for ( const [ i , match , offset ] of pending ) {
131- if ( match . index === minIndex ) {
132- return toResult ( i , match , offset )
133- }
134- }
135- }
136-
137- return null
182+ return bestIndex === - 1
183+ ? null
184+ : toResult ( bestIndex , bestIndices ! )
138185 }
139186}
0 commit comments