Karoy Lorentey 06ea8b042f
Start using Rope for attribute run storage (#166)
* rdar://108152217 Start using Rope for attribute run storage

- Replace `Array<_InternalRun>` with `Rope<_InternalRun>`.
- Fix semantics of AttributeRunBoundaries.character. It no longer pretends that attributes can be tied to grapheme clusters — that never actually worked properly, and it cannot be supported without breaking the intended use case.
- Remove cache of latest run position; log(n) might be fast enough that we don’t need to worry about that.
- Switch AttributedString.Guts members to take/return BigString.Index values, not AttributedString indices. This cuts down on constant back-and-forth conversions.
- Discard unused Guts members; update remaining mutation methods to follow Swift naming conventions.
- Stop using the `public extension` language misfeature.

* rdar://108152217 Apply notes from code review

AttributedString.CharacterView and AttributedString.UnicodeScalarView
did not use the right semantics in the case when the underlying text
was not touched. It’s okay in that case to not mutate text storage,
but we still want to override the attributes within the subrange as if
the client actually did replace text — as in, we want to use precisely
the same attribute storage for the range on both paths.

Also, make sure we correctly enforce attribute constraints in the
`!hasStringChanges` case, in both views.

* rdar://108152217 Fix logic error in AttributedString.Runs.subscript

(Discovered during code review.)

* rdar://108152217 Clean up CharacterView/UnicodeScalarView mutations & update failing test

* rdar://108152217 Apply review notes
2023-06-26 13:51:15 -07:00

372 lines
13 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2020-2023 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 FOUNDATION_FRAMEWORK
@_implementationOnly @_spi(Unstable) import CollectionsInternal
#else
package import _RopeModule
#endif
@dynamicMemberLookup
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public struct AttributedString : Sendable {
internal var _guts: Guts
internal init(_ guts: Guts) {
_guts = guts
}
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString {
internal static let currentIdentity = LockedState(initialState: 0)
internal static var _nextModifyIdentity : Int {
currentIdentity.withLock { identity in
identity += 1
return identity
}
}
}
// MARK: Initialization
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString {
public init() {
self._guts = Guts()
}
internal init(_ s: some AttributedStringProtocol) {
if let s = _specializingCast(s, to: AttributedString.self) {
self = s
} else if let s = _specializingCast(s, to: AttributedSubstring.self) {
self = AttributedString(s)
} else {
// !!!: We don't expect or want this to happen.
let substring = AttributedSubstring(s.__guts, in: s._stringBounds)
self = AttributedString(substring)
}
}
internal init(_ string: BigString, attributes: _AttributeStorage) {
guard !string.isEmpty else {
self.init()
return
}
var runs = _InternalRuns.Storage()
runs.append(_InternalRun(length: string.utf8.count, attributes: attributes))
self.init(Guts(string: string, runs: _InternalRuns(runs)))
// Only scalar-bound attributes can be incorrect if only one run exists
if attributes.containsScalarConstraint {
_guts.fixScalarConstrainedAttributes(in: string.startIndex ..< string.endIndex)
}
}
/// Creates a new attributed string with the given `String` value associated with the given
/// attributes.
public init(_ string: String, attributes: AttributeContainer = .init()) {
self.init(BigString(string), attributes: attributes.storage)
}
/// Creates a new attributed string with the given `Substring` value associated with the given
/// attributes.
public init(_ substring: Substring, attributes: AttributeContainer = .init()) {
self.init(BigString(substring), attributes: attributes.storage)
}
public init<S : Sequence>(
_ elements: S,
attributes: AttributeContainer = .init()
) where S.Element == Character {
let str = Self._bstring(from: elements)
self.init(str, attributes: attributes.storage)
}
public init(_ substring: AttributedSubstring) {
let str = BigString(substring._unicodeScalars)
let runs = substring._guts.runs.extract(utf8Offsets: substring._range._utf8OffsetRange)
assert(str.utf8.count == runs.utf8Count)
_guts = Guts(string: str, runs: runs)
// FIXME: Extracting a slice should invalidate .textChanged attribute runs on the edges
// (Compare with the `copy(in:)` call in the scope filtering initializer below -- that
// one does too much, this one does too little.)
}
#if FOUNDATION_FRAMEWORK
// TODO: Support scope-specific initialization in FoundationPreview
public init<S : AttributeScope, T : AttributedStringProtocol>(_ other: T, including scope: KeyPath<AttributeScopes, S.Type>) {
self.init(other, including: S.self)
}
public init<S : AttributeScope, T : AttributedStringProtocol>(_ other: T, including scope: S.Type) {
// FIXME: This `copy(in:)` call does too much work, potentially unexpectedly removing attributes.
self.init(other.__guts.copy(in: other._stringBounds))
let attributeTypes = scope.attributeKeyTypes()
_guts.runs(in: _guts.utf8OffsetRange).updateEach { attributes, utf8Range, modified in
modified = false
for key in attributes.keys {
if !attributeTypes.keys.contains(key) {
attributes[key] = nil
modified = true
}
}
}
}
#endif // FOUNDATION_FRAMEWORK
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString {
internal static func _bstring<S: Sequence<Character>>(from elements: S) -> BigString {
if let elements = _specializingCast(elements, to: String.self) {
return BigString(elements)
}
if let elements = _specializingCast(elements, to: Substring.self) {
return BigString(elements)
}
if let elements = _specializingCast(elements, to: AttributedString.CharacterView.self) {
return BigString(elements._characters)
}
if let elements = _specializingCast(
elements, to: Slice<AttributedString.CharacterView>.self
) {
return BigString(elements._characters)
}
return BigString(elements)
}
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString { // Equatable
public static func == (lhs: Self, rhs: Self) -> Bool {
AttributedString.Guts.characterwiseIsEqual(lhs._guts, to: rhs._guts)
}
}
// Note: The Hashable implementation is inherited from AttributedStringProtocol.
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.init(value)
}
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString { // AttributedStringAttributeMutation
public mutating func setAttributes(_ attributes: AttributeContainer) {
ensureUniqueReference()
_guts.setAttributes(attributes.storage, in: _stringBounds)
}
public mutating func mergeAttributes(_ attributes: AttributeContainer, mergePolicy: AttributeMergePolicy = .keepNew) {
ensureUniqueReference()
_guts.mergeAttributes(attributes, in: _stringBounds, mergePolicy: mergePolicy)
}
public mutating func replaceAttributes(_ attributes: AttributeContainer, with others: AttributeContainer) {
guard attributes != others else { return }
ensureUniqueReference()
let hasConstrainedAttributes = attributes._hasConstrainedAttributes || others._hasConstrainedAttributes
var fixupRanges: [Range<Int>] = []
_guts.runs(in: _guts.utf8OffsetRange).updateEach(
when: { $0.matches(attributes.storage) },
with: { runAttributes, utf8Range in
for key in attributes.storage.keys {
runAttributes[key] = nil
}
runAttributes.mergeIn(others)
if hasConstrainedAttributes {
fixupRanges._extend(with: utf8Range)
}
})
for range in fixupRanges {
// FIXME: Collect boundary constraints.
_guts.enforceAttributeConstraintsAfterMutation(in: range, type: .attributes)
}
}
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString: AttributedStringProtocol {
public struct Index : Comparable, Sendable {
internal var _value: BigString.Index
internal init(_ value: BigString.Index) {
self._value = value
}
public static func == (left: Self, right: Self) -> Bool {
left._value == right._value
}
public static func < (left: Self, right: Self) -> Bool {
left._value < right._value
}
}
public var startIndex : Index {
Index(_guts.string.startIndex)
}
public var endIndex : Index {
Index(_guts.string.endIndex)
}
@preconcurrency
public subscript<K: AttributedStringKey>(_: K.Type) -> K.Value? where K.Value : Sendable {
get {
_guts.getUniformValue(in: _stringBounds, key: K.self)?.rawValue(as: K.self)
}
set {
ensureUniqueReference()
if let v = newValue {
_guts.setAttributeValue(v, forKey: K.self, in: _stringBounds)
} else {
_guts.removeAttributeValue(forKey: K.self, in: _stringBounds)
}
}
}
@preconcurrency
public subscript<K: AttributedStringKey>(
dynamicMember keyPath: KeyPath<AttributeDynamicLookup, K>
) -> K.Value? where K.Value: Sendable {
get { self[K.self] }
set { self[K.self] = newValue }
}
public subscript<S: AttributeScope>(
dynamicMember keyPath: KeyPath<AttributeScopes, S.Type>
) -> ScopedAttributeContainer<S> {
get {
return ScopedAttributeContainer(_guts.getUniformValues(in: _stringBounds))
}
_modify {
ensureUniqueReference()
var container = ScopedAttributeContainer<S>()
defer {
if let removedKey = container.removedKey {
_guts.removeAttributeValue(forKey: removedKey, in: _stringBounds)
} else {
_guts.mergeAttributes(AttributeContainer(container.storage), in: _stringBounds)
}
}
yield &container
}
}
}
// MARK: Mutating operations
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString {
internal mutating func ensureUniqueReference() {
if !isKnownUniquelyReferenced(&_guts) {
_guts = _guts.copy()
}
}
public mutating func append(_ s: some AttributedStringProtocol) {
replaceSubrange(endIndex ..< endIndex, with: s)
}
public mutating func insert(_ s: some AttributedStringProtocol, at index: AttributedString.Index) {
replaceSubrange(index ..< index, with: s)
}
public mutating func removeSubrange(_ range: some RangeExpression<Index>) {
replaceSubrange(range, with: AttributedString())
}
public mutating func replaceSubrange(_ range: some RangeExpression<Index>, with s: some AttributedStringProtocol) {
ensureUniqueReference()
// Note: we allow sub-Character ranges, so we must use `unicodeScalars` here, not `characters`.
let subrange = range.relative(to: unicodeScalars)._bstringRange
_guts.replaceSubrange(subrange, with: s)
}
}
// MARK: Concatenation operators
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString {
public static func +(lhs: AttributedString, rhs: some AttributedStringProtocol) -> AttributedString {
var result = lhs
result.append(rhs)
return result
}
public static func +=(lhs: inout AttributedString, rhs: some AttributedStringProtocol) {
lhs.append(rhs)
}
public static func + (lhs: AttributedString, rhs: AttributedString) -> AttributedString {
var result = lhs
result.append(rhs)
return result
}
public static func += (lhs: inout Self, rhs: AttributedString) {
lhs.append(rhs)
}
}
// MARK: Substring access
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString {
public subscript(bounds: some RangeExpression<Index>) -> AttributedSubstring {
get {
// Note: we allow sub-Character ranges, so we must use `unicodeScalars` here, not `characters`.
let bounds = bounds.relative(to: unicodeScalars)
return AttributedSubstring(_guts, in: bounds._bstringRange)
}
_modify {
ensureUniqueReference()
// Note: we allow sub-Character ranges, so we must use `unicodeScalars` here, not `characters`.
let bounds = bounds.relative(to: unicodeScalars)
var substr = AttributedSubstring(_guts, in: bounds._bstringRange)
let ident = Self._nextModifyIdentity
substr._identity = ident
_guts = Guts() // Dummy guts to allow in-place mutations
defer {
if substr._identity != ident {
fatalError("Mutating an AttributedSubstring by replacing it with another from a different source is unsupported")
}
_guts = substr._guts
}
yield &substr
}
set {
// FIXME: Why is this allowed if _modify traps on replacement?
self.replaceSubrange(bounds, with: newValue)
}
}
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension Range where Bound == AttributedString.Index {
internal var _bstringRange: Range<BigString.Index> {
Range<BigString.Index>(uncheckedBounds: (lowerBound._value, upperBound._value))
}
internal var _utf8OffsetRange: Range<Int> {
Range<Int>(uncheckedBounds: (lowerBound._value.utf8Offset, upperBound._value.utf8Offset))
}
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension Range where Bound == BigString.Index {
internal var _utf8OffsetRange: Range<Int> {
Range<Int>(uncheckedBounds: (lowerBound.utf8Offset, upperBound.utf8Offset))
}
}