1- import type { AnnotationComment , SourceRange } from './types'
2- import { compareRanges , createRange , createSingleLineRange } from '../internal/ranges '
3- import { getGroupIndicesFromRegExpMatch } from '../internal/regexps '
1+ import type { AnnotatedCode } from './types'
2+ import { findSearchQueryMatchesInLine , getFirstNonAnnotationCommentLineContents , getNonAnnotationCommentLineContents } from '../internal/text-content '
3+ import { compareRanges , createSingleLineRange } from '../internal/ranges '
44
5- export type FindAnnotationTargetsOptions = {
6- codeLines : string [ ]
7- annotationComments : AnnotationComment [ ]
8- }
9-
10- export function findAnnotationTargets ( options : FindAnnotationTargetsOptions ) {
11- const { codeLines, annotationComments } = options
5+ export function findAnnotationTargets ( annotatedCode : AnnotatedCode ) {
6+ const { codeLines, annotationComments } = annotatedCode
127
138 annotationComments . forEach ( ( comment ) => {
149 const { tag, commentRange, annotationRange, targetRanges } = comment
@@ -18,7 +13,7 @@ export function findAnnotationTargets(options: FindAnnotationTargetsOptions) {
1813 if ( tag . name === 'ignore-tags' ) return
1914
2015 const commentLineIndex = commentRange . start . line
21- const commentLineContents = getNonAnnotationCommentLineContents ( commentLineIndex , options )
16+ const commentLineContents = getNonAnnotationCommentLineContents ( commentLineIndex , annotatedCode )
2217 let { relativeTargetRange } = tag
2318 const { targetSearchQuery } = tag
2419
@@ -32,12 +27,12 @@ export function findAnnotationTargets(options: FindAnnotationTargetsOptions) {
3227 return targetRanges . push ( createSingleLineRange ( commentLineIndex ) )
3328 }
3429 // Otherwise, scan for a target line below
35- const potentialTargetBelow = getFirstNonAnnotationCommentLineContents ( commentLineIndex , 'below' , options )
30+ const potentialTargetBelow = getFirstNonAnnotationCommentLineContents ( commentLineIndex , 'below' , annotatedCode )
3631 if ( potentialTargetBelow ?. hasNonWhitespaceContent ) {
3732 return targetRanges . push ( createSingleLineRange ( potentialTargetBelow . lineIndex ) )
3833 }
3934 // Finally, scan for a target line above
40- const potentialTargetAbove = getFirstNonAnnotationCommentLineContents ( commentLineIndex , 'above' , options )
35+ const potentialTargetAbove = getFirstNonAnnotationCommentLineContents ( commentLineIndex , 'above' , annotatedCode )
4136 if ( potentialTargetAbove ?. hasNonWhitespaceContent ) {
4237 return targetRanges . push ( createSingleLineRange ( potentialTargetAbove . lineIndex ) )
4338 }
@@ -51,36 +46,29 @@ export function findAnnotationTargets(options: FindAnnotationTargetsOptions) {
5146 let lineIndex = commentLineIndex
5247 let remainingLines = Math . abs ( relativeTargetRange )
5348 while ( lineIndex >= 0 && lineIndex < codeLines . length && remainingLines > 0 ) {
54- // Check if the line is a valid target
55- const lineContents = getNonAnnotationCommentLineContents ( lineIndex , options )
49+ // Check if the line is a valid target (annotation comment lines are skipped)
50+ const lineContents = getNonAnnotationCommentLineContents ( lineIndex , annotatedCode )
5651 if ( lineContents . contentRanges . length ) {
5752 targetRanges . push ( createSingleLineRange ( lineIndex ) )
5853 remainingLines --
5954 }
6055 lineIndex += step
6156 }
62- }
63-
64- // If a target search query is present, perform the search to determine the target range(s)
65- if ( targetSearchQuery ) {
66- // Read the direction and number of matches to find from the tag,
67- // or auto-detect this in case no relative target range was given
57+ } else {
58+ // A target search query is present, so we need to search for target ranges,
59+ // but first we need to ensure we know the direction and number of matches to find
6860 if ( relativeTargetRange === undefined ) {
6961 if ( commentLineContents . hasNonWhitespaceContent ) {
70- // The annotation comment is on the same line as content, so the direction
71- // depends on where the content is in relation to the annotation
72- const hasContentBeforeAnnotation = commentLineContents . nonWhitespaceContentRanges . some (
73- // Check for non-whitespace content that starts before the annotation
74- ( contentRange ) => compareRanges ( annotationRange , contentRange , 'start' ) < 0
75- )
76- relativeTargetRange = hasContentBeforeAnnotation ? - 1 : 1
62+ // The annotation comment is on the same line as content,
63+ // so it starts searching at this line and goes downwards
64+ relativeTargetRange = 1
7765 } else {
7866 // Otherwise, the direction defaults to downwards, unless the annotation comment
7967 // is visually grouped with content above it (= there no content directly below
8068 // the annotation comment(s), but there is content directly above)
8169 const isGroupedWithContentAbove =
82- ! getFirstNonAnnotationCommentLineContents ( commentLineIndex , 'below' , options ) ?. hasNonWhitespaceContent &&
83- getFirstNonAnnotationCommentLineContents ( commentLineIndex , 'above' , options ) ?. hasNonWhitespaceContent
70+ ! getFirstNonAnnotationCommentLineContents ( commentLineIndex , 'below' , annotatedCode ) ?. hasNonWhitespaceContent &&
71+ getFirstNonAnnotationCommentLineContents ( commentLineIndex , 'above' , annotatedCode ) ?. hasNonWhitespaceContent
8472 relativeTargetRange = isGroupedWithContentAbove ? - 1 : 1
8573 }
8674 }
@@ -92,8 +80,7 @@ export function findAnnotationTargets(options: FindAnnotationTargetsOptions) {
9280 while ( lineIndex >= 0 && lineIndex < codeLines . length && remainingMatches > 0 ) {
9381 // Search all ranges of the line that are not part of an annotation comment
9482 // for matches of the target search query
95- const lineContents = getNonAnnotationCommentLineContents ( lineIndex , options )
96- const matches = lineContents . contentRanges . flatMap ( ( contentRange ) => findSearchQueryMatchesInLine ( targetSearchQuery , contentRange , options ) )
83+ const matches = findSearchQueryMatchesInLine ( lineIndex , targetSearchQuery , annotatedCode )
9784 if ( matches . length ) {
9885 // Go through the matches in the direction of the relative target range
9986 // until we have found the required number of matches
@@ -107,135 +94,7 @@ export function findAnnotationTargets(options: FindAnnotationTargetsOptions) {
10794 lineIndex += step
10895 }
10996 }
97+ // In case of a negative direction, fix the potentially mixed up order of target ranges
98+ if ( relativeTargetRange < 0 ) targetRanges . sort ( ( a , b ) => compareRanges ( b , a , 'start' ) )
11099 } )
111100}
112-
113- /**
114- * Examines the given code line and annotation comments, and returns information about
115- * the contents of this line that do NOT belong to annotation comments (if any).
116- */
117- function getNonAnnotationCommentLineContents ( lineIndex : number , options : FindAnnotationTargetsOptions ) {
118- const { codeLines, annotationComments } = options
119- const lineLength = codeLines [ lineIndex ] . length
120- const contentRanges : SourceRange [ ] = [ { start : { line : lineIndex , column : 0 } , end : { line : lineIndex , column : lineLength } } ]
121-
122- annotationComments . forEach ( ( comment ) => {
123- const { commentRange } = comment
124- // Ignore the current comment if it's outside the current line
125- if ( commentRange . start . line > lineIndex || commentRange . end . line < lineIndex ) return
126- // Otherwise, go through all non-annotation ranges to remove, cut, or split them
127- // if they intersect with with the current comment
128- const commentStartColumn = commentRange . start . line === lineIndex ? ( commentRange . start . column ?? 0 ) : 0
129- const commentEndColumn = commentRange . end . line === lineIndex ? ( commentRange . end . column ?? lineLength ) : lineLength
130- for ( let i = contentRanges . length - 1 ; i >= 0 ; i -- ) {
131- const nonAnnotationRange = contentRanges [ i ]
132- const rangeStartColumn = nonAnnotationRange . start . column ?? 0
133- const rangeEndColumn = nonAnnotationRange . end . column ?? lineLength
134- if ( commentStartColumn <= rangeStartColumn && commentEndColumn >= rangeEndColumn ) {
135- // The comment completely covers the range, so remove it
136- contentRanges . splice ( i , 1 )
137- } else if ( commentStartColumn <= rangeStartColumn && commentEndColumn < rangeEndColumn ) {
138- // The comment overlaps with the start of the range, so adjust the range start
139- nonAnnotationRange . start . column = commentEndColumn
140- } else if ( commentStartColumn > rangeStartColumn && commentEndColumn >= rangeEndColumn ) {
141- // The comment overlaps with the end of the range, so adjust the range end
142- nonAnnotationRange . end . column = commentStartColumn
143- } else if ( commentStartColumn > rangeStartColumn && commentEndColumn < rangeEndColumn ) {
144- // The comment is inside the range, so split the range into two
145- // ...by making the current range end before the comment
146- nonAnnotationRange . end . column = commentStartColumn
147- // ...and inserting a new range that starts after the comment
148- contentRanges . splice ( i + 1 , 0 , { start : { line : lineIndex , column : commentEndColumn } , end : { line : lineIndex , column : rangeEndColumn } } )
149- }
150- }
151- } )
152-
153- const nonWhitespaceContentRanges = contentRanges . filter ( ( range ) => codeLines [ lineIndex ] . slice ( range . start . column , range . end . column ) . search ( / \S / ) > - 1 )
154-
155- return {
156- lineIndex,
157- contentRanges,
158- nonWhitespaceContentRanges,
159- hasNonWhitespaceContent : nonWhitespaceContentRanges . length > 0 ,
160- }
161- }
162-
163- /**
164- * Returns information about the contents of the first non-annotation comment line
165- * above or below the given start line index.
166- *
167- * If no such line is found, returns `undefined`.
168- */
169- function getFirstNonAnnotationCommentLineContents ( startLineIndex : number , searchDirection : 'above' | 'below' , options : FindAnnotationTargetsOptions ) {
170- const { codeLines } = options
171- const lineCount = codeLines . length
172- const step = searchDirection === 'above' ? - 1 : 1
173- let lineIndex = startLineIndex + step
174- while ( lineIndex >= 0 && lineIndex < lineCount ) {
175- const contents = getNonAnnotationCommentLineContents ( lineIndex , options )
176- if ( contents . contentRanges . length ) return contents
177- lineIndex += step
178- }
179- }
180-
181- /**
182- * Searches the given column range on a single line for matches of the given search query,
183- * and returns an array of source ranges that represent the matches.
184- */
185- function findSearchQueryMatchesInLine ( searchQuery : string | RegExp , rangeToSearch : SourceRange , options : FindAnnotationTargetsOptions ) {
186- const { codeLines } = options
187- const lineIndex = rangeToSearch . start . line
188- const startColumn = rangeToSearch . start . column ?? 0
189- const content = codeLines [ lineIndex ] . slice ( startColumn , rangeToSearch . end . column )
190-
191- const ranges : SourceRange [ ] = [ ]
192-
193- // Handle plaintext string search terms
194- if ( typeof searchQuery === 'string' ) {
195- let idx = content . indexOf ( searchQuery , 0 )
196- while ( idx > - 1 ) {
197- ranges . push (
198- createRange ( {
199- codeLines,
200- start : { line : lineIndex , column : startColumn + idx } ,
201- end : { line : lineIndex , column : startColumn + idx + searchQuery . length } ,
202- } )
203- )
204- idx = content . indexOf ( searchQuery , idx + searchQuery . length )
205- }
206- }
207-
208- // Handle regular expression search terms
209- if ( searchQuery instanceof RegExp ) {
210- const matches = content . matchAll ( searchQuery )
211- for ( const match of matches ) {
212- const rawGroupIndices = getGroupIndicesFromRegExpMatch ( match )
213- // Remove null group indices
214- let groupIndices = rawGroupIndices . flatMap ( ( range ) => ( range ? [ range ] : [ ] ) )
215- // If there are no non-null indices, use the full match instead
216- // (capture group feature fallback, impossible to cover in tests)
217- /* c8 ignore start */
218- if ( ! groupIndices . length ) {
219- groupIndices = [ [ match . index , match . index + match [ 0 ] . length ] ]
220- }
221- /* c8 ignore end */
222- // If there are multiple non-null indices, remove the first one
223- // as it is the full match and we only want to mark capture groups
224- if ( groupIndices . length > 1 ) {
225- groupIndices . shift ( )
226- }
227- // Create marked ranges from all remaining group indices
228- groupIndices . forEach ( ( range ) => {
229- ranges . push (
230- createRange ( {
231- codeLines,
232- start : { line : lineIndex , column : startColumn + range [ 0 ] } ,
233- end : { line : lineIndex , column : startColumn + range [ 1 ] } ,
234- } )
235- )
236- } )
237- }
238- }
239-
240- return ranges
241- }
0 commit comments