Skip to content

Commit fb4db32

Browse files
committed
Add commentRange / annotationRange separation
1 parent 29fbf9b commit fb4db32

7 files changed

Lines changed: 366 additions & 309 deletions

File tree

packages/annotation-comments/src/core/parse.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ export function parseAnnotationComments(options: ParseAnnotationCommentsOptions)
1717
const ignoreCountPerTagName = new Map<string, number>()
1818
let tagsIgnoredUntilLineIndex = -1
1919

20-
let previousCommentRanges: SourceRange[] = []
20+
let previousContentRanges: SourceRange[] = []
2121
annotationTags.forEach((tag) => {
2222
// Ignore the current tag if it is located inside the content ranges
2323
// of the previously processed annotation comment
24-
if (previousCommentRanges.some((range) => secondRangeIsInFirst(range, tag.range))) return
24+
if (previousContentRanges.some((range) => secondRangeIsInFirst(range, tag.range))) return
2525

2626
// Attempt to find a comment that the current annotation tag is located in
2727
const comment = parseParentComment({ codeLines, tag })
@@ -38,7 +38,7 @@ export function parseAnnotationComments(options: ParseAnnotationCommentsOptions)
3838
// Allow creating new ignores
3939
if (tag.name === 'ignore-tags') {
4040
// By definition, `ignore-tags` must be on its own line
41-
if (comment.commentRange.start.column || comment.commentRange.end.column) return
41+
if (comment.annotationRange.start.column || comment.annotationRange.end.column) return
4242
const ignoreRange = tag.relativeTargetRange ?? 1
4343
if (typeof tag.targetSearchQuery === 'string') {
4444
const targetTagNames = tag.targetSearchQuery.split(',').map((name) => name.trim())
@@ -53,7 +53,7 @@ export function parseAnnotationComments(options: ParseAnnotationCommentsOptions)
5353

5454
// If we arrive here, add the tag and comment to the list of annotation comments
5555
annotationComments.push(comment)
56-
previousCommentRanges = comment.contentRanges
56+
previousContentRanges = comment.contentRanges
5757
})
5858

5959
return annotationComments

packages/annotation-comments/src/core/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
11
export type AnnotationComment = {
22
tag: AnnotationTag
33
contents: string[]
4+
/**
5+
* The outer range of the parent comment that contains the annotation,
6+
* including the comment's opening and closing syntax.
7+
*
8+
* Note that multi-line comments can contain multiple annotations and non-annotation content.
9+
* In such cases, {@link AnnotationComment.commentRange} is larger than
10+
* {@link AnnotationComment.annotationRange}.
11+
*/
412
commentRange: SourceRange
13+
/**
14+
* The outer range of the annotation, covering both the annotation tag and
15+
* any optional content.
16+
*
17+
* If the parent comment only contains this annotation and nothing else,
18+
* this range is equal to {@link AnnotationComment.commentRange}, which includes
19+
* the comment's opening and closing syntax. This allows removing the annotation
20+
* from the code without leaving an empty comment behind.
21+
*
22+
* In all other cases, this range covers only the parts inside the parent comment
23+
* that belong to this annotation. This allows removing the annotation without
24+
* affecting other annotations or non-annotation content inside the parent comment.
25+
*/
26+
annotationRange: SourceRange
527
contentRanges: SourceRange[]
628
targetRanges: SourceRange[]
729
}

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import type { SourceRange } from '../core/types'
1+
import type { SourceLocation, SourceRange } from '../core/types'
22

3-
export function createRange(options: { line: string; lineIndex: number; startColumn: number; endColumn: number }) {
4-
const { line, lineIndex, startColumn, endColumn } = options
3+
/**
4+
* Creates a new source range object from the given start and end locations.
5+
*/
6+
export function createRange(options: { codeLines: string[]; start: SourceLocation; end: SourceLocation }) {
7+
const { codeLines, start, end } = options
58
const range: SourceRange = {
6-
start: { line: lineIndex },
7-
end: { line: lineIndex },
9+
start: { line: start.line },
10+
end: { line: end.line },
811
}
9-
if (startColumn > 0) range.start.column = startColumn
10-
if (endColumn < line.length) range.end.column = endColumn
12+
if (start.column ?? 0 > 0) range.start.column = start.column
13+
if (end.column && end.column < (codeLines[end.line] ?? '').length) range.end.column = end.column
1114
return range
1215
}
1316

packages/annotation-comments/src/parsers/comment-types/multi-line.ts

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -158,16 +158,14 @@ function findCommentSyntaxMatches(options: {
158158
if (syntax[type] !== sequence.delimiter) return
159159
// Otherwise, set the ranges and mark the array as updated
160160
match[delimiterProp] = createRange({
161-
line,
162-
lineIndex,
163-
startColumn: sequence.index,
164-
endColumn: sequence.index + sequence.delimiter.length,
161+
codeLines,
162+
start: { line: lineIndex, column: sequence.index },
163+
end: { line: lineIndex, column: sequence.index + sequence.delimiter.length },
165164
})
166165
match[whitespaceProp] = createRange({
167-
line,
168-
lineIndex,
169-
startColumn: sequence.index - sequence.leadingWhitespace.length,
170-
endColumn: sequence.index + sequence.delimiter.length + sequence.trailingWhitespace.length,
166+
codeLines,
167+
start: { line: lineIndex, column: sequence.index - sequence.leadingWhitespace.length },
168+
end: { line: lineIndex, column: sequence.index + sequence.delimiter.length + sequence.trailingWhitespace.length },
171169
})
172170
foundNewMatches = true
173171
})
@@ -254,28 +252,40 @@ function getCommentFromMatchingSyntaxPair(options: {
254252
const syntax = multiLineCommentSyntaxes[bestMatchIndex]
255253
const isOnSingleLine = match.openingRange.start.line === match.closingRange.end.line
256254
const isOnSingleLineBeforeCode = isOnSingleLine && match.closingRangeWithWhitespace.end.column
257-
const commentRange: SourceRange = {
255+
const commentRange = createRange({
256+
codeLines,
258257
start: isOnSingleLineBeforeCode ? match.openingRange.start : match.openingRangeWithWhitespace.start,
259258
end: match.closingRangeWithWhitespace.end,
260-
}
261-
const innerRange: SourceRange = {
259+
})
260+
// By default, assume that the annotation is the only content of the current comment
261+
// (this will be adjusted later if another annotation or non-annotation content is found)
262+
const annotationRange = createRange({
263+
codeLines,
264+
start: commentRange.start,
265+
end: commentRange.end,
266+
})
267+
268+
// Now determine and go through the comment's inner range to collect all contents
269+
const commentInnerRange = createRange({
270+
codeLines,
262271
start: match.openingRangeWithWhitespace.end,
263272
end: match.closingRangeWithWhitespace.start,
264-
}
273+
})
265274
// If the opening sequence ends at a line boundary, adjust the inner range to exclude it
266-
if (!innerRange.start.column && !isOnSingleLine) {
267-
innerRange.start = { line: innerRange.start.line + 1 }
275+
if (!commentInnerRange.start.column && !isOnSingleLine) {
276+
commentInnerRange.start = { line: commentInnerRange.start.line + 1 }
268277
}
269278
// If the closing sequence starts on a line boundary, adjust the inner range to exclude it
270-
if (!innerRange.end.column && !isOnSingleLine) {
271-
innerRange.end = { line: innerRange.end.line - 1 }
279+
if (!commentInnerRange.end.column && !isOnSingleLine) {
280+
commentInnerRange.end = { line: commentInnerRange.end.line - 1 }
272281
}
282+
273283
const contents: string[] = []
274284
const contentRanges: SourceRange[] = []
275285

276-
for (let lineIndex = innerRange.start.line; lineIndex <= innerRange.end.line; lineIndex++) {
277-
const startColumn = lineIndex === tag.range.end.line ? tag.range.end.column : lineIndex === innerRange.start.line ? innerRange.start.column : undefined
278-
const endColumn = lineIndex === innerRange.end.line ? innerRange.end.column : undefined
286+
for (let lineIndex = commentInnerRange.start.line; lineIndex <= commentInnerRange.end.line; lineIndex++) {
287+
const startColumn = lineIndex === tag.range.end.line ? tag.range.end.column : lineIndex === commentInnerRange.start.line ? commentInnerRange.start.column : undefined
288+
const endColumn = lineIndex === commentInnerRange.end.line ? commentInnerRange.end.column : undefined
279289

280290
const lineContent = getTextContentInLine({
281291
codeLines,
@@ -286,26 +296,26 @@ function getCommentFromMatchingSyntaxPair(options: {
286296
})
287297
if (lineIndex < tag.range.end.line) {
288298
// If the current comment line has content and is located before the annotation tag,
289-
// we need to reduce the comment range to exclude any non-annotation content
299+
// we need to reduce the annotation range to exclude any non-annotation content
290300
// including the opening and closing comment syntaxes, so removing the annotation
291301
// later doesn't break the commment
292302
if (lineContent.content.length) {
293-
commentRange.start = { line: tag.range.start.line }
294-
commentRange.end = { ...innerRange.end }
303+
annotationRange.start = { line: tag.range.start.line }
304+
annotationRange.end = { ...commentInnerRange.end }
295305
}
296306
} else if (lineIndex >= tag.range.end.line && lineContent.content === '---') {
297307
// We encountered a separator line after the annotation tag, so this is a mixed
298-
// comment with multiple pieces of content and we must limit the comment range
308+
// comment with multiple pieces of content and we must limit the annotation range
299309
// to the current annotation (however, we still include the separator line)
300-
commentRange.start = { line: tag.range.start.line }
301-
commentRange.end = { line: lineIndex }
310+
annotationRange.start = { line: tag.range.start.line }
311+
annotationRange.end = { line: lineIndex }
302312
break
303313
} else if (lineIndex >= tag.range.end.line && lineContent.content.startsWith('[!')) {
304314
// We encountered the beginning of another annotation tag, so this is a mixed
305-
// comment with multiple pieces of content and we must limit the comment range
315+
// comment with multiple pieces of content and we must limit the annotation range
306316
// to the current annotation
307-
commentRange.start = { line: tag.range.start.line }
308-
commentRange.end = { line: lineIndex - 1 }
317+
annotationRange.start = { line: tag.range.start.line }
318+
annotationRange.end = { line: lineIndex - 1 }
309319
break
310320
} else {
311321
contents.push(lineContent.content)
@@ -327,6 +337,7 @@ function getCommentFromMatchingSyntaxPair(options: {
327337
tag,
328338
contents,
329339
commentRange,
340+
annotationRange,
330341
contentRanges,
331342
targetRanges: [],
332343
}

packages/annotation-comments/src/parsers/comment-types/single-line.ts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import type { AnnotationComment } from '../../core/types'
1+
import type { AnnotationComment, SourceRange } from '../../core/types'
22
import type { ParseParentCommentOptions } from '../parent-comment'
33
import { escapeRegExp } from '../../internal/escaping'
44
import { getTextContentInLine } from '../text-content'
5+
import { createRange } from '../../internal/ranges'
56

67
const singleLineCommentSyntaxes: string[] = [
78
// JS, TS, Java, C, C++, C#, F#, Rust, Go, etc.
@@ -47,20 +48,17 @@ export function parseSingleLineParentComment(options: ParseParentCommentOptions)
4748
const singleLineCommentSyntax = singleLineCommentSyntaxMatches.find((match) => match.endColumn === tag.range.start.column)
4849
if (singleLineCommentSyntax) {
4950
// We found a single-line annotation comment, so start collecting its details
50-
const comment: AnnotationComment = {
51-
tag,
52-
contents: [],
53-
commentRange: {
54-
start: { line: tagLineIndex },
55-
end: { line: tagLineIndex },
56-
},
57-
contentRanges: [],
58-
targetRanges: [],
59-
}
51+
const commentRange = createRange({
52+
codeLines,
53+
start: { line: tagLineIndex },
54+
end: { line: tagLineIndex },
55+
})
56+
const contents: string[] = []
57+
const contentRanges: SourceRange[] = []
6058
// If there is code before the comment, remember the comment start column
6159
// to avoid deleting the entire line when removing the comment
6260
if (tagLine.slice(0, singleLineCommentSyntax.startColumn).trim() !== '') {
63-
comment.commentRange.start.column = singleLineCommentSyntax.startColumn
61+
commentRange.start.column = singleLineCommentSyntax.startColumn
6462
}
6563
// For common Shiki transformer syntax compatibility, support chaining multiple
6664
// single-line annotation comments on the same line
@@ -74,14 +72,14 @@ export function parseSingleLineParentComment(options: ParseParentCommentOptions)
7472
tagLine.slice(match.endColumn).startsWith('[!')
7573
)
7674
if (chainedSingleLineCommentSyntax) {
77-
comment.commentRange.end.column = chainedSingleLineCommentSyntax.startColumn
75+
commentRange.end.column = chainedSingleLineCommentSyntax.startColumn
7876
}
7977
// If there is any non-whitespace content between the end of the annotation tag
8078
// and the current end of the comment, add it to the contents and contentRanges arrays
81-
const tagLineContent = getTextContentInLine({ codeLines, lineIndex: tagLineIndex, startColumn: tagEndColumn, endColumn: comment.commentRange.end.column })
79+
const tagLineContent = getTextContentInLine({ codeLines, lineIndex: tagLineIndex, startColumn: tagEndColumn, endColumn: commentRange.end.column })
8280
if (tagLineContent.content) {
83-
comment.contents.push(tagLineContent.content)
84-
comment.contentRanges.push(tagLineContent.contentRange)
81+
contents.push(tagLineContent.content)
82+
contentRanges.push(tagLineContent.contentRange)
8583
}
8684
// For supported annotation comments, allow expanding the comment end location
8785
// and annotation content to subsequent comment lines
@@ -107,27 +105,37 @@ export function parseSingleLineParentComment(options: ParseParentCommentOptions)
107105
// Stop if the line has `---` as its only text content
108106
if (lineContent.content === '---') {
109107
// Make the line part of the comment, but don't add its content
110-
comment.commentRange.end = { line: lineIndex }
108+
commentRange.end = { line: lineIndex }
111109
break
112110
}
113111
// Stop if the line starts with an annotation tag opening sequence `[!`
114112
if (lineContent.content.startsWith('[!')) break
115113
// Otherwise, add the content and expand the comment range
116114
// to cover the additional full line
117-
comment.contents.push(lineContent.content)
118-
comment.contentRanges.push(lineContent.contentRange)
119-
comment.commentRange.end = { line: lineIndex }
115+
contents.push(lineContent.content)
116+
contentRanges.push(lineContent.contentRange)
117+
commentRange.end = { line: lineIndex }
120118
} else {
121119
// Comment lines without content are allowed, so an empty string to the content
122120
// and expand the comment range to cover the additional full line
123121
const column = Math.min(possibleContentStart, line.length)
124-
comment.contents.push('')
125-
comment.contentRanges.push({ start: { line: lineIndex, column }, end: { line: lineIndex, column } })
126-
comment.commentRange.end = { line: lineIndex }
122+
contents.push('')
123+
contentRanges.push({ start: { line: lineIndex, column }, end: { line: lineIndex, column } })
124+
commentRange.end = { line: lineIndex }
127125
}
128126
}
129127
}
130128

131-
return comment
129+
// For single-line comments, the annotation range is always equal to the comment range
130+
const annotationRange = createRange({ codeLines, start: commentRange.start, end: commentRange.end })
131+
132+
return {
133+
tag,
134+
contents,
135+
commentRange,
136+
annotationRange,
137+
contentRanges,
138+
targetRanges: [],
139+
}
132140
}
133141
}

0 commit comments

Comments
 (0)