Skip to content

Commit

Permalink
Merge SourceEdit and IncrementalEdit
Browse files Browse the repository at this point in the history
  • Loading branch information
kimdv committed Apr 16, 2024
1 parent 728e2f6 commit 32fdeb0
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 163 deletions.
2 changes: 1 addition & 1 deletion Sources/SwiftDiagnostics/FixIt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ extension FixIt {
var existingEdits = [SourceEdit]()
for change in changes {
let edit = change.edit
let isOverlapping = existingEdits.contains { edit.range.overlaps($0.range) }
let isOverlapping = existingEdits.contains { edit.range.intersects($0.range) }
if !isOverlapping {
// The edit overlaps with the previous edit. We can't apply both
// without conflicts. Apply the one that's listed first and drop the
Expand Down
4 changes: 0 additions & 4 deletions Sources/SwiftIDEUtils/FixItApplier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
44 changes: 22 additions & 22 deletions Sources/SwiftParser/IncrementalParseTransition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ struct IncrementalParseLookup {

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

Expand All @@ -172,15 +172,15 @@ struct IncrementalParseLookup {
return false
}

let nodeAffectRange = ByteSourceRange(
offset: node.position.utf8Offset,
length: nodeAffectRangeLength
)
let nodeAffectRange =
AbsolutePosition(
utf8Offset: node.position.utf8Offset
)..<AbsolutePosition(utf8Offset: node.position.utf8Offset + nodeAffectRangeLength)

for edit in edits.edits {
// Check if this node or the trivia of the next node has been edited. If
// it has, we cannot reuse it.
if edit.range.offset > nodeAffectRange.endOffset {
if edit.range.lowerBound.utf8Offset > nodeAffectRange.upperBound.utf8Offset {
// Remaining edits don't affect the node. (Edits are sorted)
break
}
Expand All @@ -195,11 +195,11 @@ struct IncrementalParseLookup {
fileprivate func translateToPreEditOffset(_ postEditOffset: Int) -> Int? {
var offset = postEditOffset
for edit in edits.edits {
if edit.range.offset > offset {
if edit.range.lowerBound.utf8Offset > offset {
// Remaining edits doesn't affect the position. (Edits are sorted)
break
}
if edit.range.offset + edit.replacementLength > offset {
if edit.range.lowerBound.utf8Offset + edit.replacementLength > offset {
// This is a position inserted by the edit, and thus doesn't exist in
// the pre-edit version of the file.
return nil
Expand Down Expand Up @@ -303,11 +303,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
}
Expand All @@ -323,7 +323,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 {
Expand All @@ -336,7 +336,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 {
Expand All @@ -345,9 +345,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] = []
Expand All @@ -360,14 +360,14 @@ public struct ConcurrentEdits: Sendable {
existingEdit.replacement.prefix(max(0, editToAdd.offset - existingEdit.replacementRange.offset))
+ editToAdd.replacement
+ existingEdit.replacement.suffix(max(0, existingEdit.replacementRange.endOffset - editToAdd.endOffset))
editToAdd = IncrementalEdit(
editToAdd = SourceEdit(
offset: Swift.min(existingEdit.offset, editToAdd.offset),
length: existingEdit.length + editToAdd.length - intersectionLength,
replacement: replacement
)
editIndicesMergedWithNewEdit.append(index)
} else if existingEdit.offset < editToAdd.endOffset {
editToAdd = IncrementalEdit(
} else if existingEdit.range.lowerBound.utf8Offset < editToAdd.range.upperBound.utf8Offset {
editToAdd = SourceEdit(
offset: editToAdd.offset - existingEdit.replacementLength + existingEdit.length,
length: editToAdd.length,
replacement: editToAdd.replacement
Expand All @@ -380,15 +380,15 @@ public struct ConcurrentEdits: Sendable {
}
let insertPos =
concurrentEdits.firstIndex(where: { edit in
editToAdd.endOffset <= edit.offset
editToAdd.range.upperBound.utf8Offset <= edit.range.lowerBound.utf8Offset
}) ?? concurrentEdits.count
concurrentEdits.insert(editToAdd, at: insertPos)
precondition(ConcurrentEdits.isValidConcurrentEditArray(concurrentEdits))
}
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.
Expand All @@ -397,7 +397,7 @@ public struct ConcurrentEdits: Sendable {
for i in 1..<edits.count {
let prevEdit = edits[i - 1]
let curEdit = edits[i]
if curEdit.range.offset < prevEdit.range.endOffset {
if curEdit.range.lowerBound.utf8Offset < prevEdit.range.upperBound.utf8Offset {
return false
}
if curEdit.intersectsRange(prevEdit.range) {
Expand All @@ -408,7 +408,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)
}
}
Expand Down
70 changes: 61 additions & 9 deletions Sources/SwiftSyntax/SourceEdit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,22 +13,74 @@
/// 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: Int { replacement.utf8.count }

@available(*, deprecated, message: "Use range instead")
public var offset: Int { return range.lowerBound.utf8Offset }

@available(*, deprecated, message: "Use replacementLength instead")
public var length: Int { return range.upperBound.utf8Offset - range.lowerBound.utf8Offset }

@available(*, deprecated, message: "Use range instead")
public var endOffset: Int { return range.upperBound.utf8Offset }

/// After the edit has been applied the range of the replacement text.
@available(*, deprecated, message: "Use range instead")
public var replacementRange: ByteSourceRange {
return ByteSourceRange(
offset: range.lowerBound.utf8Offset,
length: range.upperBound.utf8Offset - range.lowerBound.utf8Offset
)
}

@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)
}

/// 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)
}

@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
Expand Down
67 changes: 26 additions & 41 deletions Sources/SwiftSyntax/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,57 +47,42 @@ public struct ByteSourceRange: Equatable, Sendable {
}
}

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

/// The UTF-8 bytes that should be inserted as part of the edit
public let replacement: [UInt8]

/// The length of the edit replacement in UTF8 bytes.
public var replacementLength: Int { replacement.count }

public var offset: Int { return range.offset }

public var length: Int { return range.length }

public var endOffset: Int { return range.endOffset }

/// After the edit has been applied the range of the replacement text.
public var replacementRange: ByteSourceRange {
return ByteSourceRange(offset: offset, 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)
public init(position: AbsolutePosition, length: SourceLength) {
self = position..<(position + length)
}

@available(*, deprecated, message: "Use IncrementalEdit(offset:length: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)
/// Returns `true` if the intersection between this range and `other` is non-empty or if the two ranges are directly
/// adjacent to each other.
public func intersectsOrTouches(_ other: Range<AbsolutePosition>) -> Bool {
return self.upperBound >= other.lowerBound && self.lowerBound <= other.upperBound
}

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

public init(offset: Int, length: Int, replacement: String) {
self.init(offset: offset, length: length, replacement: Array(replacement.utf8))
}

public func intersectsOrTouchesRange(_ other: ByteSourceRange) -> Bool {
return self.range.intersectsOrTouches(other)
}

public func intersectsRange(_ other: ByteSourceRange) -> Bool {
return self.range.intersects(other)
/// Returns the range for the overlapping region between two ranges.
///
/// If the intersection is empty, this returns `nil`.
public func intersecting(_ other: Range<AbsolutePosition>) -> Range<AbsolutePosition>? {
let lowerBound = Swift.max(self.lowerBound, other.lowerBound)
let upperBound = Swift.min(self.upperBound, other.upperBound)
if lowerBound > upperBound {
return nil
} else {
return lowerBound..<upperBound
}
}
}

@available(*, deprecated, renamed: "SourceEdit")
public typealias IncrementalEdit = SourceEdit

extension RawUnexpectedNodesSyntax {
/// Construct a ``RawUnexpectedNodesSyntax``with the given `elements`.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,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 == "⏩️" }),
Expand All @@ -185,7 +185,7 @@ public func extractEditsAndSources(
{

originalSource += source[lastStartIndex..<startIndex]
let edit = IncrementalEdit(
let edit = SourceEdit(
offset: originalSource.utf8.count,
length: source.utf8.distance(
from: source.index(after: startIndex),
Expand Down Expand Up @@ -219,7 +219,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 {
Expand Down

0 comments on commit 32fdeb0

Please sign in to comment.