@@ -27,12 +27,15 @@ public enum FixItApplier {
27
27
/// - filterByMessages: An optional array of message strings to filter which Fix-Its to apply.
28
28
/// If `nil`, the first Fix-It from each diagnostic is applied.
29
29
/// - tree: The syntax tree to which the Fix-Its will be applied.
30
+ /// - allowDuplicateInsertions: Whether to apply duplicate insertions.
31
+ /// Defaults to `true`.
30
32
///
31
33
/// - Returns: A `String` representation of the modified syntax tree after applying the Fix-Its.
32
34
public static func applyFixes(
33
35
from diagnostics: [ Diagnostic ] ,
34
36
filterByMessages messages: [ String ] ? ,
35
- to tree: any SyntaxProtocol
37
+ to tree: some SyntaxProtocol ,
38
+ allowDuplicateInsertions: Bool = true
36
39
) -> String {
37
40
let messages = messages ?? diagnostics. compactMap { $0. fixIts. first? . message. message }
38
41
@@ -43,58 +46,103 @@ public enum FixItApplier {
43
46
. filter { messages. contains ( $0. message. message) }
44
47
. flatMap ( \. edits)
45
48
46
- return self . apply ( edits: edits, to: tree)
49
+ return self . apply ( edits: edits, to: tree, allowDuplicateInsertions : allowDuplicateInsertions )
47
50
}
48
51
49
- /// Apply the given edits to the syntax tree.
52
+ /// Applies the given edits to the given syntax tree.
50
53
///
51
54
/// - Parameters:
52
- /// - edits: The edits to apply to the syntax tree
53
- /// - tree: he syntax tree to which the edits should be applied.
54
- /// - Returns: A `String` representation of the modified syntax tree after applying the edits.
55
+ /// - edits: The edits to apply.
56
+ /// - tree: The syntax tree to which the edits should be applied.
57
+ /// - allowDuplicateInsertions: Whether to apply duplicate insertions.
58
+ /// Defaults to `true`.
59
+ ///
60
+ /// - Returns: A `String` representation of the modified syntax tree.
55
61
public static func apply(
56
62
edits: [ SourceEdit ] ,
57
- to tree: any SyntaxProtocol
63
+ to tree: some SyntaxProtocol ,
64
+ allowDuplicateInsertions: Bool = true
58
65
) -> String {
59
66
var edits = edits
60
67
var source = tree. description
61
68
62
- while let edit = edits. first {
63
- edits = Array ( edits. dropFirst ( ) )
69
+ for var editIndex in edits. indices {
70
+ let edit = edits [ editIndex]
71
+
72
+ // Empty edits do nothing.
73
+ guard !edit. isEmpty else {
74
+ continue
75
+ }
76
+
77
+ do {
78
+ let utf8 = source. utf8
79
+ let startIndex = utf8. index ( utf8. startIndex, offsetBy: edit. startUtf8Offset)
80
+ let endIndex = utf8. index ( utf8. startIndex, offsetBy: edit. endUtf8Offset)
81
+
82
+ source. replaceSubrange ( startIndex..< endIndex, with: edit. replacement)
83
+ }
84
+
85
+ // Drop any subsequent edits that conflict with one we just applied, and
86
+ // adjust the range of the rest.
87
+ while edits. formIndex ( after: & editIndex) != edits. endIndex {
88
+ let remainingEdit = edits [ editIndex]
89
+
90
+ // Empty edits do nothing.
91
+ guard !remainingEdit. isEmpty else {
92
+ continue
93
+ }
94
+
95
+ func shouldDropRemainingEdit( ) -> Bool {
96
+ // Insertions never conflict between themselves, unless we were asked
97
+ // to drop duplicate insertions.
98
+ if edit. range. isEmpty && remainingEdit. range. isEmpty {
99
+ if allowDuplicateInsertions {
100
+ return false
101
+ }
64
102
65
- let startIndex = source . utf8 . index ( source . utf8 . startIndex , offsetBy : edit. startUtf8Offset )
66
- let endIndex = source . utf8 . index ( source . utf8 . startIndex , offsetBy : edit . endUtf8Offset )
103
+ return edit == remainingEdit
104
+ }
67
105
68
- source. replaceSubrange ( startIndex..< endIndex, with: edit. replacement)
106
+ // Edits conflict in the following cases:
107
+ //
108
+ // - Their ranges have a common element.
109
+ // - One's range is empty and its lower bound is strictly within the
110
+ // other's range. So 0..<2 also conflicts with 1..<1, but not with
111
+ // 0..<0 or 2..<2.
112
+ //
113
+ return edit. endUtf8Offset > remainingEdit. startUtf8Offset
114
+ && edit. startUtf8Offset < remainingEdit. endUtf8Offset
115
+ }
69
116
70
- edits = edits. compactMap { remainingEdit -> SourceEdit ? in
71
- if remainingEdit. replacementRange. overlaps ( edit. replacementRange) {
72
- // The edit overlaps with the previous edit. We can't apply both
73
- // without conflicts. Apply the one that's listed first and drop the
74
- // later edit.
75
- return nil
117
+ guard !shouldDropRemainingEdit( ) else {
118
+ // Drop the edit by swapping it for an empty one.
119
+ edits [ editIndex] = SourceEdit ( )
120
+ continue
76
121
}
77
122
78
123
// If the remaining edit starts after or at the end of the edit that we just applied,
79
124
// shift it by the current edit's difference in length.
80
125
if edit. endUtf8Offset <= remainingEdit. startUtf8Offset {
81
- let startPosition = AbsolutePosition (
82
- utf8Offset: remainingEdit. startUtf8Offset - edit. replacementRange. count + edit. replacementLength. utf8Length
83
- )
84
- let endPosition = AbsolutePosition (
85
- utf8Offset: remainingEdit. endUtf8Offset - edit. replacementRange. count + edit. replacementLength. utf8Length
86
- )
87
- return SourceEdit ( range: startPosition..< endPosition, replacement: remainingEdit. replacement)
88
- }
126
+ let shift = edit. replacementLength. utf8Length - edit. range. count
127
+ let startPosition = AbsolutePosition ( utf8Offset: remainingEdit. startUtf8Offset + shift)
128
+ let endPosition = AbsolutePosition ( utf8Offset: remainingEdit. endUtf8Offset + shift)
89
129
90
- return remainingEdit
130
+ edits [ editIndex] = SourceEdit ( range: startPosition..< endPosition, replacement: remainingEdit. replacement)
131
+ }
91
132
}
92
133
}
93
134
94
135
return source
95
136
}
96
137
}
97
138
139
+ private extension Collection {
140
+ func formIndex( after index: inout Index ) -> Index {
141
+ self . formIndex ( after: & index) as Void
142
+ return index
143
+ }
144
+ }
145
+
98
146
private extension SourceEdit {
99
147
var startUtf8Offset : Int {
100
148
return range. lowerBound. utf8Offset
@@ -104,7 +152,15 @@ private extension SourceEdit {
104
152
return range. upperBound. utf8Offset
105
153
}
106
154
107
- var replacementRange : Range < Int > {
108
- return startUtf8Offset..< endUtf8Offset
155
+ var isEmpty : Bool {
156
+ self . range. isEmpty && self . replacement. isEmpty
157
+ }
158
+
159
+ init ( ) {
160
+ self = SourceEdit (
161
+ range: AbsolutePosition ( utf8Offset: 0 ) ..< AbsolutePosition ( utf8Offset: 0 ) ,
162
+ replacement: [ ]
163
+ )
164
+ precondition ( self . isEmpty)
109
165
}
110
166
}
0 commit comments