|
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 | + } |
25 | 147 | } |
0 commit comments