1- import { cloneRange , excludeRangesFromOuterRange , mergeIntersectingOrAdjacentRanges , rangesAreEqual , splitRangeByLines } from '../internal/ranges'
1+ import { cloneRange , excludeRangesFromOuterRange , isEmptyRange , makeRangeEmpty , mergeIntersectingOrAdjacentRanges , rangesAreEqual , splitRangeByLines } from '../internal/ranges'
22import { excludeWhitespaceRanges , getTextContentInLine } from '../internal/text-content'
3- import type { AnnotatedCode , AnnotationComment , SourceLocation , SourceRange } from './types'
3+ import type { AnnotatedCode , AnnotationComment , SourceRange } from './types'
44
55export 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 = {
3644type SourceChange = RemoveLine | EditLine
3745
3846export 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
148184function getRangeRemovalChanges ( codeLines : string [ ] , range : SourceRange ) : SourceChange [ ] {
0 commit comments