Repository: swiftlang/swift-syntax (swift)

Issue:
Merge `IncrementalEdit` and `SourceEdit`
After https://github.com/apple/swift-syntax/pull/2527, `IncrementalEdit` and `SourceEdit` are more or less equivalent types. Both store a replacement range and a replacement. The only meaningful difference is that `SourceEdit` has `String` as a replacement and `IncrementalEdit` has an array of bytes as a replacement and can thus also represent invalid UTF-8. We should merge them, ideally into a type that is byte-based (because it’s more versatile) but that offers `String`-based accessors.

The best outcome would be if `SourceEdit` could be the type that’s used to represent the edits, `IncrementalEdit` becomes a deprecated `typealias` to `SourceEdit` and `SourceEdit` gains (possibly deprecated) members for all the members that `IncrementalEdit` had, for API compatibility.

Pull request:
Resolves #2532

I'm not sure if this what you envisioned.

Patch:
diff --git a/Release Notes/600.md b/Release Notes/600.md
index ee4e5e5a8c2..360d13e6aeb 100644
--- a/Release Notes/600.md
+++ b/Release Notes/600.md
@@ -124,7 +124,7 @@
   - Description: Types can have multiple specifiers now and the syntax tree has been modified to reflect that.
   - Pull request: https://github.com/apple/swift-syntax/pull/2433
 
-- ` CanImportExprSyntax` and `CanImportVersionInfoSyntax`
+- `CanImportExprSyntax` and `CanImportVersionInfoSyntax`
   - Description: Instead of parsing `canImport` inside `#if` directives as a special expression node, parse it as a functionc call expression. This is in-line with how the `swift(>=6.0)` and `compiler(>=6.0)` directives are parsed.
   - Pull request: https://github.com/apple/swift-syntax/pull/2025
 
@@ -137,7 +137,7 @@
   - Pull request: https://github.com/apple/swift-syntax/pull/2587
 
 - `ByteSourceRange` deprecated in favor of `Range<AbsolutePosition>`
-  - Description:  `ByteSourceRange` is being dropped for `Range<AbsolutePosition>`, where the latter clearly signifies that it uses UTF-8 byte positions. `Range<AbsolutePosition>` has deprecated compatibility layers to make it API-compatible with `ByteSourceRange`
+  - Description: `ByteSourceRange` is being dropped for `Range<AbsolutePosition>`, where the latter clearly signifies that it uses UTF-8 byte positions. `Range<AbsolutePosition>` has deprecated compatibility layers to make it API-compatible with `ByteSourceRange`
   - Pull request: https://github.com/apple/swift-syntax/pull/2587
 
 ## API-Incompatible Changes
diff --git a/Release Notes/601.md b/Release Notes/601.md
index a26d42cde2e..9bf8ee872d9 100644
--- a/Release Notes/601.md
+++ b/Release Notes/601.md
@@ -11,6 +11,11 @@
 
 ## Deprecations
 
+- `IncrementalEdit` deprecated in favor of `SourceEdit`
+  - Description: `IncrementalEdit` is being dropped for `SourceEdit`. `SourceEdit` has deprecated compatibility layers to make it API-compatible with `IncrementalEdit`
+  - Issue: https://github.com/apple/swift-syntax/issues/2532
+  - Pull request: https://github.com/apple/swift-syntax/pull/2604
+
 ## API-Incompatible Changes
 
 - Moved `Radix` and `IntegerLiteralExprSyntax.radix` from `SwiftRefactor` to `SwiftSyntax`.
diff --git a/Sources/SwiftIDEUtils/FixItApplier.swift b/Sources/SwiftIDEUtils/FixItApplier.swift
index 650803ef301..5a755ce2a60 100644
--- a/Sources/SwiftIDEUtils/FixItApplier.swift
+++ b/Sources/SwiftIDEUtils/FixItApplier.swift
@@ -78,10 +78,10 @@ public enum FixItApplier {
         // shift it by the current edit's difference in length.
         if edit.endUtf8Offset <= remainingEdit.startUtf8Offset {
           let startPosition = AbsolutePosition(
-            utf8Offset: remainingEdit.startUtf8Offset - edit.replacementRange.count + edit.replacementLength
+            utf8Offset: remainingEdit.startUtf8Offset - edit.replacementRange.count + edit.replacementLength.utf8Length
           )
           let endPosition = AbsolutePosition(
-            utf8Offset: remainingEdit.endUtf8Offset - edit.replacementRange.count + edit.replacementLength
+            utf8Offset: remainingEdit.endUtf8Offset - edit.replacementRange.count + edit.replacementLength.utf8Length
           )
           return SourceEdit(range: startPosition..<endPosition, replacement: remainingEdit.replacement)
         }
@@ -103,10 +103,6 @@ private extension SourceEdit {
     return range.upperBound.utf8Offset
   }
 
-  var replacementLength: Int {
-    return replacement.utf8.count
-  }
-
   var replacementRange: Range<Int> {
     return startUtf8Offset..<endUtf8Offset
   }
diff --git a/Sources/SwiftParser/IncrementalParseTransition.swift b/Sources/SwiftParser/IncrementalParseTransition.swift
index 3666b227f2a..f24b0815ffb 100644
--- a/Sources/SwiftParser/IncrementalParseTransition.swift
+++ b/Sources/SwiftParser/IncrementalParseTransition.swift
@@ -299,11 +299,11 @@ public struct ConcurrentEdits: Sendable {
 
   /// The raw concurrent edits. Are guaranteed to satisfy the requirements
   /// stated above.
-  public let edits: [IncrementalEdit]
+  public let edits: [SourceEdit]
 
   /// Initialize this struct from edits that are already in a concurrent form
   /// and are guaranteed to satisfy the requirements posed above.
-  public init(concurrent: [IncrementalEdit]) throws {
+  public init(concurrent: [SourceEdit]) throws {
     if !Self.isValidConcurrentEditArray(concurrent) {
       throw ConcurrentEditsError.editsNotConcurrent
     }
@@ -319,7 +319,7 @@ public struct ConcurrentEdits: Sendable {
   ///  - insert 'z' at offset 2
   ///  to '012345' results in 'xyz012345'.
 
-  public init(fromSequential sequentialEdits: [IncrementalEdit]) {
+  public init(fromSequential sequentialEdits: [SourceEdit]) {
     do {
       try self.init(concurrent: Self.translateSequentialEditsToConcurrentEdits(sequentialEdits))
     } catch {
@@ -332,7 +332,7 @@ public struct ConcurrentEdits: Sendable {
   /// Construct a concurrent edits struct from a single edit. For a single edit,
   /// there is no differentiation between being it being applied concurrently
   /// or sequentially.
-  public init(_ single: IncrementalEdit) {
+  public init(_ single: SourceEdit) {
     do {
       try self.init(concurrent: [single])
     } catch {
@@ -341,9 +341,9 @@ public struct ConcurrentEdits: Sendable {
   }
 
   private static func translateSequentialEditsToConcurrentEdits(
-    _ edits: [IncrementalEdit]
-  ) -> [IncrementalEdit] {
-    var concurrentEdits: [IncrementalEdit] = []
+    _ edits: [SourceEdit]
+  ) -> [SourceEdit] {
+    var concurrentEdits: [SourceEdit] = []
     for editToAdd in edits {
       var editToAdd = editToAdd
       var editIndicesMergedWithNewEdit: [Int] = []
@@ -353,14 +353,14 @@ public struct ConcurrentEdits: Sendable {
             existingEdit.replacementRange.clamped(to: editToAdd.range).length
           let replacement: [UInt8]
           replacement =
-            existingEdit.replacement.prefix(
+            existingEdit.replacementBytes.prefix(
               max(0, editToAdd.range.lowerBound.utf8Offset - existingEdit.replacementRange.lowerBound.utf8Offset)
             )
-            + editToAdd.replacement
-            + existingEdit.replacement.suffix(
+            + editToAdd.replacementBytes
+            + existingEdit.replacementBytes.suffix(
               max(0, existingEdit.replacementRange.upperBound.utf8Offset - editToAdd.range.upperBound.utf8Offset)
             )
-          editToAdd = IncrementalEdit(
+          editToAdd = SourceEdit(
             range: Range(
               position: Swift.min(existingEdit.range.lowerBound, editToAdd.range.lowerBound),
               length: existingEdit.range.length + editToAdd.range.length - intersectionLength
@@ -369,7 +369,7 @@ public struct ConcurrentEdits: Sendable {
           )
           editIndicesMergedWithNewEdit.append(index)
         } else if existingEdit.range.lowerBound < editToAdd.range.upperBound {
-          editToAdd = IncrementalEdit(
+          editToAdd = SourceEdit(
             range: Range(
               position: editToAdd.range.lowerBound + existingEdit.range.length - existingEdit.replacementLength,
               length: editToAdd.range.length
@@ -392,7 +392,7 @@ public struct ConcurrentEdits: Sendable {
     return concurrentEdits
   }
 
-  private static func isValidConcurrentEditArray(_ edits: [IncrementalEdit]) -> Bool {
+  private static func isValidConcurrentEditArray(_ edits: [SourceEdit]) -> Bool {
     // Not quite sure if we should disallow creating an `IncrementalParseTransition`
     // object without edits but there doesn't seem to be much benefit if we do,
     // and there are 'lit' tests that want to test incremental re-parsing without edits.
@@ -412,7 +412,7 @@ public struct ConcurrentEdits: Sendable {
   }
 
   /// **Public for testing purposes only**
-  public static func _isValidConcurrentEditArray(_ edits: [IncrementalEdit]) -> Bool {
+  public static func _isValidConcurrentEditArray(_ edits: [SourceEdit]) -> Bool {
     return isValidConcurrentEditArray(edits)
   }
 }
diff --git a/Sources/SwiftSyntax/SourceEdit.swift b/Sources/SwiftSyntax/SourceEdit.swift
index 7cad887ad38..8b65e97708d 100644
--- a/Sources/SwiftSyntax/SourceEdit.swift
+++ b/Sources/SwiftSyntax/SourceEdit.swift
@@ -2,7 +2,7 @@
 //
 // This source file is part of the Swift.org open source project
 //
-// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
+// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
 // Licensed under Apache License v2.0 with Runtime Library Exception
 //
 // See https://swift.org/LICENSE.txt for license information
@@ -13,22 +13,75 @@
 /// A textual edit to the original source represented by a range and a
 /// replacement.
 public struct SourceEdit: Equatable, Sendable {
-  /// The half-open range that this edit applies to.
+  /// The byte range of the original source buffer that the edit applies to.
   public let range: Range<AbsolutePosition>
-  /// The text to replace the original range with. Empty for a deletion.
-  public let replacement: String
 
-  /// Length of the original source range that this edit applies to. Zero if
-  /// this is an addition.
-  public var length: SourceLength {
-    return SourceLength(utf8Length: range.upperBound.utf8Offset - range.lowerBound.utf8Offset)
+  /// The UTF-8 bytes that should be inserted as part of the edit
+  public let replacementBytes: [UInt8]
+
+  /// A string representation of the replacement
+  public var replacement: String { return String(decoding: replacementBytes, as: UTF8.self) }
+
+  /// The length of the edit replacement in UTF8 bytes.
+  public var replacementLength: SourceLength { return SourceLength(utf8Length: replacementBytes.count) }
+
+  @available(*, deprecated, renamed: "range.lowerBound.utf8Offset")
+  public var offset: Int { return range.offset }
+
+  @available(*, deprecated, renamed: "replacementLength")
+  public var length: Int { return range.length.utf8Length }
+
+  @available(*, deprecated, renamed: "range.upperBound.utf8Offset")
+  public var endOffset: Int { return range.endOffset }
+
+  /// After the edit has been applied the range of the replacement text.
+  public var replacementRange: Range<AbsolutePosition> {
+    return Range(position: range.lowerBound, length: replacementLength)
   }
 
   /// Create an edit to replace `range` in the original source with
   /// `replacement`.
   public init(range: Range<AbsolutePosition>, replacement: String) {
     self.range = range
-    self.replacement = replacement
+    self.replacementBytes = Array(replacement.utf8)
+  }
+
+  public init(range: Range<AbsolutePosition>, replacement: [UInt8]) {
+    self.range = range
+    self.replacementBytes = replacement
+  }
+
+  @available(*, deprecated, message: "Use SourceEdit(range:replacement:) instead")
+  public init(range: ByteSourceRange, replacementLength: Int) {
+    self.range = AbsolutePosition(utf8Offset: range.offset)..<AbsolutePosition(utf8Offset: range.endOffset)
+    self.replacementBytes = Array(repeating: UInt8(ascii: " "), count: replacementLength)
+  }
+
+  @available(*, deprecated, message: "Use SourceEdit(range:replacement:) instead")
+  public init(offset: Int, length: Int, replacementLength: Int) {
+    self.range = AbsolutePosition(utf8Offset: offset)..<AbsolutePosition(utf8Offset: offset + length)
+    self.replacementBytes = Array(repeating: UInt8(ascii: " "), count: replacementLength)
+  }
+
+  @available(*, deprecated, message: "Use SourceEdit(range:replacement:) instead")
+  public init(offset: Int, length: Int, replacement: [UInt8]) {
+    self.range = AbsolutePosition(utf8Offset: offset)..<AbsolutePosition(utf8Offset: offset + length)
+    self.replacementBytes = replacement
+  }
+
+  @available(*, deprecated, message: "Use SourceEdit(range:replacement:) instead")
+  public init(offset: Int, length: Int, replacement: String) {
+    self.init(offset: offset, length: length, replacement: Array(replacement.utf8))
+  }
+
+  public func intersectsOrTouchesRange(_ other: Range<AbsolutePosition>) -> Bool {
+    return self.range.upperBound.utf8Offset >= other.lowerBound.utf8Offset
+      && self.range.lowerBound.utf8Offset <= other.upperBound.utf8Offset
+  }
+
+  public func intersectsRange(_ other: Range<AbsolutePosition>) -> Bool {
+    return self.range.upperBound.utf8Offset > other.lowerBound.utf8Offset
+      && self.range.lowerBound.utf8Offset < other.upperBound.utf8Offset
   }
 
   /// Convenience function to create a textual addition after the given node
diff --git a/Sources/SwiftSyntax/Utils.swift b/Sources/SwiftSyntax/Utils.swift
index 93a981e2116..3be00281ca9 100644
--- a/Sources/SwiftSyntax/Utils.swift
+++ b/Sources/SwiftSyntax/Utils.swift
@@ -10,6 +10,9 @@
 //
 //===----------------------------------------------------------------------===//
 
+@available(*, deprecated, renamed: "SourceEdit")
+public typealias IncrementalEdit = SourceEdit
+
 @available(*, deprecated, message: "Use Range<AbsolutePosition> instead")
 public typealias ByteSourceRange = Range<AbsolutePosition>
 
@@ -77,60 +80,6 @@ extension Range<AbsolutePosition> {
   }
 }
 
-public struct IncrementalEdit: Equatable, Sendable {
-  /// The byte range of the original source buffer that the edit applies to.
-  public let range: Range<AbsolutePosition>
-
-  /// The UTF-8 bytes that should be inserted as part of the edit
-  public let replacement: [UInt8]
-
-  /// The length of the edit replacement in UTF-8 bytes.
-  public var replacementLength: SourceLength { SourceLength(utf8Length: replacement.count) }
-
-  @available(*, deprecated, message: "Use range.lowerBound.utf8Offset instead")
-  public var offset: Int { return range.offset }
-
-  @available(*, deprecated, message: "Use range.utf8Length instead")
-  public var length: Int { return range.length.utf8Length }
-
-  @available(*, deprecated, message: "Use range.upperBound.utf8Offset instead")
-  public var endOffset: Int { return range.endOffset }
-
-  /// After the edit has been applied the range of the replacement text.
-  public var replacementRange: Range<AbsolutePosition> {
-    return Range(position: range.lowerBound, length: replacementLength)
-  }
-
-  @available(*, deprecated, message: "Use IncrementalEdit(range:replacement:) instead")
-  public init(range: ByteSourceRange, replacementLength: Int) {
-    self.range = range
-    self.replacement = Array(repeating: UInt8(ascii: " "), count: replacementLength)
-  }
-
-  @available(*, deprecated, message: "Use IncrementalEdit(range:replacement:) instead")
-  public init(offset: Int, length: Int, replacementLength: Int) {
-    self.range = ByteSourceRange(offset: offset, length: length)
-    self.replacement = Array(repeating: UInt8(ascii: " "), count: replacementLength)
-  }
-
-  public init(range: Range<AbsolutePosition>, replacement: [UInt8]) {
-    self.range = range
-    self.replacement = replacement
-  }
-
-  public init(range: Range<AbsolutePosition>, replacement: String) {
-    self.init(range: range, replacement: Array(replacement.utf8))
-  }
-
-  public func intersectsOrTouchesRange(_ other: Range<AbsolutePosition>) -> Bool {
-    return self.range.overlapsOrTouches(other)
-  }
-
-  public func intersectsRange(_ other: Range<AbsolutePosition>) -> Bool {
-    return self.range.overlaps(other)
-  }
-}
-
 extension RawUnexpectedNodesSyntax {
   /// Construct a ``RawUnexpectedNodesSyntax``with the given `elements`.
   ///


Test patch:
diff --git a/Sources/_SwiftSyntaxTestSupport/IncrementalParseTestUtils.swift b/Sources/_SwiftSyntaxTestSupport/IncrementalParseTestUtils.swift
index 99384809968..c8fdf6b37f8 100644
--- a/Sources/_SwiftSyntaxTestSupport/IncrementalParseTestUtils.swift
+++ b/Sources/_SwiftSyntaxTestSupport/IncrementalParseTestUtils.swift
@@ -179,7 +179,7 @@ public func extractEditsAndSources(
 ) -> (edits: ConcurrentEdits, originalSource: Substring, editedSource: Substring) {
   var editedSource = Substring()
   var originalSource = Substring()
-  var concurrentEdits: [IncrementalEdit] = []
+  var concurrentEdits: [SourceEdit] = []
 
   var lastStartIndex = source.startIndex
   while let startIndex = source[lastStartIndex...].firstIndex(where: { $0 == "⏩️" }),
@@ -188,7 +188,7 @@ public func extractEditsAndSources(
   {
 
     originalSource += source[lastStartIndex..<startIndex]
-    let edit = IncrementalEdit(
+    let edit = SourceEdit(
       range: Range(
         position: AbsolutePosition(utf8Offset: originalSource.utf8.count),
         length: SourceLength(utf8Length: source.utf8.distance(from: source.index(after: startIndex), to: separateIndex))
@@ -221,7 +221,7 @@ public func extractEditsAndSources(
 /// `concurrent` specifies whether the edits should be interpreted as being
 /// applied sequentially or concurrently.
 public func applyEdits(
-  _ edits: [IncrementalEdit],
+  _ edits: [SourceEdit],
   concurrent: Bool,
   to testString: String
 ) -> String {
@@ -238,7 +238,7 @@ public func applyEdits(
   for edit in edits {
     assert(edit.range.upperBound.utf8Offset <= bytes.count)
     bytes.removeSubrange(edit.range.lowerBound.utf8Offset..<edit.range.upperBound.utf8Offset)
-    bytes.insert(contentsOf: edit.replacement, at: edit.range.lowerBound.utf8Offset)
+    bytes.insert(contentsOf: edit.replacementBytes, at: edit.range.lowerBound.utf8Offset)
   }
   return String(bytes: bytes, encoding: .utf8)!
 }
diff --git a/Tests/SwiftParserTest/SequentialToConcurrentEditTranslationTests.swift b/Tests/SwiftParserTest/SequentialToConcurrentEditTranslationTests.swift
index 26b4e15b9b8..b91c8f39406 100644
--- a/Tests/SwiftParserTest/SequentialToConcurrentEditTranslationTests.swift
+++ b/Tests/SwiftParserTest/SequentialToConcurrentEditTranslationTests.swift
@@ -34,8 +34,8 @@ let longString = """
 ///  2. applying the `sequential` and concurrent edits to `testString` results
 ///     in the same post-edit string
 func verifySequentialToConcurrentTranslation(
-  _ sequential: [IncrementalEdit],
-  _ expectedConcurrent: [IncrementalEdit],
+  _ sequential: [SourceEdit],
+  _ expectedConcurrent: [SourceEdit],
   testString: String = longString,
   file: StaticString = #filePath,
   line: UInt = #line
@@ -50,15 +50,6 @@ func verifySequentialToConcurrentTranslation(
   )
 }
 
-fileprivate extension IncrementalEdit {
-  init(offset: Int, length: Int, replacement: String) {
-    self.init(
-      range: Range(position: AbsolutePosition(utf8Offset: offset), length: SourceLength(utf8Length: length)),
-      replacement: Array(replacement.utf8)
-    )
-  }
-}
-
 final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
   func testEmpty() {
     verifySequentialToConcurrentTranslation([], [])
@@ -68,8 +59,14 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
     XCTAssertThrowsError(
       try {
         try ConcurrentEdits(concurrent: [
-          IncrementalEdit(offset: 5, length: 1, replacement: ""),
-          IncrementalEdit(offset: 5, length: 1, replacement: ""),
+          SourceEdit(
+            range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+            replacement: ""
+          ),
+          SourceEdit(
+            range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+            replacement: ""
+          ),
         ])
       }()
     )
@@ -78,10 +75,16 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
   func testSingleEdit1() {
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 5, length: 1, replacement: "")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+          replacement: ""
+        )
       ],
       [
-        IncrementalEdit(offset: 5, length: 1, replacement: "")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+          replacement: ""
+        )
       ]
     )
   }
@@ -89,10 +92,16 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
   func testSingleEdit2() {
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 5, length: 0, replacement: "1")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+          replacement: "1"
+        )
       ],
       [
-        IncrementalEdit(offset: 5, length: 0, replacement: "1")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+          replacement: "1"
+        )
       ]
     )
   }
@@ -100,12 +109,24 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
   func testTwoNonOverlappingDeletesInFrontToBackOrder() {
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 5, length: 1, replacement: ""),
-        IncrementalEdit(offset: 10, length: 2, replacement: ""),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+          replacement: ""
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 10), length: SourceLength(utf8Length: 2)),
+          replacement: ""
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 1, replacement: ""),
-        IncrementalEdit(offset: 11, length: 2, replacement: ""),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+          replacement: ""
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 11), length: SourceLength(utf8Length: 2)),
+          replacement: ""
+        ),
       ]
     )
   }
@@ -113,12 +134,24 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
   func testTwoNonOverlappingDeletesInBackToFrontOrder() {
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 10, length: 2, replacement: ""),
-        IncrementalEdit(offset: 5, length: 1, replacement: ""),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 10), length: SourceLength(utf8Length: 2)),
+          replacement: ""
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+          replacement: ""
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 1, replacement: ""),
-        IncrementalEdit(offset: 10, length: 2, replacement: ""),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+          replacement: ""
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 10), length: SourceLength(utf8Length: 2)),
+          replacement: ""
+        ),
       ]
     )
   }
@@ -126,12 +159,24 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
   func testTwoNonOverlappingInsertionsInFrontToBackOrder() {
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 5, length: 0, replacement: "12"),
-        IncrementalEdit(offset: 10, length: 0, replacement: "345"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 0)),
+          replacement: "12"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 10), length: SourceLength(utf8Length: 0)),
+          replacement: "345"
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 0, replacement: "12"),
-        IncrementalEdit(offset: 8, length: 0, replacement: "345"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 0)),
+          replacement: "12"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 8), length: SourceLength(utf8Length: 0)),
+          replacement: "345"
+        ),
       ]
     )
   }
@@ -139,12 +184,24 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
   func testTwoNonOverlappingInsertionsInBackToFrontOrder() {
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 10, length: 0, replacement: "123"),
-        IncrementalEdit(offset: 5, length: 0, replacement: "45"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 10), length: SourceLength(utf8Length: 0)),
+          replacement: "123"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 0)),
+          replacement: "45"
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 0, replacement: "45"),
-        IncrementalEdit(offset: 10, length: 0, replacement: "123"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 0)),
+          replacement: "45"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 10), length: SourceLength(utf8Length: 0)),
+          replacement: "123"
+        ),
       ]
     )
   }
@@ -152,12 +209,24 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
   func testTwoNonOverlappingReplacementsInFrontToBackOrder() {
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 5, length: 4, replacement: "12"),
-        IncrementalEdit(offset: 20, length: 5, replacement: "345"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 4)),
+          replacement: "12"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 20), length: SourceLength(utf8Length: 5)),
+          replacement: "345"
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 4, replacement: "12"),
-        IncrementalEdit(offset: 22, length: 5, replacement: "345"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 4)),
+          replacement: "12"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 22), length: SourceLength(utf8Length: 5)),
+          replacement: "345"
+        ),
       ]
     )
   }
@@ -165,12 +234,24 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
   func testTwoNonOverlappingReplacementsInBackToFrontOrder() {
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 20, length: 5, replacement: "123"),
-        IncrementalEdit(offset: 5, length: 4, replacement: "45"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 20), length: SourceLength(utf8Length: 5)),
+          replacement: "123"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 4)),
+          replacement: "45"
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 4, replacement: "45"),
-        IncrementalEdit(offset: 20, length: 5, replacement: "123"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 4)),
+          replacement: "45"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 20), length: SourceLength(utf8Length: 5)),
+          replacement: "123"
+        ),
       ]
     )
   }
@@ -178,16 +259,40 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
   func testMultipleNonOverlappingEdits() {
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 0, length: 6, replacement: ""),
-        IncrementalEdit(offset: 15, length: 7, replacement: "123456789"),
-        IncrementalEdit(offset: 10, length: 0, replacement: "abc"),
-        IncrementalEdit(offset: 30, length: 2, replacement: "AB"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 0), length: SourceLength(utf8Length: 6)),
+          replacement: ""
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 15), length: SourceLength(utf8Length: 7)),
+          replacement: "123456789"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 10), length: SourceLength(utf8Length: 0)),
+          replacement: "abc"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 30), length: SourceLength(utf8Length: 2)),
+          replacement: "AB"
+        ),
       ],
       [
-        IncrementalEdit(offset: 0, length: 6, replacement: ""),
-        IncrementalEdit(offset: 16, length: 0, replacement: "abc"),
-        IncrementalEdit(offset: 21, length: 7, replacement: "123456789"),
-        IncrementalEdit(offset: 31, length: 2, replacement: "AB"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 0), length: SourceLength(utf8Length: 6)),
+          replacement: ""
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 16), length: SourceLength(utf8Length: 0)),
+          replacement: "abc"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 21), length: SourceLength(utf8Length: 7)),
+          replacement: "123456789"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 31), length: SourceLength(utf8Length: 2)),
+          replacement: "AB"
+        ),
       ]
     )
   }
@@ -197,11 +302,20 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
     // [--- edit2 ----]
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 10, length: 1, replacement: "xyz"),
-        IncrementalEdit(offset: 5, length: 5, replacement: "a"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 10), length: SourceLength(utf8Length: 1)),
+          replacement: "xyz"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 5)),
+          replacement: "a"
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 6, replacement: "axyz")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 6)),
+          replacement: "axyz"
+        )
       ]
     )
   }
@@ -211,11 +325,20 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
     // [--- edit2 ----]
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 10, length: 1, replacement: "xyz"),
-        IncrementalEdit(offset: 5, length: 5, replacement: ""),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 10), length: SourceLength(utf8Length: 1)),
+          replacement: "xyz"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 5)),
+          replacement: ""
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 6, replacement: "xyz")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 6)),
+          replacement: "xyz"
+        )
       ]
     )
   }
@@ -225,11 +348,20 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
     // [--- edit2 ----]
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 5, length: 3, replacement: "a"),
-        IncrementalEdit(offset: 4, length: 2, replacement: "xy"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 3)),
+          replacement: "a"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 4), length: SourceLength(utf8Length: 2)),
+          replacement: "xy"
+        ),
       ],
       [
-        IncrementalEdit(offset: 4, length: 4, replacement: "xy")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 4), length: SourceLength(utf8Length: 4)),
+          replacement: "xy"
+        )
       ]
     )
   }
@@ -239,11 +371,20 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
     // [------- edit2 --------]
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 5, length: 3, replacement: "a"),
-        IncrementalEdit(offset: 4, length: 4, replacement: "xy"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 3)),
+          replacement: "a"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 4), length: SourceLength(utf8Length: 4)),
+          replacement: "xy"
+        ),
       ],
       [
-        IncrementalEdit(offset: 4, length: 6, replacement: "xy")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 4), length: SourceLength(utf8Length: 6)),
+          replacement: "xy"
+        )
       ]
     )
   }
@@ -253,11 +394,20 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
     // [--- edit2 (length 0) ----]
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 5, length: 0, replacement: "a"),
-        IncrementalEdit(offset: 5, length: 0, replacement: "xy"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 0)),
+          replacement: "a"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 0)),
+          replacement: "xy"
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 0, replacement: "xya")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 0)),
+          replacement: "xya"
+        )
       ]
     )
   }
@@ -267,11 +417,20 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
     // [--- edit2 ----]
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 5, length: 1, replacement: "ab"),
-        IncrementalEdit(offset: 5, length: 1, replacement: "xyz"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+          replacement: "ab"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+          replacement: "xyz"
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 1, replacement: "xyzb")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+          replacement: "xyzb"
+        )
       ]
     )
   }
@@ -281,11 +440,20 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
     // [--- edit2 ----]
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 5, length: 2, replacement: "ab"),
-        IncrementalEdit(offset: 5, length: 1, replacement: "xyz"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 2)),
+          replacement: "ab"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 1)),
+          replacement: "xyz"
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 2, replacement: "xyzb")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 2)),
+          replacement: "xyzb"
+        )
       ]
     )
   }
@@ -295,11 +463,20 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
     //        [--- edit2 ----]
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 5, length: 5, replacement: "ab"),
-        IncrementalEdit(offset: 6, length: 1, replacement: ""),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 5)),
+          replacement: "ab"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 6), length: SourceLength(utf8Length: 1)),
+          replacement: ""
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 5, replacement: "a")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 5)),
+          replacement: "a"
+        )
       ]
     )
   }
@@ -309,11 +486,20 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
     //        [--- edit2 ----]
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 3, length: 3, replacement: "ab"),
-        IncrementalEdit(offset: 4, length: 3, replacement: "xy"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 3), length: SourceLength(utf8Length: 3)),
+          replacement: "ab"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 4), length: SourceLength(utf8Length: 3)),
+          replacement: "xy"
+        ),
       ],
       [
-        IncrementalEdit(offset: 3, length: 5, replacement: "axy")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 3), length: SourceLength(utf8Length: 5)),
+          replacement: "axy"
+        )
       ]
     )
   }
@@ -321,11 +507,20 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
   func testTwoOverlappingInsertions() {
     verifySequentialToConcurrentTranslation(
       [
-        IncrementalEdit(offset: 5, length: 0, replacement: "abc"),
-        IncrementalEdit(offset: 6, length: 0, replacement: "xy"),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 0)),
+          replacement: "abc"
+        ),
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 6), length: SourceLength(utf8Length: 0)),
+          replacement: "xy"
+        ),
       ],
       [
-        IncrementalEdit(offset: 5, length: 0, replacement: "axybc")
+        SourceEdit(
+          range: Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 0)),
+          replacement: "axybc"
+        )
       ]
     )
   }
@@ -336,7 +531,7 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
     var i = 0
     while true {
       i += 1
-      var edits: [IncrementalEdit] = []
+      var edits: [SourceEdit] = []
       let numEdits = Int.random(in: 1..<10)
       for _ in 0..<numEdits {
         let replacementLength = Int.random(in: 0..<32)
@@ -344,9 +539,10 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase {
           (UInt8(ascii: "a")..<UInt8(ascii: "z")).randomElement()!
         }
         edits.append(
-          IncrementalEdit(
-            offset: Int.random(in: 0..<32),
-            length: Int.random(in: 0..<32),
+          SourceEdit(
+            range: AbsolutePosition(
+              utf8Offset: Int.random(in: 0..<32)
+            )..<AbsolutePosition(utf8Offset: Int.random(in: 0..<32)),
             replacement: String(data: Data(replacementBytes), encoding: .ascii)!
           )
         )
diff --git a/Tests/SwiftSyntaxTestSupportTest/IncrementalParseTestUtilsTest.swift b/Tests/SwiftSyntaxTestSupportTest/IncrementalParseTestUtilsTest.swift
index f2f394e4549..5254449a8d6 100644
--- a/Tests/SwiftSyntaxTestSupportTest/IncrementalParseTestUtilsTest.swift
+++ b/Tests/SwiftSyntaxTestSupportTest/IncrementalParseTestUtilsTest.swift
@@ -32,15 +32,15 @@ class IncrementalParseUtilTest: XCTestCase {
     XCTAssertEqual(
       concurrentEdits.edits,
       [
-        IncrementalEdit(
+        SourceEdit(
           range: Range(position: AbsolutePosition(utf8Offset: 0), length: SourceLength(utf8Length: 5)),
           replacement: "struct"
         ),
-        IncrementalEdit(
+        SourceEdit(
           range: Range(position: AbsolutePosition(utf8Offset: 27), length: SourceLength(utf8Length: 0)),
           replacement: "let bar = 10"
         ),
-        IncrementalEdit(
+        SourceEdit(
           range: Range(position: AbsolutePosition(utf8Offset: 35), length: SourceLength(utf8Length: 13)),
           replacement: ""
         ),
@@ -73,7 +73,7 @@ class IncrementalParseUtilTest: XCTestCase {
     XCTAssertEqual(
       concurrentEdits.edits,
       [
-        IncrementalEdit(
+        SourceEdit(
           range: Range(position: AbsolutePosition(utf8Offset: 0), length: SourceLength(utf8Length: 25)),
           replacement: "🎉"
         )
@@ -91,7 +91,7 @@ class IncrementalParseUtilTest: XCTestCase {
     XCTAssertEqual(
       concurrentEdits.edits,
       [
-        IncrementalEdit(
+        SourceEdit(
           range: Range(position: AbsolutePosition(utf8Offset: 0), length: SourceLength(utf8Length: 1)),
           replacement: "👨‍👩‍👧‍👦"
         )