diff --git a/Release Notes/600.md b/Release Notes/600.md index 5ea7e9ee2be..ee4e5e5a8c2 100644 --- a/Release Notes/600.md +++ b/Release Notes/600.md @@ -75,22 +75,26 @@ - Description: With the change to parse `#if canImport(MyModule, _version: 1.2.3)` as a function call instead of a dedicated syntax node, `1.2.3` natively gets parsed as a member access `3` to the `1.2` float literal. This property allows the reinterpretation of such an expression as a version tuple. - Pull request: https://github.com/apple/swift-syntax/pull/2025 -- `SyntaxProtocol.node(at:)` +- `SyntaxProtocol.node(at:)` - Description: Given a `SyntaxIdentifier`, returns the `Syntax` node with that identifier - Pull request: https://github.com/apple/swift-syntax/pull/2594 - `SyntaxIdentifier.IndexInTree` - Description: Uniquely identifies a syntax node within a tree. This is similar to ``SyntaxIdentifier`` but does not store the root ID of the tree. It can thus be transferred across trees that are structurally equivalent, for example two copies of the same tree that live in different processes. The only public functions on this type are `toOpaque` and `init(fromOpaque:)`, which allow serialization of the `IndexInTree`. - Pull request: https://github.com/apple/swift-syntax/pull/2594 - + - `SyntaxIdentifier` conformance to `Comparable`: - Description: A `SyntaxIdentifier` compares less than another `SyntaxIdentifier` if the node at that identifier occurs first during a depth-first traversal of the tree. - Pull request: https://github.com/apple/swift-syntax/pull/2594 -- `SyntaxIdentifier.indexInTree` and `SyntaxIdentifier.fromIndexInTree` +- `SyntaxIdentifier.indexInTree` and `SyntaxIdentifier.fromIndexInTree` - Description: `SyntaxIdentifier.indexInTree` allows the retrieval of a `SyntaxIdentifier` that identifies the syntax node independent of the syntax tree. `SyntaxIdentifier.fromIndexInTree` allows the creation for a `SyntaxIdentifier` from a tree-agnostic `SyntaxIdentifier.IndexInTree` and the tree's root node. - Pull request: https://github.com/apple/swift-syntax/pull/2594 +- `Range` + - Description: `Range` gained a few convenience functions inspired from `ByteSourceRange`: `init(position:length:)`, `length`, `overlapsOrTouches` + - Pull request: https://github.com/apple/swift-syntax/pull/2587 + ## API Behavior Changes ## Deprecations @@ -124,6 +128,18 @@ - 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 +- `SyntaxClassifiedRange.offset`, `length` and `endOffset` + - Description: Deprecated these properties in favor of `range`. + - Pull request: https://github.com/apple/swift-syntax/pull/2587 + +- `SyntaxProtocol.totalByteRange` and `trimmedByteRange` + - Description: Renamed to `range` and `trimmedRange`, both now returning a `Range`. + - Pull request: https://github.com/apple/swift-syntax/pull/2587 + +- `ByteSourceRange` deprecated in favor of `Range` + - Description: `ByteSourceRange` is being dropped for `Range`, where the latter clearly signifies that it uses UTF-8 byte positions. `Range` has deprecated compatibility layers to make it API-compatible with `ByteSourceRange` + - Pull request: https://github.com/apple/swift-syntax/pull/2587 + ## API-Incompatible Changes - `MacroDefinition` used for expanding macros: @@ -177,6 +193,21 @@ - Pull request: https://github.com/apple/swift-syntax/pull/2489 - Migration steps: Stop using this module. +- `ByteSourceRange.length` changed from `Int` to `SourceLength` + - Description: `ByteSourceRange` has been deprecated and declared as a typealias for `Range`. At the same time, `Range` gained `length: SourceLength` that provides type-system information about the kind of length (UTF-8 byte length). + - Pull request: https://github.com/apple/swift-syntax/pull/2587 + +- `IncrementalEdit.replacementLength` changed from `Int` to `SourceLength` + - Description: The type of `IncrementalEdit.replacementLength` has been changed from `Int` to `SourceLength` which provides type-system information about the kind of length (UTF-8 byte length). + - Pull request: https://github.com/apple/swift-syntax/pull/2587 + +## API-Behavior Changes + +- `SyntaxProtocol.classifications(in:)` and `SyntaxProtocol.classification(at:)` take positions relative to the root of the syntax tree instead of relative to the start of the node + - Description: With the deprecation of `ByteSourceRange` in favor of `Range`, the `AbsolutePosition`s passed as the range are measured from the start of the syntax tree instead of the start of the current node. + - Pull request: https://github.com/apple/swift-syntax/pull/2587 + - Migration steps: Pass absolute positions measured from the root of the syntax tree instead of positions relative to the current node. + ## Template - *Affected API or two word description* diff --git a/Sources/SwiftIDEUtils/Syntax+Classifications.swift b/Sources/SwiftIDEUtils/Syntax+Classifications.swift index 3c094975f7c..00730d389ff 100644 --- a/Sources/SwiftIDEUtils/Syntax+Classifications.swift +++ b/Sources/SwiftIDEUtils/Syntax+Classifications.swift @@ -25,12 +25,11 @@ public extension SyntaxProtocol { /// consecutive tokens would have the same classification then a single classified /// range is provided for all of them. var classifications: SyntaxClassifications { - let fullRange = ByteSourceRange(offset: 0, length: totalLength.utf8Length) - return SyntaxClassifications(_syntaxNode, in: fullRange) + return SyntaxClassifications(_syntaxNode, in: self.range) } /// Sequence of ``SyntaxClassifiedRange``s contained in this syntax node within - /// a relative range. + /// a source range. /// /// The provided classified ranges may extend beyond the provided `range`. /// Active classifications (non-`none`) will extend the range to include the @@ -41,9 +40,9 @@ public extension SyntaxProtocol { /// intersect the provided `range`. /// /// - Parameters: - /// - in: The relative byte range to pull ``SyntaxClassifiedRange``s from. + /// - in: The range to pull ``SyntaxClassifiedRange``s from. /// - Returns: Sequence of ``SyntaxClassifiedRange``s. - func classifications(in range: ByteSourceRange) -> SyntaxClassifications { + func classifications(in range: Range) -> SyntaxClassifications { return SyntaxClassifications(_syntaxNode, in: range) } @@ -52,19 +51,20 @@ public extension SyntaxProtocol { /// - at: The relative to the node byte offset. /// - Returns: The ``SyntaxClassifiedRange`` for the offset or nil if the source text /// at the given offset is unclassified. + @available(*, deprecated, message: "Use classification(at: AbsolutePosition) instead.") func classification(at offset: Int) -> SyntaxClassifiedRange? { - let classifications = SyntaxClassifications(_syntaxNode, in: ByteSourceRange(offset: offset, length: 1)) - var iterator = classifications.makeIterator() - return iterator.next() + return classification(at: AbsolutePosition(utf8Offset: offset + self.position.utf8Offset)) } /// The ``SyntaxClassifiedRange`` for an absolute position. /// - Parameters: /// - at: The absolute position. - /// - Returns: The ``SyntaxClassifiedRange`` for the position or nil if the source text + /// - Returns: The ``SyntaxClassifiedRange`` for the position or `nil`` if the source text /// at the given position is unclassified. func classification(at position: AbsolutePosition) -> SyntaxClassifiedRange? { - let relativeOffset = position.utf8Offset - self.position.utf8Offset - return self.classification(at: relativeOffset) + let range = Range(position: position, length: SourceLength(utf8Length: 1)) + let classifications = SyntaxClassifications(_syntaxNode, in: range) + var iterator = classifications.makeIterator() + return iterator.next() } } diff --git a/Sources/SwiftIDEUtils/SyntaxClassifier.swift b/Sources/SwiftIDEUtils/SyntaxClassifier.swift index 7271c7a791c..a39ad367806 100644 --- a/Sources/SwiftIDEUtils/SyntaxClassifier.swift +++ b/Sources/SwiftIDEUtils/SyntaxClassifier.swift @@ -44,7 +44,7 @@ extension TokenSyntax { extension RawTriviaPiece { func classify(offset: Int) -> SyntaxClassifiedRange { - let range = ByteSourceRange(offset: offset, length: byteLength) + let range = AbsolutePosition(utf8Offset: offset).. SyntaxClassifiedRange { - let range = ByteSourceRange(offset: offset, length: text.count) + let range = AbsolutePosition(utf8Offset: offset).. + @available(*, deprecated, message: "Use range.lowerBound.utf8Offset instead") public var offset: Int { return range.offset } - public var length: Int { return range.length } + + @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 } } @@ -110,19 +115,14 @@ private struct ClassificationVisitor { var contextualClassification: (SyntaxClassification, Bool)? } - /// Only tokens within this absolute range will be classified. No - /// classifications will be reported for tokens out of this range. - private var targetRange: ByteSourceRange + /// Only tokens within this range will be classified. + /// No classifications will be reported for tokens out of this range. + private var targetRange: Range var classifications: [SyntaxClassifiedRange] - /// Only classify tokens in `relativeClassificationRange`, where the start - /// offset is relative to `node`. - init(node: Syntax, relativeClassificationRange: ByteSourceRange) { - let range = ByteSourceRange( - offset: node.position.utf8Offset + relativeClassificationRange.offset, - length: relativeClassificationRange.length - ) + /// Only classify tokens in `range`. + init(node: Syntax, range: Range) { self.targetRange = range self.classifications = [] @@ -140,24 +140,22 @@ private struct ClassificationVisitor { } private mutating func report(range: SyntaxClassifiedRange) { - if range.kind == .none && range.length == 0 { + if range.kind == .none && range.range.isEmpty { return } // Merge consecutive classified ranges of the same kind. if let last = classifications.last, last.kind == range.kind, - last.endOffset == range.offset + last.range.upperBound == range.range.lowerBound { - classifications[classifications.count - 1].range = ByteSourceRange( - offset: last.offset, - length: last.length + range.length - ) + classifications[classifications.count - 1].range = + last.range.lowerBound..<(last.range.upperBound + range.range.length) return } - guard range.offset <= targetRange.endOffset, - range.endOffset >= targetRange.offset + guard range.range.lowerBound <= targetRange.upperBound, + range.range.upperBound >= targetRange.lowerBound else { return } @@ -219,10 +217,9 @@ private struct ClassificationVisitor { let layoutNodeTextLength = child.byteLength - child.leadingTriviaByteLength - child.trailingTriviaByteLength let range = SyntaxClassifiedRange( kind: classification.classification, - range: ByteSourceRange( - offset: byteOffset, - length: layoutNodeTextLength - ) + range: AbsolutePosition( + utf8Offset: byteOffset + ).. VisitResult { - guard descriptor.byteOffset < targetRange.endOffset else { + guard descriptor.byteOffset < targetRange.upperBound.utf8Offset else { return .break } - guard descriptor.byteOffset + descriptor.node.byteLength > targetRange.offset else { + guard descriptor.byteOffset + descriptor.node.byteLength > targetRange.lowerBound.utf8Offset else { return .continue } guard SyntaxTreeViewMode.sourceAccurate.shouldTraverse(node: descriptor.node) else { @@ -273,8 +270,8 @@ public struct SyntaxClassifications: Sequence, Sendable { var classifications: [SyntaxClassifiedRange] - public init(_ node: Syntax, in relRange: ByteSourceRange) { - let visitor = ClassificationVisitor(node: node, relativeClassificationRange: relRange) + public init(_ node: Syntax, in range: Range) { + let visitor = ClassificationVisitor(node: node, range: range) self.classifications = visitor.classifications } diff --git a/Sources/SwiftParser/IncrementalParseTransition.swift b/Sources/SwiftParser/IncrementalParseTransition.swift index 10ad4bdf159..3666b227f2a 100644 --- a/Sources/SwiftParser/IncrementalParseTransition.swift +++ b/Sources/SwiftParser/IncrementalParseTransition.swift @@ -23,7 +23,7 @@ extension Parser { } let currentOffset = self.lexemes.offsetToStart(self.currentToken) - if let node = parseLookup!.lookUp(currentOffset, kind: kind) { + if let node = parseLookup!.lookUp(AbsolutePosition(utf8Offset: currentOffset), kind: kind) { self.lexemes.advance(by: node.totalLength.utf8Length, currentToken: &self.currentToken) return node } @@ -117,16 +117,15 @@ struct IncrementalParseLookup { /// has invalidated the previous ``Syntax`` node. /// /// - Parameters: - /// - offset: The byte offset of the source string that is currently parsed. + /// - position: The position in the source string that is currently parsed. /// - kind: The `CSyntaxKind` that the parser expects at this position. /// - Returns: A ``Syntax`` node from the previous parse invocation, /// representing the contents of this region, if it is still valid /// to re-use. `nil` otherwise. - fileprivate mutating func lookUp(_ newOffset: Int, kind: SyntaxKind) -> Syntax? { - guard let prevOffset = translateToPreEditOffset(newOffset) else { + fileprivate mutating func lookUp(_ newPosition: AbsolutePosition, kind: SyntaxKind) -> Syntax? { + guard let prevPosition = translateToPreEditPosition(newPosition) else { return nil } - let prevPosition = AbsolutePosition(utf8Offset: prevOffset) let node = cursorLookup(prevPosition: prevPosition, kind: kind) if let node { reusedCallback?(node) @@ -162,7 +161,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 < node.position { return true } @@ -172,15 +171,12 @@ struct IncrementalParseLookup { return false } - let nodeAffectRange = ByteSourceRange( - offset: node.position.utf8Offset, - length: nodeAffectRangeLength - ) + let nodeAffectRange = node.position.. nodeAffectRange.endOffset { + if edit.range.lowerBound > nodeAffectRange.upperBound { // Remaining edits don't affect the node. (Edits are sorted) break } @@ -192,19 +188,19 @@ struct IncrementalParseLookup { return true } - fileprivate func translateToPreEditOffset(_ postEditOffset: Int) -> Int? { + fileprivate func translateToPreEditPosition(_ postEditOffset: AbsolutePosition) -> AbsolutePosition? { var offset = postEditOffset for edit in edits.edits { - if edit.range.offset > offset { + if edit.range.lowerBound > offset { // Remaining edits doesn't affect the position. (Edits are sorted) break } - if edit.range.offset + edit.replacementLength > offset { + if edit.range.lowerBound + 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 } - offset = offset - edit.replacementLength + edit.range.length + offset = offset + edit.range.length - edit.replacementLength } return offset } @@ -352,24 +348,32 @@ public struct ConcurrentEdits: Sendable { var editToAdd = editToAdd var editIndicesMergedWithNewEdit: [Int] = [] for (index, existingEdit) in concurrentEdits.enumerated() { - if existingEdit.replacementRange.intersectsOrTouches(editToAdd.range) { + if existingEdit.replacementRange.overlapsOrTouches(editToAdd.range) { let intersectionLength = - existingEdit.replacementRange.intersected(editToAdd.range).length + existingEdit.replacementRange.clamped(to: editToAdd.range).length let replacement: [UInt8] replacement = - existingEdit.replacement.prefix(max(0, editToAdd.offset - existingEdit.replacementRange.offset)) + existingEdit.replacement.prefix( + max(0, editToAdd.range.lowerBound.utf8Offset - existingEdit.replacementRange.lowerBound.utf8Offset) + ) + editToAdd.replacement - + existingEdit.replacement.suffix(max(0, existingEdit.replacementRange.endOffset - editToAdd.endOffset)) + + existingEdit.replacement.suffix( + max(0, existingEdit.replacementRange.upperBound.utf8Offset - editToAdd.range.upperBound.utf8Offset) + ) editToAdd = IncrementalEdit( - offset: Swift.min(existingEdit.offset, editToAdd.offset), - length: existingEdit.length + editToAdd.length - intersectionLength, + range: Range( + position: Swift.min(existingEdit.range.lowerBound, editToAdd.range.lowerBound), + length: existingEdit.range.length + editToAdd.range.length - intersectionLength + ), replacement: replacement ) editIndicesMergedWithNewEdit.append(index) - } else if existingEdit.offset < editToAdd.endOffset { + } else if existingEdit.range.lowerBound < editToAdd.range.upperBound { editToAdd = IncrementalEdit( - offset: editToAdd.offset - existingEdit.replacementLength + existingEdit.length, - length: editToAdd.length, + range: Range( + position: editToAdd.range.lowerBound + existingEdit.range.length - existingEdit.replacementLength, + length: editToAdd.range.length + ), replacement: editToAdd.replacement ) } @@ -380,7 +384,7 @@ public struct ConcurrentEdits: Sendable { } let insertPos = concurrentEdits.firstIndex(where: { edit in - editToAdd.endOffset <= edit.offset + editToAdd.range.upperBound <= edit.range.lowerBound }) ?? concurrentEdits.count concurrentEdits.insert(editToAdd, at: insertPos) precondition(ConcurrentEdits.isValidConcurrentEditArray(concurrentEdits)) @@ -397,7 +401,7 @@ public struct ConcurrentEdits: Sendable { for i in 1.. { + return position.. { + return positionAfterSkippingLeadingTrivia.. instead") +public typealias ByteSourceRange = Range - public init(offset: Int, length: Int) { - self.offset = offset - self.length = length +public extension Range { + @available(*, deprecated, message: "Use lowerBound.utf8Offset instead") + var offset: Int { lowerBound.utf8Offset } + + @available(*, deprecated, message: "Construct a Range instead") + init(offset: Int, length: Int) { + self = AbsolutePosition(utf8Offset: offset)..) -> Range { + return self.clamped(to: other) } +} + +extension Range { + /// 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) } - public var endOffset: Int { - return offset + length + public init(position: AbsolutePosition, length: SourceLength) { + self = position..<(position + length) } - public var isEmpty: Bool { - return length == 0 + // 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 overlapsOrTouches(_ other: Range) -> Bool { + return self.upperBound >= other.lowerBound && self.lowerBound <= other.upperBound } - public func intersectsOrTouches(_ other: ByteSourceRange) -> Bool { - return self.endOffset >= other.offset && self.offset <= other.endOffset + /// Returns `true` if the intersection between this range and `other` is non-empty or if the two ranges are directly + /// adjacent to each other. + @available(*, deprecated, renamed: "overlapsOrTouches(_:)") + public func intersectsOrTouches(_ other: Range) -> Bool { + return self.upperBound >= other.lowerBound && self.lowerBound <= other.upperBound } - public func intersects(_ other: ByteSourceRange) -> Bool { - return self.endOffset > other.offset && self.offset < other.endOffset + /// Returns `true` if the intersection between this range and `other` is non-empty. + @available(*, deprecated, renamed: "overlaps(_:)") + public func intersects(_ other: Range) -> Bool { + return self.upperBound > other.lowerBound && self.lowerBound < other.upperBound } - /// Returns the byte range for the overlapping region between two ranges. - public func intersected(_ other: ByteSourceRange) -> ByteSourceRange { - let start = max(self.offset, other.offset) - let end = min(self.endOffset, other.endOffset) - if start > end { - return ByteSourceRange(offset: 0, length: 0) + /// Returns the range for the overlapping region between two ranges. + /// + /// If the intersection is empty, this returns `nil`. + @available(*, deprecated, message: "Use clamped(to:) instead") + public func intersecting(_ other: Range) -> Range? { + let lowerBound = Swift.max(self.lowerBound, other.lowerBound) + let upperBound = Swift.min(self.upperBound, other.upperBound) + if lowerBound > upperBound { + return nil } else { - return ByteSourceRange(offset: start, length: end - start) + return lowerBound.. /// 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 } + /// 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 } - public var length: Int { return range.length } + @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: ByteSourceRange { - return ByteSourceRange(offset: offset, length: replacementLength) + public var replacementRange: Range { + return Range(position: range.lowerBound, length: replacementLength) } @available(*, deprecated, message: "Use IncrementalEdit(range:replacement:) instead") @@ -74,27 +107,27 @@ public struct IncrementalEdit: Equatable, Sendable { self.replacement = Array(repeating: UInt8(ascii: " "), count: replacementLength) } - @available(*, deprecated, message: "Use IncrementalEdit(offset:length:replacement:) instead") + @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(offset: Int, length: Int, replacement: [UInt8]) { - self.range = ByteSourceRange(offset: offset, length: length) + public init(range: Range, replacement: [UInt8]) { + self.range = range self.replacement = replacement } - public init(offset: Int, length: Int, replacement: String) { - self.init(offset: offset, length: length, replacement: Array(replacement.utf8)) + public init(range: Range, replacement: String) { + self.init(range: range, replacement: Array(replacement.utf8)) } - public func intersectsOrTouchesRange(_ other: ByteSourceRange) -> Bool { - return self.range.intersectsOrTouches(other) + public func intersectsOrTouchesRange(_ other: Range) -> Bool { + return self.range.overlapsOrTouches(other) } - public func intersectsRange(_ other: ByteSourceRange) -> Bool { - return self.range.intersects(other) + public func intersectsRange(_ other: Range) -> Bool { + return self.range.overlaps(other) } } diff --git a/Sources/_SwiftSyntaxTestSupport/IncrementalParseTestUtils.swift b/Sources/_SwiftSyntaxTestSupport/IncrementalParseTestUtils.swift index 8a2a65905ee..99384809968 100644 --- a/Sources/_SwiftSyntaxTestSupport/IncrementalParseTestUtils.swift +++ b/Sources/_SwiftSyntaxTestSupport/IncrementalParseTestUtils.swift @@ -99,17 +99,17 @@ public func assertIncrementalParse( var lastRangeUpperBound = originalString.startIndex for expectedReusedNode in expectedReusedNodes { - guard let range = byteSourceRange(for: expectedReusedNode.source, in: originalString, after: lastRangeUpperBound) + guard let range = positionRange(of: expectedReusedNode.source, in: originalString, after: lastRangeUpperBound) else { XCTFail("Fail to find string in original source,", file: expectedReusedNode.file, line: expectedReusedNode.line) continue } - guard let reusedNode = reusedNodes.first(where: { $0.trimmedByteRange == range }) else { + guard let reusedNode = reusedNodes.first(where: { $0.trimmedRange == range }) else { XCTFail( """ Fail to match the range of \(expectedReusedNode.source) in: - \(reusedNodes.map({"\($0.trimmedByteRange): \($0.description)"}).joined(separator: "\n")) + \(reusedNodes.map({"\($0.trimmedRange): \($0.description)"}).joined(separator: "\n")) """, file: expectedReusedNode.file, line: expectedReusedNode.line @@ -127,16 +127,19 @@ public func assertIncrementalParse( line: expectedReusedNode.line ) - lastRangeUpperBound = originalString.index(originalString.startIndex, offsetBy: range.endOffset) + lastRangeUpperBound = originalString.utf8.index(originalString.startIndex, offsetBy: range.upperBound.utf8Offset) } } -public func byteSourceRange(for substring: String, in sourceString: String, after: String.Index) -> ByteSourceRange? { +public func positionRange( + of substring: String, + in sourceString: String, + after: String.Index +) -> Range? { if let range = sourceString[after...].range(of: substring) { - return ByteSourceRange( - offset: sourceString.utf8.distance(from: sourceString.startIndex, to: range.lowerBound), - length: sourceString.utf8.distance(from: range.lowerBound, to: range.upperBound) - ) + let lowerBound = sourceString.utf8.distance(from: sourceString.startIndex, to: range.lowerBound) + let upperBound = sourceString.utf8.distance(from: sourceString.startIndex, to: range.upperBound) + return AbsolutePosition(utf8Offset: lowerBound)..? = nil, expected: [ClassificationSpec], file: StaticString = #filePath, line: UInt = #line @@ -41,12 +41,16 @@ func assertClassification( classifications = classifications.filter { $0.kind != .none } if expected.count != classifications.count { - XCTFail("Expected \(expected.count) re-used nodes but received \(classifications.count)", file: file, line: line) + XCTFail( + "Expected \(expected.count) classifications \(classifications.count): \(classifications)", + file: file, + line: line + ) } var lastRangeUpperBound = source.startIndex for (classification, spec) in zip(classifications, expected) { - guard let range = byteSourceRange(for: spec.source, in: source, after: lastRangeUpperBound) else { + guard let range = positionRange(of: spec.source, in: source, after: lastRangeUpperBound) else { XCTFail("Fail to find string in original source,", file: spec.file, line: spec.line) continue } @@ -71,7 +75,7 @@ func assertClassification( line: spec.line ) - lastRangeUpperBound = source.utf8.index(source.utf8.startIndex, offsetBy: range.endOffset) + lastRangeUpperBound = source.utf8.index(source.utf8.startIndex, offsetBy: range.upperBound.utf8Offset) } } diff --git a/Tests/SwiftIDEUtilsTest/ClassificationTests.swift b/Tests/SwiftIDEUtilsTest/ClassificationTests.swift index 9e0aed2fbe4..6a482d450f9 100644 --- a/Tests/SwiftIDEUtilsTest/ClassificationTests.swift +++ b/Tests/SwiftIDEUtilsTest/ClassificationTests.swift @@ -35,7 +35,7 @@ class ClassificationTests: XCTestCase { assertClassification( "x/*yo*/ ", - in: ByteSourceRange(offset: 1, length: 6), + in: Range(position: AbsolutePosition(utf8Offset: 1), length: SourceLength(utf8Length: 6)), expected: [ ClassificationSpec(source: "x", kind: .identifier), ClassificationSpec(source: "/*yo*/", kind: .blockComment), @@ -49,7 +49,7 @@ class ClassificationTests: XCTestCase { // blah. let x/*yo*/ = 0 """, - in: ByteSourceRange(offset: 7, length: 8), + in: Range(position: AbsolutePosition(utf8Offset: 7), length: SourceLength(utf8Length: 8)), expected: [ ClassificationSpec(source: "// blah.", kind: .lineComment), ClassificationSpec(source: "let", kind: .keyword), @@ -65,21 +65,24 @@ class ClassificationTests: XCTestCase { // blah. let x/*yo*/ = 0 """, - in: ByteSourceRange(offset: 21, length: 2), + in: Range(position: AbsolutePosition(utf8Offset: 21), length: SourceLength(utf8Length: 2)), expected: [] ) } public func testClassificationAt() throws { - let tree = Parser.parse(source: "func foo() {}") - let keyword = try XCTUnwrap(tree.classification(at: 3)) - let identifier = try XCTUnwrap(tree.classification(at: AbsolutePosition(utf8Offset: 6))) + let tree = Parser.parse(source: "func foo /* a */() {}") + let keyword = try XCTUnwrap(tree.classification(at: AbsolutePosition(utf8Offset: 3))) XCTAssertEqual(keyword.kind, .keyword) - XCTAssertEqual(keyword.range, ByteSourceRange(offset: 0, length: 4)) + XCTAssertEqual(keyword.range, Range(position: AbsolutePosition(utf8Offset: 0), length: SourceLength(utf8Length: 4))) + let identifier = try XCTUnwrap(tree.classification(at: AbsolutePosition(utf8Offset: 6))) XCTAssertEqual(identifier.kind, .identifier) - XCTAssertEqual(identifier.range, ByteSourceRange(offset: 5, length: 3)) + XCTAssertEqual( + identifier.range, + Range(position: AbsolutePosition(utf8Offset: 5), length: SourceLength(utf8Length: 3)) + ) } public func testTokenClassification() { diff --git a/Tests/SwiftParserTest/SequentialToConcurrentEditTranslationTests.swift b/Tests/SwiftParserTest/SequentialToConcurrentEditTranslationTests.swift index 4bc26e1406b..a75bd25f6a4 100644 --- a/Tests/SwiftParserTest/SequentialToConcurrentEditTranslationTests.swift +++ b/Tests/SwiftParserTest/SequentialToConcurrentEditTranslationTests.swift @@ -52,7 +52,10 @@ func verifySequentialToConcurrentTranslation( fileprivate extension IncrementalEdit { init(offset: Int, length: Int, replacement: String) { - self.init(offset: offset, length: length, replacement: Array(replacement.utf8)) + self.init( + range: Range(position: AbsolutePosition(utf8Offset: offset), length: SourceLength(utf8Length: length)), + replacement: Array(replacement.utf8) + ) } } @@ -344,7 +347,7 @@ final class TranslateSequentialToConcurrentEditsTests: ParserTestCase { IncrementalEdit( offset: Int.random(in: 0..<32), length: Int.random(in: 0..<32), - replacement: replacementBytes + replacement: String(data: Data(replacementBytes), encoding: .ascii)! ) ) } diff --git a/Tests/SwiftSyntaxTestSupportTest/IncrementalParseTestUtilsTest.swift b/Tests/SwiftSyntaxTestSupportTest/IncrementalParseTestUtilsTest.swift index 8d5223539e6..f2f394e4549 100644 --- a/Tests/SwiftSyntaxTestSupportTest/IncrementalParseTestUtilsTest.swift +++ b/Tests/SwiftSyntaxTestSupportTest/IncrementalParseTestUtilsTest.swift @@ -32,9 +32,18 @@ class IncrementalParseUtilTest: XCTestCase { XCTAssertEqual( concurrentEdits.edits, [ - IncrementalEdit(offset: 0, length: 5, replacement: "struct"), - IncrementalEdit(offset: 27, length: 0, replacement: "let bar = 10"), - IncrementalEdit(offset: 35, length: 13, replacement: ""), + IncrementalEdit( + range: Range(position: AbsolutePosition(utf8Offset: 0), length: SourceLength(utf8Length: 5)), + replacement: "struct" + ), + IncrementalEdit( + range: Range(position: AbsolutePosition(utf8Offset: 27), length: SourceLength(utf8Length: 0)), + replacement: "let bar = 10" + ), + IncrementalEdit( + range: Range(position: AbsolutePosition(utf8Offset: 35), length: SourceLength(utf8Length: 13)), + replacement: "" + ), ] ) @@ -64,7 +73,10 @@ class IncrementalParseUtilTest: XCTestCase { XCTAssertEqual( concurrentEdits.edits, [ - IncrementalEdit(offset: 0, length: 25, replacement: "🎉") + IncrementalEdit( + range: Range(position: AbsolutePosition(utf8Offset: 0), length: SourceLength(utf8Length: 25)), + replacement: "🎉" + ) ] ) } @@ -79,7 +91,10 @@ class IncrementalParseUtilTest: XCTestCase { XCTAssertEqual( concurrentEdits.edits, [ - IncrementalEdit(offset: 0, length: 1, replacement: "👨‍👩‍👧‍👦") + IncrementalEdit( + range: Range(position: AbsolutePosition(utf8Offset: 0), length: SourceLength(utf8Length: 1)), + replacement: "👨‍👩‍👧‍👦" + ) ] ) }