Skip to content

Commit 851b3bc

Browse files
committed
Begin findAnnotationTargets implementation
1 parent cf8c901 commit 851b3bc

3 files changed

Lines changed: 151 additions & 29 deletions

File tree

Lines changed: 146 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,147 @@
1-
/*
2-
- Now, the function goes through all identified annotation comments and does the following:
3-
- If the annotation has **no relative target range** given, automatically determine it:
4-
- If the annotation has **no target search query**, it attempts to target full lines:
5-
- If the **annotation comment is on the same line as content** (= annotation start line contains content other than whitespace or other annotation comments), the target range is the annotation comment line.
6-
- Otherwise, find the first line above and first line below that don't fully consist of annotation comments
7-
- If the **line below has content**, it is the target range.
8-
- Otherwise, if the **line above has content**, it is the target range.
9-
- Otherwise (**both lines are empty**), there is no target range (same as the relative range `:0`).
10-
- Otherwise, the annotation **has a target search query**, so determine the search direction:
11-
- If the **annotation comment is on the same line as content** (= annotation start line contains content other than whitespace or other annotation comments), the direction depends on where the content is in relation to the comment.
12-
- Otherwise, find the first line above and first line below that don't fully consist of annotation comments
13-
- If the **line above has content** and the **line below is empty**, the relative range is `:-1`.
14-
- Otherwise, the relative range is `:1`.
15-
- If a target search query is present, **perform the search** to determine the target range(s):
16-
- The target search query can be a simple string, a single-quoted string, a double-quoted string, or a regular expression. Regular expressions can optionally contain capture groups, which will then be used to determine the target range(s) instead of the full match.
17-
- The search is performed line by line, starting at the start or end of the annotation comment and going in the direction determined by the relative target range that was either given or automatically determined as described above.
18-
- Before searching a line for matches, all characters that lie within the `outerRange` of any annotation comment are removed from the line. If matches are found, the matched ranges are adjusted to include the removed characters.
19-
- Each match is added to the `targetRanges` until the number of matches equals the absolute value of the relative target range, or the end of the code is reached.
20-
- In the case of regular expressions with capture groups, a single match can result in multiple target ranges, one for each capture group.
21-
*/
22-
23-
export function findAnnotationTargets() {
24-
// TODO: Implement findAnnotationTargets()
1+
import type { AnnotationComment, SourceRange } from './types'
2+
import { compareRanges, createSingleLineRange } from '../internal/ranges'
3+
4+
export type FindAnnotationTargetsOptions = {
5+
codeLines: string[]
6+
annotationComments: AnnotationComment[]
7+
}
8+
9+
export function findAnnotationTargets(options: FindAnnotationTargetsOptions) {
10+
const { codeLines, annotationComments } = options
11+
12+
// TODO: Finish implementation
13+
annotationComments.forEach((comment) => {
14+
const annotationLineIndex = comment.annotationRange.start.line
15+
const annotationLineContents = getNonAnnotationCommentLineContents(annotationLineIndex, options)
16+
17+
// If the annotation has no relative target range and no target search query,
18+
// try to find a nearby target line that is not empty and not an annotation comment
19+
if (comment.tag.relativeTargetRange === undefined && comment.tag.targetSearchQuery === undefined) {
20+
// Check if the annotation line itself is a valid target
21+
if (annotationLineContents.hasNonWhitespaceContent) {
22+
return comment.targetRanges.push(createSingleLineRange(annotationLineIndex))
23+
}
24+
// Otherwise, scan for a target line below
25+
const potentialTargetBelow = getFirstNonAnnotationCommentLineContents(annotationLineIndex, 'below', options)
26+
if (potentialTargetBelow?.hasNonWhitespaceContent) {
27+
return comment.targetRanges.push(createSingleLineRange(potentialTargetBelow.lineIndex))
28+
}
29+
// Finally, scan for a target line above
30+
const potentialTargetAbove = getFirstNonAnnotationCommentLineContents(annotationLineIndex, 'above', options)
31+
if (potentialTargetAbove?.hasNonWhitespaceContent) {
32+
return comment.targetRanges.push(createSingleLineRange(potentialTargetAbove.lineIndex))
33+
}
34+
// If we arrive here, there is no target range (same as the relative range `:0`),
35+
// so don't do anything
36+
return
37+
}
38+
39+
// If a target search query is present, perform the search to determine the target range(s)
40+
if (comment.tag.targetSearchQuery) {
41+
// Read the direction and number of matches to find from the tag,
42+
// or auto-detect this in case no relative target range was given
43+
let relativeTargetRange = comment.tag.relativeTargetRange
44+
if (relativeTargetRange === undefined) {
45+
if (annotationLineContents.hasNonWhitespaceContent) {
46+
// The annotation comment is on the same line as content, so the direction
47+
// depends on where the content is in relation to the comment
48+
const hasContentBeforeAnnotation = annotationLineContents.nonWhitespaceContentRanges.some(
49+
(contentRange) => compareRanges(comment.annotationRange, contentRange, 'start') < 0
50+
)
51+
relativeTargetRange = hasContentBeforeAnnotation ? -1 : 1
52+
} else {
53+
// Otherwise, the direction defaults to downwards, unless the annotation comment
54+
// is visually grouped with content above it (= there no content directly below
55+
// the annotation comment(s), but there is content directly above)
56+
const isGroupedWithContentAbove =
57+
!getFirstNonAnnotationCommentLineContents(annotationLineIndex, 'below', options)?.hasNonWhitespaceContent &&
58+
getFirstNonAnnotationCommentLineContents(annotationLineIndex, 'above', options)?.hasNonWhitespaceContent
59+
relativeTargetRange = isGroupedWithContentAbove ? -1 : 1
60+
}
61+
}
62+
63+
// - Perform the search:
64+
// - The target search query can be a simple string, a single-quoted string, a double-quoted
65+
// string, or a regular expression. Regular expressions can optionally contain capture groups,
66+
// which will then be used to determine the target range(s) instead of the full match.
67+
// - The search is performed line by line, starting at the start or end of the annotation comment
68+
// and going in the direction determined by the relative target range that was either given or
69+
// automatically determined as described above.
70+
// - Before searching a line for matches, all characters that lie within the `outerRange` of any
71+
// annotation comment are removed from the line. If matches are found, the matched ranges are
72+
// adjusted to include the removed characters.
73+
// - Each match is added to the `targetRanges` until the number of matches equals the absolute
74+
// value of the relative target range, or the end of the code is reached.
75+
// - In the case of regular expressions with capture groups, a single match can result in multiple
76+
// target ranges, one for each capture group.
77+
}
78+
})
79+
}
80+
81+
/**
82+
* Examines the given code line and annotation comments, and returns information about
83+
* the contents of this line that do NOT belong to annotation comments (if any).
84+
*/
85+
function getNonAnnotationCommentLineContents(lineIndex: number, options: FindAnnotationTargetsOptions) {
86+
const { codeLines, annotationComments } = options
87+
const lineLength = codeLines[lineIndex].length
88+
const contentRanges: SourceRange[] = [{ start: { line: lineIndex, column: 0 }, end: { line: lineIndex, column: lineLength } }]
89+
90+
annotationComments.forEach((comment) => {
91+
const { commentRange } = comment
92+
// Ignore the current comment if it's outside the current line
93+
if (commentRange.start.line > lineIndex || commentRange.end.line < lineIndex) return
94+
// Otherwise, go through all non-annotation ranges to remove, cut, or split them
95+
// if they intersect with with the current comment
96+
const commentStartColumn = commentRange.start.line === lineIndex ? (commentRange.start.column ?? 0) : 0
97+
const commentEndColumn = commentRange.end.line === lineIndex ? (commentRange.end.column ?? lineLength) : lineLength
98+
for (let i = contentRanges.length - 1; i >= 0; i--) {
99+
const nonAnnotationRange = contentRanges[i]
100+
const rangeStartColumn = nonAnnotationRange.start.column ?? 0
101+
const rangeEndColumn = nonAnnotationRange.end.column ?? lineLength
102+
if (commentStartColumn <= rangeStartColumn && commentEndColumn >= rangeEndColumn) {
103+
// The comment completely covers the range, so remove it
104+
contentRanges.splice(i, 1)
105+
} else if (commentStartColumn <= rangeStartColumn && commentEndColumn < rangeEndColumn) {
106+
// The comment overlaps with the start of the range, so adjust the range start
107+
nonAnnotationRange.start.column = commentEndColumn
108+
} else if (commentStartColumn > rangeStartColumn && commentEndColumn >= rangeEndColumn) {
109+
// The comment overlaps with the end of the range, so adjust the range end
110+
nonAnnotationRange.end.column = commentStartColumn
111+
} else if (commentStartColumn > rangeStartColumn && commentEndColumn < rangeEndColumn) {
112+
// The comment is inside the range, so split the range into two
113+
// ...by making the current range end before the comment
114+
nonAnnotationRange.end.column = commentStartColumn
115+
// ...and inserting a new range that starts after the comment
116+
contentRanges.splice(i + 1, 0, { start: { line: lineIndex, column: commentEndColumn }, end: { line: lineIndex, column: rangeEndColumn } })
117+
}
118+
}
119+
})
120+
121+
const nonWhitespaceContentRanges = contentRanges.filter((range) => codeLines[lineIndex].slice(range.start.column, range.end.column).search(/\S/) > -1)
122+
123+
return {
124+
lineIndex,
125+
contentRanges,
126+
nonWhitespaceContentRanges,
127+
hasNonWhitespaceContent: nonWhitespaceContentRanges.length > 0,
128+
}
129+
}
130+
131+
/**
132+
* Returns information about the contents of the first non-annotation comment line
133+
* above or below the given start line index.
134+
*
135+
* If no such line is found, returns `undefined`.
136+
*/
137+
function getFirstNonAnnotationCommentLineContents(startLineIndex: number, searchDirection: 'above' | 'below', options: FindAnnotationTargetsOptions) {
138+
const { codeLines } = options
139+
const lineCount = codeLines.length
140+
const step = searchDirection === 'above' ? -1 : 1
141+
let lineIndex = startLineIndex + step
142+
while (lineIndex >= 0 && lineIndex < lineCount) {
143+
const contents = getNonAnnotationCommentLineContents(lineIndex, options)
144+
if (contents.contentRanges.length) return contents
145+
lineIndex += step
146+
}
25147
}

packages/annotation-comments/src/internal/ranges.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { SourceLocation, SourceRange } from '../core/types'
33
/**
44
* Creates a new source range object from the given start and end locations.
55
*/
6-
export function createRange(options: { codeLines: string[]; start: SourceLocation; end: SourceLocation }) {
6+
export function createRange(options: { codeLines: string[]; start: SourceLocation; end: SourceLocation }): SourceRange {
77
const { codeLines, start, end } = options
88
const range: SourceRange = {
99
start: { line: start.line },
@@ -14,6 +14,10 @@ export function createRange(options: { codeLines: string[]; start: SourceLocatio
1414
return range
1515
}
1616

17+
export function createSingleLineRange(lineIndex: number): SourceRange {
18+
return { start: { line: lineIndex }, end: { line: lineIndex } }
19+
}
20+
1721
/**
1822
* Compares two source ranges by their start or end locations.
1923
*

packages/annotation-comments/test/parent-comment.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -678,8 +678,6 @@ describe('parseParentComment()', () => {
678678
validateParentComment({
679679
lines,
680680
contents: ['Annotation content'],
681-
// Expect the closing syntax not to be included in the comment range
682-
// as the comment also contains non-annotation content
683681
commentRange: { start: { line: 2 }, end: { line: 2 } },
684682
})
685683
})
@@ -695,8 +693,6 @@ describe('parseParentComment()', () => {
695693
validateParentComment({
696694
lines,
697695
contents: ['Annotation content', 'that spans multiple lines'],
698-
// Expect the closing syntax not to be included in the comment range
699-
// as the comment also contains non-annotation content
700696
commentRange: { start: { line: 2 }, end: { line: 5 } },
701697
})
702698
})

0 commit comments

Comments
 (0)