mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-21 21:09:57 +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
231 lines
8.1 KiB
Swift
231 lines
8.1 KiB
Swift
//===----------------------------------------------------------------------===//
|
||
//
|
||
// This source file is part of the Swift.org open source project
|
||
//
|
||
// Copyright (c) 2020 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 import ReflectionInternal
|
||
@_implementationOnly @_spi(Unstable) import CollectionsInternal
|
||
#else
|
||
package import _RopeModule
|
||
#endif
|
||
|
||
// MARK: AttributedStringKey API
|
||
|
||
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
|
||
extension AttributedString {
|
||
public enum AttributeRunBoundaries : Hashable, Sendable {
|
||
case paragraph
|
||
|
||
// FIXME: This is semantically wrong. We do not ever want to constrain attributes on
|
||
// characters (i.e., grapheme clusters) -- they are way too vague, and way
|
||
// too eager to accidentally merge with neighboring string data. (And they're also way
|
||
// too slow for this use case.)
|
||
//
|
||
// The entire point of this feature is to anchor attributes that describe attachments like
|
||
// custom views that should be embedded in the text. We do not _ever_ want the anchor text
|
||
// to accidentally compose with a subsequent combining character, losing the attachment.
|
||
//
|
||
// This needs to be deprecated and replaced by `case unicodeScalar(UnicodeScalar)`.
|
||
//
|
||
// The current implementation already works like that -- it ignores all but the first scalar
|
||
// of the specified `Character`, and does not engage in normalization or grapheme breaking.
|
||
case character(Character)
|
||
}
|
||
}
|
||
|
||
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
|
||
extension AttributedString.AttributeRunBoundaries {
|
||
var _isScalarConstrained: Bool {
|
||
if case .character = self { return true }
|
||
return false
|
||
}
|
||
|
||
var _constrainedScalar: Unicode.Scalar? {
|
||
switch self {
|
||
case .character(let char): return char.unicodeScalars.first
|
||
default: return nil
|
||
}
|
||
}
|
||
}
|
||
|
||
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
|
||
extension AttributedString {
|
||
public struct AttributeInvalidationCondition : Hashable, Sendable {
|
||
private enum _Storage : Hashable {
|
||
case textChanged
|
||
case attributeChanged(String)
|
||
}
|
||
|
||
private let storage: _Storage
|
||
|
||
private init(_ storage: _Storage) {
|
||
self.storage = storage
|
||
}
|
||
|
||
var isAttribute: Bool {
|
||
guard case .attributeChanged = storage else { return false }
|
||
return true
|
||
}
|
||
|
||
var attributeKey: String? {
|
||
switch storage {
|
||
case .textChanged:
|
||
return nil
|
||
case .attributeChanged(let string):
|
||
return string
|
||
}
|
||
}
|
||
|
||
public static let textChanged = Self(.textChanged)
|
||
|
||
public static func attributeChanged<T: AttributedStringKey>(_ key: T.Type) -> Self {
|
||
Self(.attributeChanged(key.name))
|
||
}
|
||
|
||
public static func attributeChanged<T: AttributedStringKey>(_ key: KeyPath<AttributeDynamicLookup, T>) -> Self {
|
||
Self(.attributeChanged(T.name))
|
||
}
|
||
|
||
static func attributeChanged(_ name: String) -> Self {
|
||
Self(.attributeChanged(name))
|
||
}
|
||
}
|
||
}
|
||
|
||
// Developers define new attributes by implementing AttributeKey.
|
||
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
||
public protocol AttributedStringKey {
|
||
associatedtype Value : Hashable
|
||
static var name : String { get }
|
||
|
||
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
|
||
static var runBoundaries : AttributedString.AttributeRunBoundaries? { get }
|
||
|
||
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
|
||
static var inheritedByAddedText : Bool { get }
|
||
|
||
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
|
||
static var invalidationConditions : Set<AttributedString.AttributeInvalidationCondition>? { get }
|
||
}
|
||
|
||
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
||
extension AttributedStringKey {
|
||
public var description: String { Self.name }
|
||
|
||
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
|
||
public static var runBoundaries : AttributedString.AttributeRunBoundaries? { nil }
|
||
|
||
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
|
||
public static var inheritedByAddedText : Bool { true }
|
||
|
||
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
|
||
public static var invalidationConditions : Set<AttributedString.AttributeInvalidationCondition>? { nil }
|
||
}
|
||
|
||
extension AttributedStringKey {
|
||
// FIXME: ☠️ Allocating an Array here is not a good idea.
|
||
static var _constraintsInvolved: [AttributedString.AttributeRunBoundaries] {
|
||
guard let rb = runBoundaries else { return [] }
|
||
return [rb]
|
||
}
|
||
}
|
||
|
||
// MARK: Attribute Scopes
|
||
|
||
@_nonSendable
|
||
@dynamicMemberLookup @frozen
|
||
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
||
public enum AttributeDynamicLookup {
|
||
public subscript<T: AttributedStringKey>(_: T.Type) -> T {
|
||
get { fatalError("Called outside of a dynamicMemberLookup subscript overload") }
|
||
}
|
||
}
|
||
|
||
@dynamicMemberLookup
|
||
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
|
||
public struct ScopedAttributeContainer<S: AttributeScope> : Sendable {
|
||
internal var storage : AttributedString._AttributeStorage
|
||
|
||
// Record the most recently deleted key for use in AttributedString mutation subscripts that use _modify
|
||
// Note: if ScopedAttributeContainer ever adds a mutating function that can mutate multiple attributes, this will need to record multiple removed keys
|
||
internal var removedKey : String?
|
||
|
||
@preconcurrency
|
||
public subscript<T: AttributedStringKey>(dynamicMember keyPath: KeyPath<S, T>) -> T.Value? where T.Value : Sendable {
|
||
get { storage[T.self] }
|
||
set {
|
||
storage[T.self] = newValue
|
||
if newValue == nil {
|
||
removedKey = T.name
|
||
}
|
||
}
|
||
}
|
||
|
||
internal init(_ storage : AttributedString._AttributeStorage = .init()) {
|
||
self.storage = storage
|
||
}
|
||
|
||
#if FOUNDATION_FRAMEWORK
|
||
// TODO: Support scope-specific equality/attributes in FoundationPreview
|
||
internal func equals(_ other: Self) -> Bool {
|
||
for (name, _) in S.attributeKeyTypes() {
|
||
if self.storage[name] != other.storage[name] {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
internal var attributes : AttributeContainer {
|
||
var contents = AttributedString._AttributeStorage()
|
||
for (name, _) in S.attributeKeyTypes() {
|
||
contents[name] = self.storage[name]
|
||
}
|
||
return AttributeContainer(contents)
|
||
}
|
||
|
||
#endif // FOUNDATION_FRAMEWORK
|
||
}
|
||
|
||
|
||
// MARK: Internals
|
||
|
||
#if FOUNDATION_FRAMEWORK
|
||
|
||
internal extension AttributedStringKey {
|
||
static func _convertToObjectiveCValue(_ value: Value) throws -> AnyObject {
|
||
if let convertibleType = Self.self as? any ObjectiveCConvertibleAttributedStringKey.Type {
|
||
func project<K: ObjectiveCConvertibleAttributedStringKey>(_: K.Type) throws -> AnyObject {
|
||
try K.objectiveCValue(for: value as! K.Value)
|
||
}
|
||
return try project(convertibleType)
|
||
} else {
|
||
return value as AnyObject
|
||
}
|
||
}
|
||
|
||
static func _convertFromObjectiveCValue(_ value: AnyObject) throws -> Value {
|
||
if let convertibleType = Self.self as? any ObjectiveCConvertibleAttributedStringKey.Type {
|
||
func project<K: ObjectiveCConvertibleAttributedStringKey>(_: K.Type) throws -> Value {
|
||
guard let objcValue = value as? K.ObjectiveCValue else {
|
||
throw CocoaError(.coderInvalidValue)
|
||
}
|
||
return try K.value(for: objcValue) as! Value
|
||
}
|
||
return try project(convertibleType)
|
||
} else if let trueValue = value as? Value {
|
||
return trueValue
|
||
} else {
|
||
throw CocoaError(.coderInvalidValue)
|
||
}
|
||
}
|
||
}
|
||
#endif // FOUNDATION_FRAMEWORK |