Skip to content

Commit f932f69

Browse files
authored
Add allowCleaning option, rename option to updateCodeRanges (#5)
1 parent d622983 commit f932f69

File tree

6 files changed

+661
-52
lines changed

6 files changed

+661
-52
lines changed

.changeset/curvy-zebras-grab.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'annotation-comments': minor
3+
---
4+
5+
Adds the `cleanCode()` option `allowCleaning`.
6+
7+
By default, `cleanCode()` will clean all annotation comments. If you set `allowCleaning` to a function, you can now control which annotation comments are cleaned.
8+
9+
The function will be called once per annotation comment, and is expected to return a boolean to indicate whether the comment should be cleaned or not.

.changeset/thirty-carpets-sip.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'annotation-comments': minor
3+
---
4+
5+
Renames the `cleanCode()` option `updateTargetRanges` to `updateCodeRanges`.
6+
7+
The option was renamed because the function is now capable of updating all other code ranges referenced by the annotation comments (e.g. tag ranges, comment ranges, content ranges etc.) in addition to the target ranges.
8+
9+
In combination with the `allowCleaning` and `removeAnnotationContents` options, this allows multi-step cleaning of the source code, where only a subset of annotation comments is cleaned in each step. This can be useful to create multiple versions of the source code, e.g. one for copying to the clipboard (where only the annotation tags are removed while keeping the rest of the annotation comments visible in the source code), and one for HTML output (where the entire annotation comments are removed so they can be rendered separately).

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

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
import { cloneRange, excludeRangesFromOuterRange, mergeIntersectingOrAdjacentRanges, rangesAreEqual, splitRangeByLines } from '../internal/ranges'
1+
import { cloneRange, excludeRangesFromOuterRange, isEmptyRange, makeRangeEmpty, mergeIntersectingOrAdjacentRanges, rangesAreEqual, splitRangeByLines } from '../internal/ranges'
22
import { excludeWhitespaceRanges, getTextContentInLine } from '../internal/text-content'
3-
import type { AnnotatedCode, AnnotationComment, SourceLocation, SourceRange } from './types'
3+
import type { AnnotatedCode, AnnotationComment, SourceRange } from './types'
44

55
export type CleanCodeOptions = AnnotatedCode & {
6-
removeAnnotationContents?: boolean | ((context: RemoveAnnotationContentsContext) => boolean)
7-
updateTargetRanges?: boolean
6+
/**
7+
* An optional function that is called for each annotation comment.
8+
* Its return value determines whether the annotation should be cleaned from the code.
9+
*/
10+
allowCleaning?: (context: CleanAnnotationContext) => boolean
11+
removeAnnotationContents?: boolean | ((context: CleanAnnotationContext) => boolean)
12+
/**
13+
* Whether to update all ranges in the annotation comments after applying the changes.
14+
*/
15+
updateCodeRanges?: boolean
816
handleRemoveLine?: (context: HandleRemoveLineContext) => boolean
917
handleEditLine?: (context: HandleEditLineContext) => boolean
1018
}
1119

12-
export type RemoveAnnotationContentsContext = {
20+
export type CleanAnnotationContext = {
1321
comment: AnnotationComment
1422
}
1523

@@ -36,12 +44,18 @@ export type EditLine = {
3644
type SourceChange = RemoveLine | EditLine
3745

3846
export function cleanCode(options: CleanCodeOptions) {
39-
const { codeLines, annotationComments, removeAnnotationContents = false, updateTargetRanges = true, handleRemoveLine, handleEditLine } = options
47+
const { codeLines, annotationComments, removeAnnotationContents = false, updateCodeRanges = true, handleRemoveLine, handleEditLine } = options
4048

41-
// Go through all annotation comments and collect an array of ranges to be removed
49+
// Create a subset of annotation comments to clean
50+
const commentsToClean = annotationComments.filter((comment) => typeof options.allowCleaning !== 'function' || options.allowCleaning({ comment }))
51+
52+
// Go through all annotation comments to clean and collect an array of ranges to be removed
4253
const rangesToBeRemoved: SourceRange[] = []
43-
annotationComments.forEach((annotationComment) => {
44-
const hasContents = annotationComment.contents.length > 0
54+
commentsToClean.forEach((annotationComment) => {
55+
if (isEmptyRange(annotationComment.annotationRange)) return
56+
57+
// Determine whether to remove the entire annotation or just the tag
58+
const hasContents = annotationComment.contentRanges.length && annotationComment.contents.length
4559
const removeEntireAnnotation =
4660
!hasContents || (typeof removeAnnotationContents === 'function' ? removeAnnotationContents({ comment: annotationComment }) : removeAnnotationContents)
4761
const rangeToBeRemoved = cloneRange(removeEntireAnnotation ? annotationComment.annotationRange : annotationComment.tag.range)
@@ -78,7 +92,7 @@ export function cleanCode(options: CleanCodeOptions) {
7892

7993
// Remove any parent comments that would be empty after removing the annotations
8094
const handledRanges: SourceRange[] = []
81-
annotationComments.forEach(({ commentRange, commentInnerRange }) => {
95+
commentsToClean.forEach(({ commentRange, commentInnerRange }) => {
8296
if (handledRanges.some((range) => rangesAreEqual(range, commentInnerRange))) return
8397
handledRanges.push(commentInnerRange)
8498
// If the outer range is already in the list of ranges to be removed, skip this comment
@@ -105,44 +119,66 @@ export function cleanCode(options: CleanCodeOptions) {
105119
if (!handleRemoveLine || !handleRemoveLine({ codeLines, ...change })) {
106120
codeLines.splice(change.lineIndex, 1)
107121
}
108-
if (updateTargetRanges) updateTargetRangesAfterRemoveLine(annotationComments, change)
122+
if (updateCodeRanges) updateCodeRangesAfterChange(annotationComments, change)
109123
} else {
110124
if (!handleEditLine || !handleEditLine({ codeLines, ...change })) {
111125
const line = codeLines[change.lineIndex]
112126
codeLines[change.lineIndex] = line.slice(0, change.startColumn) + (change.newText ?? '') + line.slice(change.endColumn)
113127
}
114-
if (updateTargetRanges) updateTargetRangesAfterEditLine(annotationComments, change)
128+
if (updateCodeRanges) updateCodeRangesAfterChange(annotationComments, change)
115129
}
116130
})
117131
}
118132

119-
function updateTargetRangesAfterRemoveLine(annotationComments: AnnotationComment[], change: RemoveLine) {
120-
annotationComments.forEach(({ targetRanges }) => {
121-
targetRanges.forEach((targetRange, index) => {
122-
if (targetRange.start.line === change.lineIndex) {
123-
targetRanges.splice(index, 1)
124-
} else if (targetRange.start.line > change.lineIndex) {
125-
targetRange.start.line--
126-
targetRange.end.line--
127-
}
128-
})
133+
function updateCodeRangesAfterChange(annotationComments: AnnotationComment[], change: RemoveLine | EditLine) {
134+
annotationComments.forEach((comment) => {
135+
updateCodeRange(comment.tag.range, change)
136+
updateCodeRange(comment.annotationRange, change)
137+
updateCodeRange(comment.commentRange, change)
138+
updateCodeRange(comment.commentInnerRange, change)
139+
updateCodeRanges(comment.contentRanges, change)
140+
updateCodeRanges(comment.targetRanges, change)
129141
})
130142
}
131143

132-
function updateTargetRangesAfterEditLine(annotationComments: AnnotationComment[], change: EditLine) {
133-
annotationComments.forEach(({ targetRanges }) => {
134-
targetRanges.forEach((targetRange) => {
135-
updateLocationIfNecessary(targetRange.start, change)
136-
updateLocationIfNecessary(targetRange.end, change)
137-
})
138-
})
144+
function updateCodeRanges(ranges: SourceRange[], change: RemoveLine | EditLine) {
145+
ranges.forEach((range) => updateCodeRange(range, change))
146+
for (let i = ranges.length - 1; i >= 0; i--) {
147+
if (isEmptyRange(ranges[i])) ranges.splice(i, 1)
148+
}
139149
}
140150

141-
function updateLocationIfNecessary(location: SourceLocation, change: EditLine) {
142-
if (location.line !== change.lineIndex) return
143-
if (!location.column || change.startColumn > location.column) return
144-
const changeDelta = (change.newText?.length ?? 0) - (change.endColumn - change.startColumn)
145-
location.column += changeDelta
151+
export function updateCodeRange(range: SourceRange, change: RemoveLine | EditLine) {
152+
const { start, end } = range
153+
const changeLine = change.lineIndex
154+
if (change.editType === 'removeLine') {
155+
// Range was completely inside the removed line and is now empty
156+
if (start.line === changeLine && end.line === changeLine) return makeRangeEmpty(range)
157+
// Range ended at the removed line, so it now ends at the end of the previous line
158+
if (end.line === changeLine) range.end = { line: changeLine - 1 }
159+
// Range ended after the removed line, so keep column, but move line up
160+
if (end.line > changeLine) end.line--
161+
// Range started at the removed line, so it now starts at the beginning of the new line
162+
if (start.line === changeLine) range.start = { line: changeLine }
163+
// Range started after the removed line, so keep column, but move line up
164+
if (start.line > changeLine) start.line--
165+
} else {
166+
// Ignore inline edits that do not affect the start or end line of the range
167+
if (start.line !== changeLine && end.line !== changeLine) return
168+
const changeDelta = change.endColumn - change.startColumn + (change.newText?.length ?? 0)
169+
const rangeStartColumn = start.column ?? 0
170+
const rangeEndColumn = end.column ?? Infinity
171+
// If the inline edit completely covers the range, the range is now empty
172+
if (start.line === end.line && change.startColumn <= rangeStartColumn && change.endColumn >= rangeEndColumn) return makeRangeEmpty(range)
173+
// Range started at the edited line after the edit, so adjust its start column
174+
if (start.line === changeLine && change.startColumn < rangeStartColumn) {
175+
start.column = rangeStartColumn - Math.min(changeDelta, rangeStartColumn - change.startColumn)
176+
}
177+
// Range ended at the edited line after the edit, so adjust its end column
178+
if (end.line === changeLine && change.startColumn < rangeEndColumn) {
179+
end.column = rangeEndColumn === Infinity ? undefined : rangeEndColumn - Math.min(changeDelta, rangeEndColumn - change.startColumn)
180+
}
181+
}
146182
}
147183

148184
function getRangeRemovalChanges(codeLines: string[], range: SourceRange): SourceChange[] {

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ export function createRange(options: { codeLines: string[]; start: SourceLocatio
1414
return range
1515
}
1616

17+
export function makeRangeEmpty(range: SourceRange) {
18+
range.end = { line: range.start.line, column: range.start.column ?? 0 }
19+
}
20+
21+
export function isEmptyRange(range: SourceRange): boolean {
22+
if (range.start.line !== range.end.line) return false
23+
return range.end.column === 0 || range.end.column === (range.start.column ?? 0)
24+
}
25+
1726
/**
1827
* Returns a copy of the given source range.
1928
*/
@@ -117,7 +126,10 @@ export function mergeIntersectingOrAdjacentRanges(ranges: SourceRange[]): Source
117126
}
118127
// If the new range starts inside or right at the end of the current one,
119128
// extend the current range if needed
120-
if (compareRanges(newRange, currentRange, 'start', 'end') <= 0) {
129+
if (
130+
compareRanges(newRange, currentRange, 'start', 'end') <= 0 ||
131+
(currentRange.end.line + 1 == newRange.start.line && currentRange.end.column === undefined && !newRange.start.column)
132+
) {
121133
if (compareRanges(newRange, currentRange, 'end') > 0) currentRange.end = newRange.end
122134
continue
123135
}
@@ -137,15 +149,15 @@ export function mergeIntersectingOrAdjacentRanges(ranges: SourceRange[]): Source
137149
* and full line ranges. The array can also be empty if the outer range is completely covered
138150
* by the exclusions.
139151
*/
140-
export function excludeRangesFromOuterRange(options: { codeLines: string[]; outerRange: SourceRange; rangesToExclude: SourceRange[] }): SourceRange[] {
152+
export function excludeRangesFromOuterRange(options: { codeLines?: string[] | undefined; outerRange: SourceRange; rangesToExclude: SourceRange[] }): SourceRange[] {
141153
const { codeLines, outerRange, rangesToExclude } = options
142154

143155
const remainingRanges: SourceRange[] = splitRangeByLines(outerRange)
144156
const exclusionsSplitByLine = rangesToExclude.flatMap((exclusion) => splitRangeByLines(exclusion))
145157

146158
exclusionsSplitByLine.forEach((exclusion) => {
147159
const lineIndex = exclusion.start.line
148-
const lineLength = codeLines[lineIndex].length
160+
const lineLength = codeLines?.[lineIndex].length ?? Infinity
149161
const exclusionStartColumn = exclusion.start.column ?? 0
150162
const exclusionEndColumn = exclusion.end.column ?? lineLength
151163
for (let i = remainingRanges.length - 1; i >= 0; i--) {

0 commit comments

Comments
 (0)