mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-22 13:39:25 +08:00
* 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
372 lines
13 KiB
Swift
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))
|
|
}
|
|
}
|