@@ -14,6 +14,13 @@ export function createRange(options: { codeLines: string[]; start: SourceLocatio
1414 return range
1515}
1616
17+ /**
18+ * Returns a copy of the given source range.
19+ */
20+ export function cloneRange ( range : SourceRange ) : SourceRange {
21+ return { start : { ...range . start } , end : { ...range . end } }
22+ }
23+
1724export function createSingleLineRange ( lineIndex : number ) : SourceRange {
1825 return { start : { line : lineIndex } , end : { line : lineIndex } }
1926}
@@ -26,35 +33,149 @@ export function createSingleLineRanges(...lineIndices: number[]): SourceRange[]
2633 * Compares two source ranges by their start or end locations.
2734 *
2835 * Returns:
29- * - `> 0` if the second location is **greater than** (comes after) the first ,
30- * - `< 0` if the second location is **smaller than** (comes before) the first , or
36+ * - `> 0` if the first location is **greater than** (comes after) the second ,
37+ * - `< 0` if the first location is **smaller than** (comes before) the second , or
3138 * - `0` if they are equal.
3239 */
33- export function compareRanges ( a : SourceRange , b : SourceRange , prop : 'start' | 'end' ) : number {
40+ export function compareRanges ( a : SourceRange , b : SourceRange , propA : 'start' | 'end' , propB : 'start' | 'end' = propA ) : number {
3441 // Compare line numbers first
35- const lineResult = b [ prop ] . line - a [ prop ] . line
42+ const lineResult = a [ propA ] . line - b [ propB ] . line
3643 if ( lineResult !== 0 ) return lineResult
3744
3845 // Line numbers are equal, so compare columns
39- const aCol = a [ prop ] . column
40- const bCol = b [ prop ] . column
46+ const aCol = a [ propA ] . column
47+ const bCol = b [ propB ] . column
4148
4249 // If both columns are undefined, the ranges are equal
4350 if ( aCol === undefined && bCol === undefined ) return 0
4451
4552 // If only one column is undefined (= covers the full line),
4653 // the other column starts after and ends before it
47- if ( aCol === undefined ) return prop === 'start' ? 1 : - 1
48- if ( bCol === undefined ) return prop === 'start' ? - 1 : 1
54+ if ( aCol === undefined ) return propA === 'start' ? - 1 : 1
55+ if ( bCol === undefined ) return propB === 'start' ? 1 : - 1
4956
50- return bCol - aCol
57+ return aCol - bCol
58+ }
59+
60+ export function rangesAreEqual ( a : SourceRange , b : SourceRange ) : boolean {
61+ return compareRanges ( a , b , 'start' ) === 0 && compareRanges ( a , b , 'end' ) === 0
5162}
5263
5364export function secondRangeIsInFirst ( potentialOuterRange : SourceRange , rangeToTest : SourceRange ) : boolean {
5465 return (
5566 // To be in range, rangeToTest must start at or after potentialOuterRange...
56- compareRanges ( potentialOuterRange , rangeToTest , 'start' ) >= 0 &&
67+ compareRanges ( rangeToTest , potentialOuterRange , 'start' ) >= 0 &&
5768 // ...and end at or before potentialOuterRange
58- compareRanges ( potentialOuterRange , rangeToTest , 'end' ) <= 0
69+ compareRanges ( rangeToTest , potentialOuterRange , 'end' ) <= 0
5970 )
6071}
72+
73+ /**
74+ * Splits the given range that may span multiple lines into an array of single-line ranges.
75+ */
76+ export function splitRangeByLines ( range : SourceRange ) : SourceRange [ ] {
77+ // For single-line ranges, just return a copy of the range
78+ if ( range . start . line === range . end . line ) return [ cloneRange ( range ) ]
79+
80+ // For multi-line ranges, create a mix of column ranges and full line ranges as needed
81+ const ranges : SourceRange [ ] = [ ]
82+ const isPartialStartLine = range . start . column ? range . start . column > 0 : false
83+ const isPartialEndLine = range . end . column !== undefined
84+
85+ // If the range starts in the middle of a line, add an inline range
86+ if ( isPartialStartLine ) {
87+ ranges . push ( {
88+ start : { line : range . start . line , column : range . start . column } ,
89+ end : { line : range . start . line } ,
90+ } )
91+ }
92+ // Add all full line ranges
93+ for ( let lineIndex = range . start . line + ( isPartialStartLine ? 1 : 0 ) ; lineIndex < range . end . line + ( isPartialEndLine ? 0 : 1 ) ; lineIndex ++ ) {
94+ ranges . push ( createSingleLineRange ( lineIndex ) )
95+ }
96+ // If the range ends in the middle of a line, add an inline range
97+ if ( isPartialEndLine ) {
98+ ranges . push ( {
99+ start : { line : range . end . line } ,
100+ end : { line : range . end . line , column : range . end . column } ,
101+ } )
102+ }
103+ return ranges
104+ }
105+
106+ /**
107+ * Merges any intersecting or adjacent ranges in the given array of source ranges.
108+ */
109+ export function mergeIntersectingOrAdjacentRanges ( ranges : SourceRange [ ] ) : SourceRange [ ] {
110+ const sortedRanges = ranges . slice ( ) . sort ( ( a , b ) => compareRanges ( a , b , 'start' ) )
111+ const mergedRanges : SourceRange [ ] = [ ]
112+ let currentRange : SourceRange | undefined
113+ for ( const newRange of sortedRanges ) {
114+ if ( ! currentRange ) {
115+ currentRange = cloneRange ( newRange )
116+ continue
117+ }
118+ // If the new range starts inside or right at the end of the current one,
119+ // extend the current range if needed
120+ if ( compareRanges ( newRange , currentRange , 'start' , 'end' ) <= 0 ) {
121+ if ( compareRanges ( newRange , currentRange , 'end' ) > 0 ) currentRange . end = newRange . end
122+ continue
123+ }
124+ // Otherwise, we're done with the current range and can switch to the new one
125+ mergedRanges . push ( currentRange )
126+ currentRange = cloneRange ( newRange )
127+ }
128+ if ( currentRange ) mergedRanges . push ( currentRange )
129+ return mergedRanges
130+ }
131+
132+ /**
133+ * Excludes the given exclusion ranges from the outer range by splitting the outer range into
134+ * multiple parts that do not overlap with the exclusions.
135+ *
136+ * The resulting array of source ranges is split by lines and can be a combination of partial
137+ * and full line ranges. The array can also be empty if the outer range is completely covered
138+ * by the exclusions.
139+ */
140+ export function excludeRangesFromOuterRange ( options : { codeLines : string [ ] ; outerRange : SourceRange ; rangesToExclude : SourceRange [ ] } ) : SourceRange [ ] {
141+ const { codeLines, outerRange, rangesToExclude } = options
142+
143+ const remainingRanges : SourceRange [ ] = splitRangeByLines ( outerRange )
144+ const exclusionsSplitByLine = rangesToExclude . flatMap ( ( exclusion ) => splitRangeByLines ( exclusion ) )
145+
146+ exclusionsSplitByLine . forEach ( ( exclusion ) => {
147+ const lineIndex = exclusion . start . line
148+ const lineLength = codeLines [ lineIndex ] . length
149+ const exclusionStartColumn = exclusion . start . column ?? 0
150+ const exclusionEndColumn = exclusion . end . column ?? lineLength
151+ for ( let i = remainingRanges . length - 1 ; i >= 0 ; i -- ) {
152+ const range = remainingRanges [ i ]
153+ // If the range is on a different line, it cannot be affected by the exclusion
154+ if ( range . start . line !== lineIndex ) continue
155+ const rangeStartColumn = range . start . column ?? 0
156+ const rangeEndColumn = range . end . column ?? lineLength
157+ if ( exclusionStartColumn <= rangeStartColumn && exclusionEndColumn >= rangeEndColumn ) {
158+ // The exclusion completely covers the range, so remove it
159+ remainingRanges . splice ( i , 1 )
160+ } else if ( exclusionStartColumn <= rangeStartColumn && exclusionEndColumn < rangeEndColumn ) {
161+ // The exclusion overlaps with the start of the range, so adjust the range start
162+ range . start . column = exclusionEndColumn
163+ } else if ( exclusionStartColumn > rangeStartColumn && exclusionEndColumn >= rangeEndColumn ) {
164+ // The exclusion overlaps with the end of the range, so adjust the range end
165+ range . end . column = exclusionStartColumn
166+ } else if ( exclusionStartColumn > rangeStartColumn && exclusionEndColumn < rangeEndColumn ) {
167+ // The exclusion is inside the range, so split the range into two
168+ // ...by making the current range end before the exclusion
169+ range . end . column = exclusionStartColumn
170+ // ...and inserting a new range that starts after the exclusion
171+ // and ends at the end of the current range
172+ const rangeAfterExclusion = createSingleLineRange ( lineIndex )
173+ if ( exclusionEndColumn > 0 ) rangeAfterExclusion . start . column = exclusionEndColumn
174+ if ( rangeEndColumn < lineLength ) rangeAfterExclusion . end . column = rangeEndColumn
175+ remainingRanges . splice ( i + 1 , 0 , rangeAfterExclusion )
176+ }
177+ }
178+ } )
179+
180+ return remainingRanges
181+ }
0 commit comments