Skip to content

Commit 3a4bb8b

Browse files
committed
Merge SourceEdit and IncrementalEdit
1 parent 728e2f6 commit 3a4bb8b

File tree

7 files changed

+200
-171
lines changed

7 files changed

+200
-171
lines changed

Sources/SwiftIDEUtils/FixItApplier.swift

-4
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,6 @@ private extension SourceEdit {
103103
return range.upperBound.utf8Offset
104104
}
105105

106-
var replacementLength: Int {
107-
return replacement.utf8.count
108-
}
109-
110106
var replacementRange: Range<Int> {
111107
return startUtf8Offset..<endUtf8Offset
112108
}

Sources/SwiftParser/IncrementalParseTransition.swift

+32-30
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ struct IncrementalParseLookup {
162162

163163
// Fast path check: if parser is past all the edits then any matching node
164164
// can be re-used.
165-
if !edits.edits.isEmpty && edits.edits.last!.range.endOffset < node.position.utf8Offset {
165+
if !edits.edits.isEmpty && edits.edits.last!.range.upperBound.utf8Offset < node.position.utf8Offset {
166166
return true
167167
}
168168

@@ -172,15 +172,15 @@ struct IncrementalParseLookup {
172172
return false
173173
}
174174

175-
let nodeAffectRange = ByteSourceRange(
176-
offset: node.position.utf8Offset,
177-
length: nodeAffectRangeLength
178-
)
175+
let nodeAffectRange =
176+
AbsolutePosition(
177+
utf8Offset: node.position.utf8Offset
178+
)..<AbsolutePosition(utf8Offset: node.position.utf8Offset + nodeAffectRangeLength)
179179

180180
for edit in edits.edits {
181181
// Check if this node or the trivia of the next node has been edited. If
182182
// it has, we cannot reuse it.
183-
if edit.range.offset > nodeAffectRange.endOffset {
183+
if edit.range.lowerBound.utf8Offset > nodeAffectRange.upperBound.utf8Offset {
184184
// Remaining edits don't affect the node. (Edits are sorted)
185185
break
186186
}
@@ -195,16 +195,16 @@ struct IncrementalParseLookup {
195195
fileprivate func translateToPreEditOffset(_ postEditOffset: Int) -> Int? {
196196
var offset = postEditOffset
197197
for edit in edits.edits {
198-
if edit.range.offset > offset {
198+
if edit.range.lowerBound.utf8Offset > offset {
199199
// Remaining edits doesn't affect the position. (Edits are sorted)
200200
break
201201
}
202-
if edit.range.offset + edit.replacementLength > offset {
202+
if edit.range.lowerBound.utf8Offset + edit.replacementLength > offset {
203203
// This is a position inserted by the edit, and thus doesn't exist in
204204
// the pre-edit version of the file.
205205
return nil
206206
}
207-
offset = offset - edit.replacementLength + edit.range.length
207+
offset = offset - edit.replacementLength + edit.range.length.utf8Length
208208
}
209209
return offset
210210
}
@@ -303,11 +303,11 @@ public struct ConcurrentEdits: Sendable {
303303

304304
/// The raw concurrent edits. Are guaranteed to satisfy the requirements
305305
/// stated above.
306-
public let edits: [IncrementalEdit]
306+
public let edits: [SourceEdit]
307307

308308
/// Initialize this struct from edits that are already in a concurrent form
309309
/// and are guaranteed to satisfy the requirements posed above.
310-
public init(concurrent: [IncrementalEdit]) throws {
310+
public init(concurrent: [SourceEdit]) throws {
311311
if !Self.isValidConcurrentEditArray(concurrent) {
312312
throw ConcurrentEditsError.editsNotConcurrent
313313
}
@@ -323,7 +323,7 @@ public struct ConcurrentEdits: Sendable {
323323
/// - insert 'z' at offset 2
324324
/// to '012345' results in 'xyz012345'.
325325

326-
public init(fromSequential sequentialEdits: [IncrementalEdit]) {
326+
public init(fromSequential sequentialEdits: [SourceEdit]) {
327327
do {
328328
try self.init(concurrent: Self.translateSequentialEditsToConcurrentEdits(sequentialEdits))
329329
} catch {
@@ -336,7 +336,7 @@ public struct ConcurrentEdits: Sendable {
336336
/// Construct a concurrent edits struct from a single edit. For a single edit,
337337
/// there is no differentiation between being it being applied concurrently
338338
/// or sequentially.
339-
public init(_ single: IncrementalEdit) {
339+
public init(_ single: SourceEdit) {
340340
do {
341341
try self.init(concurrent: [single])
342342
} catch {
@@ -345,29 +345,31 @@ public struct ConcurrentEdits: Sendable {
345345
}
346346

347347
private static func translateSequentialEditsToConcurrentEdits(
348-
_ edits: [IncrementalEdit]
349-
) -> [IncrementalEdit] {
350-
var concurrentEdits: [IncrementalEdit] = []
348+
_ edits: [SourceEdit]
349+
) -> [SourceEdit] {
350+
var concurrentEdits: [SourceEdit] = []
351351
for editToAdd in edits {
352352
var editToAdd = editToAdd
353353
var editIndicesMergedWithNewEdit: [Int] = []
354354
for (index, existingEdit) in concurrentEdits.enumerated() {
355-
if existingEdit.replacementRange.intersectsOrTouches(editToAdd.range) {
356-
let intersectionLength =
357-
existingEdit.replacementRange.intersected(editToAdd.range).length
355+
if existingEdit.range.intersectsOrTouches(editToAdd.range),
356+
let intersectionLength = existingEdit.range.intersecting(editToAdd.range)?.length
357+
{
358358
let replacement: [UInt8]
359359
replacement =
360-
existingEdit.replacement.prefix(max(0, editToAdd.offset - existingEdit.replacementRange.offset))
361-
+ editToAdd.replacement
362-
+ existingEdit.replacement.suffix(max(0, existingEdit.replacementRange.endOffset - editToAdd.endOffset))
363-
editToAdd = IncrementalEdit(
360+
existingEdit.replacementBytes.prefix(max(0, editToAdd.offset - existingEdit.replacementRange.offset))
361+
+ editToAdd.replacementBytes
362+
+ existingEdit.replacementBytes.suffix(
363+
max(0, existingEdit.replacementRange.endOffset - editToAdd.endOffset)
364+
)
365+
editToAdd = SourceEdit(
364366
offset: Swift.min(existingEdit.offset, editToAdd.offset),
365-
length: existingEdit.length + editToAdd.length - intersectionLength,
367+
length: existingEdit.length + editToAdd.length - intersectionLength.utf8Length,
366368
replacement: replacement
367369
)
368370
editIndicesMergedWithNewEdit.append(index)
369-
} else if existingEdit.offset < editToAdd.endOffset {
370-
editToAdd = IncrementalEdit(
371+
} else if existingEdit.range.lowerBound.utf8Offset < editToAdd.range.upperBound.utf8Offset {
372+
editToAdd = SourceEdit(
371373
offset: editToAdd.offset - existingEdit.replacementLength + existingEdit.length,
372374
length: editToAdd.length,
373375
replacement: editToAdd.replacement
@@ -380,15 +382,15 @@ public struct ConcurrentEdits: Sendable {
380382
}
381383
let insertPos =
382384
concurrentEdits.firstIndex(where: { edit in
383-
editToAdd.endOffset <= edit.offset
385+
editToAdd.range.upperBound.utf8Offset <= edit.range.lowerBound.utf8Offset
384386
}) ?? concurrentEdits.count
385387
concurrentEdits.insert(editToAdd, at: insertPos)
386388
precondition(ConcurrentEdits.isValidConcurrentEditArray(concurrentEdits))
387389
}
388390
return concurrentEdits
389391
}
390392

391-
private static func isValidConcurrentEditArray(_ edits: [IncrementalEdit]) -> Bool {
393+
private static func isValidConcurrentEditArray(_ edits: [SourceEdit]) -> Bool {
392394
// Not quite sure if we should disallow creating an `IncrementalParseTransition`
393395
// object without edits but there doesn't seem to be much benefit if we do,
394396
// and there are 'lit' tests that want to test incremental re-parsing without edits.
@@ -397,7 +399,7 @@ public struct ConcurrentEdits: Sendable {
397399
for i in 1..<edits.count {
398400
let prevEdit = edits[i - 1]
399401
let curEdit = edits[i]
400-
if curEdit.range.offset < prevEdit.range.endOffset {
402+
if curEdit.range.lowerBound.utf8Offset < prevEdit.range.upperBound.utf8Offset {
401403
return false
402404
}
403405
if curEdit.intersectsRange(prevEdit.range) {
@@ -408,7 +410,7 @@ public struct ConcurrentEdits: Sendable {
408410
}
409411

410412
/// **Public for testing purposes only**
411-
public static func _isValidConcurrentEditArray(_ edits: [IncrementalEdit]) -> Bool {
413+
public static func _isValidConcurrentEditArray(_ edits: [SourceEdit]) -> Bool {
412414
return isValidConcurrentEditArray(edits)
413415
}
414416
}

Sources/SwiftSyntax/SourceEdit.swift

+61-9
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -13,22 +13,74 @@
1313
/// A textual edit to the original source represented by a range and a
1414
/// replacement.
1515
public struct SourceEdit: Equatable, Sendable {
16-
/// The half-open range that this edit applies to.
16+
/// The byte range of the original source buffer that the edit applies to.
1717
public let range: Range<AbsolutePosition>
18-
/// The text to replace the original range with. Empty for a deletion.
19-
public let replacement: String
2018

21-
/// Length of the original source range that this edit applies to. Zero if
22-
/// this is an addition.
23-
public var length: SourceLength {
24-
return SourceLength(utf8Length: range.upperBound.utf8Offset - range.lowerBound.utf8Offset)
19+
/// The UTF-8 bytes that should be inserted as part of the edit
20+
public let replacementBytes: [UInt8]
21+
22+
/// A string representation of the replacement
23+
public var replacement: String { return String(decoding: replacementBytes, as: UTF8.self) }
24+
25+
/// The length of the edit replacement in UTF8 bytes.
26+
public var replacementLength: Int { replacement.utf8.count }
27+
28+
@available(*, deprecated, message: "Use range instead")
29+
public var offset: Int { return range.lowerBound.utf8Offset }
30+
31+
@available(*, deprecated, message: "Use replacementLength instead")
32+
public var length: Int { return range.upperBound.utf8Offset - range.lowerBound.utf8Offset }
33+
34+
@available(*, deprecated, message: "Use range instead")
35+
public var endOffset: Int { return range.upperBound.utf8Offset }
36+
37+
/// After the edit has been applied the range of the replacement text.
38+
@available(*, deprecated, message: "Use range instead")
39+
public var replacementRange: ByteSourceRange {
40+
return ByteSourceRange(
41+
offset: range.lowerBound.utf8Offset,
42+
length: range.upperBound.utf8Offset - range.lowerBound.utf8Offset
43+
)
44+
}
45+
46+
@available(*, deprecated, message: "Use SourceEdit(range:replacement:) instead")
47+
public init(range: ByteSourceRange, replacementLength: Int) {
48+
self.range = AbsolutePosition(utf8Offset: range.offset)..<AbsolutePosition(utf8Offset: range.endOffset)
49+
self.replacementBytes = Array(repeating: UInt8(ascii: " "), count: replacementLength)
50+
}
51+
52+
@available(*, deprecated, message: "Use SourceEdit(range:replacement:) instead")
53+
public init(offset: Int, length: Int, replacementLength: Int) {
54+
self.range = AbsolutePosition(utf8Offset: offset)..<AbsolutePosition(utf8Offset: offset + length)
55+
self.replacementBytes = Array(repeating: UInt8(ascii: " "), count: replacementLength)
2556
}
2657

2758
/// Create an edit to replace `range` in the original source with
2859
/// `replacement`.
2960
public init(range: Range<AbsolutePosition>, replacement: String) {
3061
self.range = range
31-
self.replacement = replacement
62+
self.replacementBytes = Array(replacement.utf8)
63+
}
64+
65+
@available(*, deprecated, message: "Use SourceEdit(range:replacement:) instead")
66+
public init(offset: Int, length: Int, replacement: [UInt8]) {
67+
self.range = AbsolutePosition(utf8Offset: offset)..<AbsolutePosition(utf8Offset: offset + length)
68+
self.replacementBytes = replacement
69+
}
70+
71+
@available(*, deprecated, message: "Use SourceEdit(range:replacement:) instead")
72+
public init(offset: Int, length: Int, replacement: String) {
73+
self.init(offset: offset, length: length, replacement: Array(replacement.utf8))
74+
}
75+
76+
public func intersectsOrTouchesRange(_ other: Range<AbsolutePosition>) -> Bool {
77+
return self.range.upperBound.utf8Offset >= other.lowerBound.utf8Offset
78+
&& self.range.lowerBound.utf8Offset <= other.upperBound.utf8Offset
79+
}
80+
81+
public func intersectsRange(_ other: Range<AbsolutePosition>) -> Bool {
82+
return self.range.upperBound.utf8Offset > other.lowerBound.utf8Offset
83+
&& self.range.lowerBound.utf8Offset < other.upperBound.utf8Offset
3284
}
3385

3486
/// Convenience function to create a textual addition after the given node

Sources/SwiftSyntax/Utils.swift

+26-41
Original file line numberDiff line numberDiff line change
@@ -47,57 +47,42 @@ public struct ByteSourceRange: Equatable, Sendable {
4747
}
4848
}
4949

50-
public struct IncrementalEdit: Equatable, Sendable {
51-
/// The byte range of the original source buffer that the edit applies to.
52-
public let range: ByteSourceRange
50+
extension Range<AbsolutePosition> {
51+
/// The number of bytes between the range's lower bound and its upper bound
52+
public var length: SourceLength { return SourceLength(utf8Length: upperBound.utf8Offset - lowerBound.utf8Offset) }
5353

54-
/// The UTF-8 bytes that should be inserted as part of the edit
55-
public let replacement: [UInt8]
56-
57-
/// The length of the edit replacement in UTF8 bytes.
58-
public var replacementLength: Int { replacement.count }
59-
60-
public var offset: Int { return range.offset }
61-
62-
public var length: Int { return range.length }
63-
64-
public var endOffset: Int { return range.endOffset }
65-
66-
/// After the edit has been applied the range of the replacement text.
67-
public var replacementRange: ByteSourceRange {
68-
return ByteSourceRange(offset: offset, length: replacementLength)
69-
}
70-
71-
@available(*, deprecated, message: "Use IncrementalEdit(range:replacement:) instead")
72-
public init(range: ByteSourceRange, replacementLength: Int) {
73-
self.range = range
74-
self.replacement = Array(repeating: UInt8(ascii: " "), count: replacementLength)
54+
public init(position: AbsolutePosition, length: SourceLength) {
55+
self = position..<(position + length)
7556
}
7657

77-
@available(*, deprecated, message: "Use IncrementalEdit(offset:length:replacement:) instead")
78-
public init(offset: Int, length: Int, replacementLength: Int) {
79-
self.range = ByteSourceRange(offset: offset, length: length)
80-
self.replacement = Array(repeating: UInt8(ascii: " "), count: replacementLength)
58+
/// Returns `true` if the intersection between this range and `other` is non-empty or if the two ranges are directly
59+
/// adjacent to each other.
60+
public func intersectsOrTouches(_ other: Range<AbsolutePosition>) -> Bool {
61+
return self.upperBound >= other.lowerBound && self.lowerBound <= other.upperBound
8162
}
8263

83-
public init(offset: Int, length: Int, replacement: [UInt8]) {
84-
self.range = ByteSourceRange(offset: offset, length: length)
85-
self.replacement = replacement
64+
/// Returns `true` if the intersection between this range and `other` is non-empty.
65+
public func intersects(_ other: Range<AbsolutePosition>) -> Bool {
66+
return self.upperBound > other.lowerBound && self.lowerBound < other.upperBound
8667
}
8768

88-
public init(offset: Int, length: Int, replacement: String) {
89-
self.init(offset: offset, length: length, replacement: Array(replacement.utf8))
90-
}
91-
92-
public func intersectsOrTouchesRange(_ other: ByteSourceRange) -> Bool {
93-
return self.range.intersectsOrTouches(other)
94-
}
95-
96-
public func intersectsRange(_ other: ByteSourceRange) -> Bool {
97-
return self.range.intersects(other)
69+
/// Returns the range for the overlapping region between two ranges.
70+
///
71+
/// If the intersection is empty, this returns `nil`.
72+
public func intersecting(_ other: Range<AbsolutePosition>) -> Range<AbsolutePosition>? {
73+
let lowerBound = Swift.max(self.lowerBound, other.lowerBound)
74+
let upperBound = Swift.min(self.upperBound, other.upperBound)
75+
if lowerBound > upperBound {
76+
return nil
77+
} else {
78+
return lowerBound..<upperBound
79+
}
9880
}
9981
}
10082

83+
@available(*, deprecated, renamed: "SourceEdit")
84+
public typealias IncrementalEdit = SourceEdit
85+
10186
extension RawUnexpectedNodesSyntax {
10287
/// Construct a ``RawUnexpectedNodesSyntax``with the given `elements`.
10388
///

Sources/_SwiftSyntaxTestSupport/IncrementalParseTestUtils.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public func extractEditsAndSources(
176176
) -> (edits: ConcurrentEdits, originalSource: Substring, editedSource: Substring) {
177177
var editedSource = Substring()
178178
var originalSource = Substring()
179-
var concurrentEdits: [IncrementalEdit] = []
179+
var concurrentEdits: [SourceEdit] = []
180180

181181
var lastStartIndex = source.startIndex
182182
while let startIndex = source[lastStartIndex...].firstIndex(where: { $0 == "⏩️" }),
@@ -185,7 +185,7 @@ public func extractEditsAndSources(
185185
{
186186

187187
originalSource += source[lastStartIndex..<startIndex]
188-
let edit = IncrementalEdit(
188+
let edit = SourceEdit(
189189
offset: originalSource.utf8.count,
190190
length: source.utf8.distance(
191191
from: source.index(after: startIndex),
@@ -219,7 +219,7 @@ public func extractEditsAndSources(
219219
/// `concurrent` specifies whether the edits should be interpreted as being
220220
/// applied sequentially or concurrently.
221221
public func applyEdits(
222-
_ edits: [IncrementalEdit],
222+
_ edits: [SourceEdit],
223223
concurrent: Bool,
224224
to testString: String
225225
) -> String {
@@ -236,7 +236,7 @@ public func applyEdits(
236236
for edit in edits {
237237
assert(edit.endOffset <= bytes.count)
238238
bytes.removeSubrange(edit.offset..<edit.endOffset)
239-
bytes.insert(contentsOf: edit.replacement, at: edit.offset)
239+
bytes.insert(contentsOf: edit.replacementBytes, at: edit.offset)
240240
}
241241
return String(bytes: bytes, encoding: .utf8)!
242242
}

0 commit comments

Comments
 (0)