Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AttributedString Index Validity APIs #1177

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,11 @@ extension AttributedString.CharacterView: BidirectionalCollection {
public typealias Index = AttributedString.Index

public var startIndex: AttributedString.Index {
.init(_range.lowerBound)
.init(_range.lowerBound, version: _guts.version)
}

public var endIndex: AttributedString.Index {
.init(_range.upperBound)
.init(_range.upperBound, version: _guts.version)
}

@_alwaysEmitIntoClient
Expand All @@ -131,14 +131,14 @@ extension AttributedString.CharacterView: BidirectionalCollection {

public func index(before i: AttributedString.Index) -> AttributedString.Index {
precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds")
let j = Index(_guts.string.index(before: i._value))
let j = Index(_guts.string.index(before: i._value), version: _guts.version)
precondition(j >= startIndex, "Can't advance AttributedString index before start index")
return j
}

public func index(after i: AttributedString.Index) -> AttributedString.Index {
precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds")
let j = Index(_guts.string.index(after: i._value))
let j = Index(_guts.string.index(after: i._value), version: _guts.version)
precondition(j <= endIndex, "Can't advance AttributedString index after end index")
return j
}
Expand All @@ -157,7 +157,7 @@ extension AttributedString.CharacterView: BidirectionalCollection {
@usableFromInline
internal func _index(_ i: AttributedString.Index, offsetBy distance: Int) -> AttributedString.Index {
precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds")
let j = Index(_guts.string.index(i._value, offsetBy: distance))
let j = Index(_guts.string.index(i._value, offsetBy: distance), version: _guts.version)
precondition(j >= startIndex && j <= endIndex, "AttributedString index out of bounds")
return j
}
Expand Down Expand Up @@ -192,7 +192,7 @@ extension AttributedString.CharacterView: BidirectionalCollection {
}
precondition(j >= startIndex._value && j <= endIndex._value,
"AttributedString index out of bounds")
return Index(j)
return Index(j, version: _guts.version)
}

@_alwaysEmitIntoClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ extension AttributedString {
typealias _AttributeValue = AttributedString._AttributeValue
typealias _AttributeStorage = AttributedString._AttributeStorage

var version: Version
var string: BigString
var runs: _InternalRuns

// Note: the caller is responsible for performing attribute fix-ups if needed based on the source of the runs
init(string: BigString, runs: _InternalRuns) {
precondition(string.isEmpty == runs.isEmpty, "An empty attributed string should not contain any runs")
self.version = Self.createNewVersion()
self.string = string
self.runs = runs
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 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
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

#if canImport(Synchronization)
internal import Synchronization
#endif

extension AttributedString.Guts {
typealias Version = UInt

#if canImport(Synchronization)
private static let _nextVersion = Atomic<Version>(0)

static func createNewVersion() -> Version {
_nextVersion.wrappingAdd(1, ordering: .relaxed).oldValue
}
#else
private static let _nextVersion = LockedState<Version>(initialState: 0)

static func createNewVersion() -> Version {
_nextVersion.withLock { value in
defer {
value &+= 1
}
return value
}
}
#endif

func incrementVersion() {
self.version = Self.createNewVersion()
}
}

// MARK: - Public API

@available(FoundationPreview 6.2, *)
extension AttributedString.Index {
public func isValid(within text: some AttributedStringProtocol) -> Bool {
self._version == text.__guts.version &&
self >= text.startIndex &&
self < text.endIndex
}

public func isValid(within text: DiscontiguousAttributedSubstring) -> Bool {
self._version == text._guts.version &&
text._indices.contains(self._value)
}
}

@available(FoundationPreview 6.2, *)
extension Range<AttributedString.Index> {
public func isValid(within text: some AttributedStringProtocol) -> Bool {
self.lowerBound._version == text.__guts.version &&
self.lowerBound >= text.startIndex &&
self.lowerBound <= text.endIndex &&
self.upperBound._version == text.__guts.version &&
self.upperBound >= text.startIndex &&
self.upperBound <= text.endIndex
}

public func isValid(within text: DiscontiguousAttributedSubstring) -> Bool {
let endIndex = text._indices.ranges.last?.upperBound
return self.lowerBound._version == text._guts.version &&
(self.lowerBound._value == endIndex || text._indices.contains(self.lowerBound._value)) &&
self.upperBound._version == text._guts.version &&
(self.upperBound._value == endIndex || text._indices.contains(self.upperBound._value))
}
}

@available(FoundationPreview 6.2, *)
extension RangeSet<AttributedString.Index> {
public func isValid(within text: some AttributedStringProtocol) -> Bool {
self.ranges.allSatisfy {
$0.isValid(within: text)
}
}

public func isValid(within text: DiscontiguousAttributedSubstring) -> Bool {
self.ranges.allSatisfy {
$0.isValid(within: text)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ extension AttributedString.Runs {
}

public var startIndex: Index {
Index(runs.startIndex._stringIndex!)
Index(runs.startIndex._stringIndex!, version: runs._guts.version)
}

public var endIndex: Index {
Index(runs.endIndex._stringIndex!)
Index(runs.endIndex._stringIndex!, version: runs._guts.version)
}

public func index(before i: Index) -> Index {
Expand Down Expand Up @@ -223,11 +223,11 @@ extension AttributedString.Runs {
}

public var startIndex: Index {
Index(runs.startIndex._stringIndex!)
Index(runs.startIndex._stringIndex!, version: runs._guts.version)
}

public var endIndex: Index {
Index(runs.endIndex._stringIndex!)
Index(runs.endIndex._stringIndex!, version: runs._guts.version)
}

public func index(before i: Index) -> Index {
Expand Down Expand Up @@ -385,11 +385,11 @@ extension AttributedString.Runs {
}

public var startIndex: Index {
Index(runs.startIndex._stringIndex!)
Index(runs.startIndex._stringIndex!, version: runs._guts.version)
}

public var endIndex: Index {
Index(runs.endIndex._stringIndex!)
Index(runs.endIndex._stringIndex!, version: runs._guts.version)
}

public func index(before i: Index) -> Index {
Expand Down Expand Up @@ -557,11 +557,11 @@ extension AttributedString.Runs {
}

public var startIndex: Index {
Index(runs.startIndex._stringIndex!)
Index(runs.startIndex._stringIndex!, version: runs._guts.version)
}

public var endIndex: Index {
Index(runs.endIndex._stringIndex!)
Index(runs.endIndex._stringIndex!, version: runs._guts.version)
}

public func index(before i: Index) -> Index {
Expand Down Expand Up @@ -745,11 +745,11 @@ extension AttributedString.Runs {
}

public var startIndex: Index {
Index(runs.startIndex._stringIndex!)
Index(runs.startIndex._stringIndex!, version: runs._guts.version)
}

public var endIndex: Index {
Index(runs.endIndex._stringIndex!)
Index(runs.endIndex._stringIndex!, version: runs._guts.version)
}

public func index(before i: Index) -> Index {
Expand Down Expand Up @@ -895,11 +895,11 @@ extension AttributedString.Runs {
}

public var startIndex: Index {
Index(_runs.startIndex._stringIndex!)
Index(_runs.startIndex._stringIndex!, version: _runs._guts.version)
}

public var endIndex: Index {
Index(_runs.endIndex._stringIndex!)
Index(_runs.endIndex._stringIndex!, version: _runs._guts.version)
}

public func index(before i: Index) -> Index {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ extension AttributedString.Runs.Run: CustomStringConvertible {
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString.Runs.Run {
public var range: Range<AttributedString.Index> {
let lower = AttributedString.Index(_range.lowerBound)
let upper = AttributedString.Index(_range.upperBound)
let lower = AttributedString.Index(_range.lowerBound, version: _guts.version)
let upper = AttributedString.Index(_range.upperBound, version: _guts.version)
return Range(uncheckedBounds: (lower, upper))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,22 +486,22 @@ extension AttributedString.Runs {
let currentRange = _strBounds.ranges[currentRangeIdx]
if strIndexEnd < currentRange.upperBound {
// The coalesced run ends within the current range, so just look for the next break in the coalesced run
return .init(_guts.string._firstConstraintBreak(in: i._value ..< strIndexEnd, with: constraints))
return .init(_guts.string._firstConstraintBreak(in: i._value ..< strIndexEnd, with: constraints), version: _guts.version)
} else {
// The coalesced run extends beyond our range
// First determine if there's a constraint break to handle
let constraintBreak = _guts.string._firstConstraintBreak(in: i._value ..< currentRange.upperBound, with: constraints)
if constraintBreak == currentRange.upperBound {
if endOfCurrent { return .init(currentRange.upperBound) }
if endOfCurrent { return .init(currentRange.upperBound, version: _guts.version) }
// No constraint break, return the next subrange start or the end index
if currentRangeIdx == _strBounds.ranges.count - 1 {
return .init(currentRange.upperBound)
return .init(currentRange.upperBound, version: _guts.version)
} else {
return .init(_strBounds.ranges[currentRangeIdx + 1].lowerBound)
return .init(_strBounds.ranges[currentRangeIdx + 1].lowerBound, version: _guts.version)
}
} else {
// There is a constraint break before the end of the subrange, so return that break
return .init(constraintBreak)
return .init(constraintBreak, version: _guts.version)
}
}

Expand Down Expand Up @@ -533,18 +533,18 @@ extension AttributedString.Runs {
currentRangeIdx -= 1
currentRange = _strBounds.ranges[currentRangeIdx]
currentStringIdx = currentRange.upperBound
if endOfPrevious { return .init(currentStringIdx) }
if endOfPrevious { return .init(currentStringIdx, version: _guts.version) }
}
let beforeStringIdx = _guts.string.utf8.index(before: currentStringIdx)
let r = _guts.runs.index(atUTF8Offset: beforeStringIdx.utf8Offset)
let startRun = _firstOfMatchingRuns(with: r.index, comparing: attributeNames)
if startRun.utf8Offset >= currentRange.lowerBound.utf8Offset {
// The coalesced run begins within the current range, so just look for the next break in the coalesced run
let runStartStringIdx = _guts.string.utf8.index(beforeStringIdx, offsetBy: startRun.utf8Offset - beforeStringIdx.utf8Offset)
return .init(_guts.string._lastConstraintBreak(in: runStartStringIdx ..< currentStringIdx, with: constraints))
return .init(_guts.string._lastConstraintBreak(in: runStartStringIdx ..< currentStringIdx, with: constraints), version: _guts.version)
} else {
// The coalesced run starts before the current range, and we've already looked back once so we shouldn't look back again
return .init(_guts.string._lastConstraintBreak(in: currentRange.lowerBound ..< currentStringIdx, with: constraints))
return .init(_guts.string._lastConstraintBreak(in: currentRange.lowerBound ..< currentStringIdx, with: constraints), version: _guts.version)
}
}

Expand All @@ -569,7 +569,7 @@ extension AttributedString.Runs {

let j = _guts.string.unicodeScalars.index(after: i._value)
let last = _guts.string._lastConstraintBreak(in: stringStart ..< j, with: constraints)
return (.init(last), r.runIndex)
return (.init(last, version: _guts.version), r.runIndex)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ extension AttributedString.UTF16View: BidirectionalCollection {
public typealias Subsequence = Self

public var startIndex: AttributedString.Index {
.init(_range.lowerBound)
.init(_range.lowerBound, version: _guts.version)
}

public var endIndex: AttributedString.Index {
.init(_range.upperBound)
.init(_range.upperBound, version: _guts.version)
}

public var count: Int {
Expand All @@ -74,21 +74,21 @@ extension AttributedString.UTF16View: BidirectionalCollection {

public func index(before i: AttributedString.Index) -> AttributedString.Index {
precondition(i > startIndex && i <= endIndex, "AttributedString index out of bounds")
let j = Index(_guts.string.utf16.index(before: i._value))
let j = Index(_guts.string.utf16.index(before: i._value), version: _guts.version)
precondition(j >= startIndex, "Can't advance AttributedString index before start index")
return j
}

public func index(after i: AttributedString.Index) -> AttributedString.Index {
precondition(i >= startIndex && i < endIndex, "AttributedString index out of bounds")
let j = Index(_guts.string.utf16.index(after: i._value))
let j = Index(_guts.string.utf16.index(after: i._value), version: _guts.version)
precondition(j <= endIndex, "Can't advance AttributedString index after end index")
return j
}

public func index(_ i: AttributedString.Index, offsetBy distance: Int) -> AttributedString.Index {
precondition(i >= startIndex && i <= endIndex, "AttributedString index out of bounds")
let j = Index(_guts.string.utf16.index(i._value, offsetBy: distance))
let j = Index(_guts.string.utf16.index(i._value, offsetBy: distance), version: _guts.version)
precondition(j >= startIndex && j <= endIndex, "AttributedString index out of bounds")
return j
}
Expand All @@ -107,7 +107,7 @@ extension AttributedString.UTF16View: BidirectionalCollection {
}
precondition(j >= startIndex._value && j <= endIndex._value,
"AttributedString index out of bounds")
return Index(j)
return Index(j, version: _guts.version)
}

public func distance(
Expand Down
Loading