@@ -27,12 +27,15 @@ public enum FixItApplier {
2727 /// - filterByMessages: An optional array of message strings to filter which Fix-Its to apply.
2828 /// If `nil`, the first Fix-It from each diagnostic is applied.
2929 /// - tree: The syntax tree to which the Fix-Its will be applied.
30+ /// - allowDuplicateInsertions: Whether to apply duplicate insertions.
31+ /// Defaults to `true`.
3032 ///
3133 /// - Returns: A `String` representation of the modified syntax tree after applying the Fix-Its.
3234 public static func applyFixes(
3335 from diagnostics: [ Diagnostic ] ,
3436 filterByMessages messages: [ String ] ? ,
35- to tree: any SyntaxProtocol
37+ to tree: some SyntaxProtocol ,
38+ allowDuplicateInsertions: Bool = true
3639 ) -> String {
3740 let messages = messages ?? diagnostics. compactMap { $0. fixIts. first? . message. message }
3841
@@ -43,58 +46,103 @@ public enum FixItApplier {
4346 . filter { messages. contains ( $0. message. message) }
4447 . flatMap ( \. edits)
4548
46- return self . apply ( edits: edits, to: tree)
49+ return self . apply ( edits: edits, to: tree, allowDuplicateInsertions : allowDuplicateInsertions )
4750 }
4851
49- /// Apply the given edits to the syntax tree.
52+ /// Applies the given edits to the given syntax tree.
5053 ///
5154 /// - 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.
5561 public static func apply(
5662 edits: [ SourceEdit ] ,
57- to tree: any SyntaxProtocol
63+ to tree: some SyntaxProtocol ,
64+ allowDuplicateInsertions: Bool = true
5865 ) -> String {
5966 var edits = edits
6067 var source = tree. description
6168
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+ }
64102
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+ }
67105
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+ }
69116
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
76121 }
77122
78123 // If the remaining edit starts after or at the end of the edit that we just applied,
79124 // shift it by the current edit's difference in length.
80125 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)
89129
90- return remainingEdit
130+ edits [ editIndex] = SourceEdit ( range: startPosition..< endPosition, replacement: remainingEdit. replacement)
131+ }
91132 }
92133 }
93134
94135 return source
95136 }
96137}
97138
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+
98146private extension SourceEdit {
99147 var startUtf8Offset : Int {
100148 return range. lowerBound. utf8Offset
@@ -104,7 +152,15 @@ private extension SourceEdit {
104152 return range. upperBound. utf8Offset
105153 }
106154
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)
109165 }
110166}
0 commit comments