@@ -20,6 +20,13 @@ export interface SpanMapSegment {
2020/** Internal segment representation after omitted features have been normalized to `All`. */
2121type NormalizedSpanMapSegment = SpanMapSegment & { readonly features : SpanMapFeature ; } ;
2222
23+ /** Lazily built interval index for original-to-virtual lookups. */
24+ interface OriginalIndex {
25+ readonly segments : readonly NormalizedSpanMapSegment [ ] ;
26+ readonly leafCount : number ;
27+ readonly maxEnds : readonly number [ ] ;
28+ }
29+
2330/** One virtual projection of an original position and its mapping fidelity. */
2431export interface MappedPosition {
2532 readonly position : number ;
@@ -35,7 +42,7 @@ export interface MappedRange {
3542/** Provides bidirectional span-aware mapping between virtual and original text. */
3643export class SpanMap {
3744 readonly segments : readonly NormalizedSpanMapSegment [ ] ;
38- private originalSegments : readonly NormalizedSpanMapSegment [ ] | undefined ;
45+ private originalIndex : OriginalIndex | undefined ;
3946
4047 /** Copies and sorts segments by virtual start, normalizing omitted features to `All`. */
4148 constructor ( segments : readonly SpanMapSegment [ ] ) {
@@ -90,7 +97,7 @@ export class SpanMap {
9097 * Results are ordered by virtual position; uncovered or disabled positions produce no results.
9198 */
9299 originalToVirtualPositions ( position : number , feature : SpanMapFeature ) : readonly MappedPosition [ ] {
93- const groups = segmentGroupsAtOriginalPosition ( this . getOriginalSegments ( ) , position ) ;
100+ const groups = segmentGroupsAtOriginalPosition ( this . getOriginalIndex ( ) , position ) ;
94101 const results : MappedPosition [ ] = [ ] ;
95102 for ( const group of groups ) {
96103 for ( const segment of group . segments ) {
@@ -108,7 +115,7 @@ export class SpanMap {
108115
109116 /**
110117 * Returns every feature-compatible virtual projection of an original range.
111- * A range contained by one duplicate group produces one exact or atom result per matching group member .
118+ * A range contained by one or more segments produces one exact or atom result per matching segment .
112119 *
113120 * A range that starts in one group and ends in another can have several possible virtual ranges. For
114121 * example, suppose two original segments are each copied twice into the virtual text:
@@ -131,16 +138,24 @@ export class SpanMap {
131138 originalToVirtualSpans ( range : ReadonlyTextRange , feature : SpanMapFeature ) : readonly MappedRange [ ] {
132139 const start = range . pos ;
133140 const end = Math . max ( range . end , start ) ;
134- const lastCharacter = end > start ? end - 1 : end ;
135- const originalSegments = this . getOriginalSegments ( ) ;
136- const startSegments = segmentsAtOriginalPosition ( originalSegments , start ) ;
137- const endSegments = segmentsAtOriginalPosition ( originalSegments , lastCharacter ) ;
141+ if ( start === end ) {
142+ return this . originalToVirtualPositions ( start , feature ) . map ( ( { position, fidelity } ) => ( {
143+ range : { pos : position , end : position } ,
144+ fidelity,
145+ } ) ) ;
146+ }
147+ const lastCharacter = end - 1 ;
148+ const originalIndex = this . getOriginalIndex ( ) ;
149+ const startSegments = segmentsAtOriginalPosition ( originalIndex , start ) ;
150+ const endSegments = segmentsAtOriginalPosition ( originalIndex , lastCharacter ) ;
138151 if ( ! startSegments || ! endSegments ) return [ ] ;
139- if ( sameOriginalRange ( startSegments [ 0 ] , endSegments [ 0 ] ) ) {
140- return originalToVirtualSpansInGroup ( startSegments , start , end , feature ) ;
152+ const containing = startSegments . filter ( segment => end <= segment . originalEnd ) ;
153+ if ( containing . length > 0 ) {
154+ const results = [ ...originalToVirtualSpansInSegments ( containing , start , end , feature ) ] ;
155+ if ( results . length > 0 ) return results . sort ( ( left , right ) => left . range . pos - right . range . pos ) ;
141156 }
142- const starts = originalStartProjections ( startSegments , start , feature ) ;
143- const ends = originalEndProjections ( endSegments , end , feature ) ;
157+ const starts = [ ... originalStartProjections ( startSegments , start , feature ) ] . sort ( ( left , right ) => left - right ) ;
158+ const ends = [ ... originalEndProjections ( endSegments , end , feature ) ] . sort ( ( left , right ) => left - right ) ;
144159 if ( starts . length === 0 || ends . length === 0 ) return [ ] ;
145160 return starts . flatMap ( ( virtualStart , index ) => {
146161 const virtualEnd = ends . find ( end => end >= virtualStart ) ;
@@ -154,8 +169,12 @@ export class SpanMap {
154169 private mapRange ( range : ReadonlyTextRange , segments : readonly SpanMapSegment [ ] , reverse : boolean ) : MappedRange {
155170 const start = range . pos ;
156171 const end = Math . max ( range . end , start ) ;
172+ if ( start === end ) {
173+ const { position, fidelity } = this . mapPoint ( start , segments , reverse ) ;
174+ return { range : { pos : position , end : position } , fidelity } ;
175+ }
157176 const [ startIndex , startInside ] = segmentIndexAt ( segments , start , reverse ) ;
158- const endProbe = end > start ? end - 1 : end ;
177+ const endProbe = end - 1 ;
159178 const [ endIndex , endInside ] = segmentIndexAt ( segments , endProbe , reverse ) ;
160179
161180 if ( startIndex === endIndex && startInside === endInside ) {
@@ -193,13 +212,20 @@ export class SpanMap {
193212 } ;
194213 }
195214
196- /** Returns the lazily built segment index ordered by original start. */
197- private getOriginalSegments ( ) : readonly NormalizedSpanMapSegment [ ] {
198- return this . originalSegments ??= [ ...this . segments ] . sort ( ( left , right ) =>
215+ /** Returns the lazily built original-text interval index. */
216+ private getOriginalIndex ( ) : OriginalIndex {
217+ if ( this . originalIndex ) return this . originalIndex ;
218+ const segments = [ ...this . segments ] . sort ( ( left , right ) =>
199219 left . originalStart - right . originalStart
200220 || left . originalEnd - right . originalEnd
201221 || left . virtualStart - right . virtualStart
202222 ) ;
223+ let leafCount = 1 ;
224+ while ( leafCount < segments . length ) leafCount *= 2 ;
225+ const maxEnds = new Array < number > ( 2 * leafCount ) . fill ( 0 ) ;
226+ for ( let i = 0 ; i < segments . length ; i ++ ) maxEnds [ leafCount + i ] = segments [ i ] . originalEnd ;
227+ for ( let i = leafCount - 1 ; i > 0 ; i -- ) maxEnds [ i ] = Math . max ( maxEnds [ 2 * i ] , maxEnds [ 2 * i + 1 ] ) ;
228+ return this . originalIndex = { segments, leafCount, maxEnds } ;
203229 }
204230
205231 private virtualRangeSupportsFeature ( range : ReadonlyTextRange , feature : SpanMapFeature ) : boolean {
@@ -269,8 +295,8 @@ function originalEndProjections(segments: readonly NormalizedSpanMapSegment[], e
269295 ) ;
270296}
271297
272- /** Maps a range whose boundaries are known to lie in one duplicate group . */
273- function originalToVirtualSpansInGroup ( segments : readonly NormalizedSpanMapSegment [ ] , start : number , end : number , feature : SpanMapFeature ) : readonly MappedRange [ ] {
298+ /** Maps a range fully contained by each segment . */
299+ function originalToVirtualSpansInSegments ( segments : readonly NormalizedSpanMapSegment [ ] , start : number , end : number , feature : SpanMapFeature ) : readonly MappedRange [ ] {
274300 return segments
275301 . filter ( segment => supportsFeature ( segment , feature ) )
276302 . map ( segment => {
@@ -289,30 +315,70 @@ function sameOriginalRange(left: SpanMapSegment, right: SpanMapSegment): boolean
289315}
290316
291317/**
292- * Returns the complete duplicate group of mapping segments containing the original-text `position`.
293- * Segment ends are exclusive; starts, including zero-length segment starts, are included. It finds a candidate
294- * in O(log n), then scans only the duplicate group. `segments` must be ordered by original start, original end,
295- * and virtual start.
318+ * Returns every mapping segment containing the original-text `position`.
319+ * Segment ends are exclusive; starts, including zero-length segment starts, are included.
296320 */
297- function segmentsAtOriginalPosition ( segments : readonly NormalizedSpanMapSegment [ ] , position : number ) : readonly NormalizedSpanMapSegment [ ] | undefined {
321+ function segmentsAtOriginalPosition ( index : OriginalIndex , position : number ) : readonly NormalizedSpanMapSegment [ ] | undefined {
322+ // Query intervals that contain position strictly before their exclusive end. Segments starting exactly at
323+ // position are appended separately so zero-length segments are included while maxEnd <= position is pruned.
324+ const start = firstOriginalSegmentAtOrAfter ( index . segments , position ) ;
325+ const results = segmentsEndingAtOrAfter ( index , start , position , false ) ;
326+ const end = firstOriginalSegmentAfter ( index . segments , position ) ;
327+ results . push ( ...index . segments . slice ( start , end ) ) ;
328+ return results . length > 0 ? results : undefined ;
329+ }
330+
331+ /** Returns segments among `[0, limit)` whose original end reaches `position`. */
332+ function segmentsEndingAtOrAfter ( index : OriginalIndex , limit : number , position : number , includeEnd : boolean ) : NormalizedSpanMapSegment [ ] {
333+ const results : NormalizedSpanMapSegment [ ] = [ ] ;
334+ collectSegmentsEndingAtOrAfter ( index , 1 , 0 , index . leafCount , limit , position , includeEnd , results ) ;
335+ return results ;
336+ }
337+
338+ /** Walks the flat max-end tree left-to-right, preserving original-text order. */
339+ function collectSegmentsEndingAtOrAfter (
340+ index : OriginalIndex ,
341+ node : number ,
342+ start : number ,
343+ end : number ,
344+ limit : number ,
345+ position : number ,
346+ includeEnd : boolean ,
347+ results : NormalizedSpanMapSegment [ ] ,
348+ ) : void {
349+ const maxEnd = index . maxEnds [ node ] ;
350+ if ( start >= limit || maxEnd < position || ! includeEnd && maxEnd === position ) return ;
351+ if ( end - start === 1 ) {
352+ results . push ( index . segments [ start ] ) ;
353+ return ;
354+ }
355+ const middle = start + ( ( end - start ) >>> 1 ) ;
356+ collectSegmentsEndingAtOrAfter ( index , 2 * node , start , middle , limit , position , includeEnd , results ) ;
357+ collectSegmentsEndingAtOrAfter ( index , 2 * node + 1 , middle , end , limit , position , includeEnd , results ) ;
358+ }
359+
360+ /** Returns the first original-ordered segment whose start is greater than or equal to `position`. */
361+ function firstOriginalSegmentAtOrAfter ( segments : readonly NormalizedSpanMapSegment [ ] , position : number ) : number {
298362 let low = 0 ;
299363 let high = segments . length ;
300364 while ( low < high ) {
301365 const middle = ( low + high ) >>> 1 ;
302366 if ( segments [ middle ] . originalStart < position ) low = middle + 1 ;
303367 else high = middle ;
304368 }
305- let index = low < segments . length && segments [ low ] . originalStart === position ? low : low - 1 ;
306- if (
307- index < 0 || ! (
308- segments [ index ] . originalStart === position
309- || position < segments [ index ] . originalEnd
310- )
311- ) return undefined ;
312- while ( index > 0 && sameOriginalRange ( segments [ index - 1 ] , segments [ index ] ) ) index -- ;
313- let end = index + 1 ;
314- while ( end < segments . length && sameOriginalRange ( segments [ end ] , segments [ index ] ) ) end ++ ;
315- return segments . slice ( index , end ) ;
369+ return low ;
370+ }
371+
372+ /** Returns the first original-ordered segment whose start is greater than `position`. */
373+ function firstOriginalSegmentAfter ( segments : readonly NormalizedSpanMapSegment [ ] , position : number ) : number {
374+ let low = 0 ;
375+ let high = segments . length ;
376+ while ( low < high ) {
377+ const middle = ( low + high ) >>> 1 ;
378+ if ( segments [ middle ] . originalStart <= position ) low = middle + 1 ;
379+ else high = middle ;
380+ }
381+ return low ;
316382}
317383
318384interface SegmentGroupAtOriginalPosition {
@@ -321,8 +387,8 @@ interface SegmentGroupAtOriginalPosition {
321387}
322388
323389/**
324- * Returns groups of mapping segments containing or touching the original-text `position`.
325- * At a shared boundary, segments ending at the point and segments starting there form separate groups :
390+ * Returns every group of equal-range mapping segments containing or touching the original-text `position`.
391+ * Segment ends are included for point mapping :
326392 *
327393 * ```text
328394 * original: [--- A ---)[--- B ---)
@@ -333,31 +399,23 @@ interface SegmentGroupAtOriginalPosition {
333399 * atEnd: true atEnd: false
334400 * ```
335401 */
336- function segmentGroupsAtOriginalPosition ( segments : readonly NormalizedSpanMapSegment [ ] , position : number ) : readonly SegmentGroupAtOriginalPosition [ ] {
337- let low = 0 ;
338- let high = segments . length ;
339- while ( low < high ) {
340- const middle = ( low + high ) >>> 1 ;
341- if ( segments [ middle ] . originalStart < position ) low = middle + 1 ;
342- else high = middle ;
343- }
344- if ( low < segments . length && segments [ low ] . originalStart === position ) {
345- const right = segmentsAtOriginalPosition ( segments , position ) ! ;
346- const groups : SegmentGroupAtOriginalPosition [ ] = [ ] ;
347- if ( low > 0 && segments [ low - 1 ] . originalEnd === position ) {
348- let leftStart = low - 1 ;
349- while ( leftStart > 0 && sameOriginalRange ( segments [ leftStart - 1 ] , segments [ low - 1 ] ) ) leftStart -- ;
350- groups . push ( { segments : segments . slice ( leftStart , low ) , atEnd : true } ) ;
402+ function segmentGroupsAtOriginalPosition ( index : OriginalIndex , position : number ) : readonly SegmentGroupAtOriginalPosition [ ] {
403+ const limit = firstOriginalSegmentAfter ( index . segments , position ) ;
404+ const segments = segmentsEndingAtOrAfter ( index , limit , position , true ) ;
405+ const groups : SegmentGroupAtOriginalPosition [ ] = [ ] ;
406+ for ( let start = 0 ; start < segments . length ; ) {
407+ let end = start + 1 ;
408+ while ( end < segments . length && sameOriginalRange ( segments [ start ] , segments [ end ] ) ) end ++ ;
409+ const segment = segments [ start ] ;
410+ if ( position <= segment . originalEnd ) {
411+ groups . push ( {
412+ segments : segments . slice ( start , end ) ,
413+ atEnd : position === segment . originalEnd && position !== segment . originalStart ,
414+ } ) ;
351415 }
352- groups . push ( { segments : right , atEnd : false } ) ;
353- return groups ;
416+ start = end ;
354417 }
355- if ( low === 0 ) return [ ] ;
356- const left = segments [ low - 1 ] ;
357- if ( position > left . originalEnd ) return [ ] ;
358- let start = low - 1 ;
359- while ( start > 0 && sameOriginalRange ( segments [ start - 1 ] , left ) ) start -- ;
360- return [ { segments : segments . slice ( start , low ) , atEnd : position === left . originalEnd } ] ;
418+ return groups ;
361419}
362420
363421/** Reports whether a segment participates in an original-to-virtual query for `features`. */
0 commit comments