swift-foundation/Sources/FoundationEssentials/AttributedString/AttributedString+Runs+AttributeSlices.swift
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

823 lines
26 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//===----------------------------------------------------------------------===//
//
// 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
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString.Runs {
@preconcurrency
public struct AttributesSlice1<T : AttributedStringKey> : BidirectionalCollection, Sendable
where T.Value : Sendable {
public typealias Index = AttributedString.Index
// FIXME: Why no labels?
public typealias Element = (T.Value?, Range<AttributedString.Index>)
internal typealias Runs = AttributedString.Runs
let runs: Runs
let _names: [String]
let _constraints: [AttributeRunBoundaries]
init(runs: Runs) {
self.runs = runs
// FIXME: Get these from a proper cache in runs._guts.
_names = [T.name]
_constraints = T._constraintsInvolved
}
public struct Iterator: IteratorProtocol, Sendable {
// Note: This is basically equivalent to `IndexingIterator`.
public typealias Element = AttributesSlice1.Element
let _slice: AttributesSlice1
var _index: AttributesSlice1.Index
internal init(_ slice: AttributesSlice1) {
self._slice = slice
self._index = slice.startIndex
}
public mutating func next() -> Element? {
if _index == _slice.endIndex {
return nil
}
let run = _slice.runs[_index]
let next = _slice.index(after: _index)
let range = _index ..< next
_index = next
return (run._attributes[T.self], range)
}
}
public func makeIterator() -> Iterator {
Iterator(self)
}
public var startIndex: Index {
Index(runs._strBounds.lowerBound)
}
public var endIndex: Index {
Index(runs._strBounds.upperBound)
}
public func index(before i: Index) -> Index {
runs._slicedRunBoundary(
before: i,
attributeNames: _names,
constraints: _constraints)
}
public func index(after i: Index) -> Index {
runs._slicedRunBoundary(
after: i,
attributeNames: _names,
constraints: _constraints)
}
public subscript(position: AttributedString.Index) -> Element {
let (start, runIndex) = runs._slicedRunBoundary(
roundingDown: position,
attributeNames: _names,
constraints: _constraints)
let end = self.index(after: position)
let attributes = runs._guts.runs[runIndex].attributes
return (attributes[T.self], start ..< end)
}
// FIXME: This is a collection with potentially unaligned indices that uses Slice as its
// SubSequence. Slicing the collection on such an index will produce spurious crashes.
// Add a custom implementation for the range subscript that forcibly rounds the given bounds
// down to the nearest valid indices.
}
public subscript<T : AttributedStringKey>(_ keyPath: KeyPath<AttributeDynamicLookup, T>) -> AttributesSlice1<T> {
return AttributesSlice1<T>(runs: self)
}
public subscript<T : AttributedStringKey>(_ t: T.Type) -> AttributesSlice1<T> {
return AttributesSlice1<T>(runs: self)
}
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString.Runs {
@preconcurrency
public struct AttributesSlice2<
T : AttributedStringKey,
U : AttributedStringKey
> : BidirectionalCollection, Sendable
where
T.Value : Sendable,
U.Value : Sendable
{
public typealias Index = AttributedString.Index
// FIXME: Why no labels?
public typealias Element = (T.Value?, U.Value?, Range<AttributedString.Index>)
internal typealias Runs = AttributedString.Runs
let runs : Runs
let _names: [String]
let _constraints: [AttributeRunBoundaries]
init(runs: Runs) {
self.runs = runs
// FIXME: Get these from a proper cache in runs._guts.
_names = [T.name, U.name]
_constraints = Array(_contents: T.runBoundaries, U.runBoundaries)
}
public struct Iterator: IteratorProtocol, Sendable {
// Note: This is basically equivalent to `IndexingIterator`.
public typealias Element = AttributesSlice2.Element
let _slice: AttributesSlice2
var _index: AttributedString.Index
internal init(_ slice: AttributesSlice2) {
self._slice = slice
self._index = slice.startIndex
}
public mutating func next() -> Element? {
if _index == _slice.endIndex {
return nil
}
let run = _slice.runs[_index]
let next = _slice.index(after: _index)
let range = _index ..< next
_index = next
return (run._attributes[T.self], run._attributes[U.self], range)
}
}
public func makeIterator() -> Iterator {
Iterator(self)
}
public var startIndex: Index {
Index(runs._strBounds.lowerBound)
}
public var endIndex: Index {
Index(runs._strBounds.upperBound)
}
public func index(before i: Index) -> Index {
runs._slicedRunBoundary(
before: i,
attributeNames: _names,
constraints: _constraints)
}
public func index(after i: Index) -> Index {
runs._slicedRunBoundary(
after: i,
attributeNames: _names,
constraints: _constraints)
}
public subscript(position: AttributedString.Index) -> Element {
let (start, runIndex) = runs._slicedRunBoundary(
roundingDown: position,
attributeNames: _names,
constraints: _constraints)
let end = self.index(after: position)
let attributes = runs._guts.runs[runIndex].attributes
return (attributes[T.self], attributes[U.self], start ..< end)
}
// FIXME: This is a collection with potentially unaligned indices that uses Slice as its
// SubSequence. Slicing the collection on such an index will produce spurious crashes.
// Add a custom implementation for the range subscript that forcibly rounds the given bounds
// down to the nearest valid indices.
}
public subscript <
T : AttributedStringKey,
U : AttributedStringKey
> (
_ t: KeyPath<AttributeDynamicLookup, T>,
_ u: KeyPath<AttributeDynamicLookup, U>
) -> AttributesSlice2<T, U> {
return AttributesSlice2<T, U>(runs: self)
}
public subscript <
T : AttributedStringKey,
U : AttributedStringKey
> (
_ t: T.Type,
_ u: U.Type
) -> AttributesSlice2<T, U> {
return AttributesSlice2<T, U>(runs: self)
}
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString.Runs {
@preconcurrency
public struct AttributesSlice3<
T : AttributedStringKey,
U : AttributedStringKey,
V : AttributedStringKey
> : BidirectionalCollection, Sendable
where
T.Value : Sendable,
U.Value : Sendable,
V.Value : Sendable
{
public typealias Index = AttributedString.Index
// FIXME: Why no labels?
public typealias Element = (T.Value?, U.Value?, V.Value?, Range<AttributedString.Index>)
internal typealias Runs = AttributedString.Runs
let runs : Runs
let _names: [String]
let _constraints: [AttributeRunBoundaries]
init(runs: Runs) {
self.runs = runs
// FIXME: Get these from a proper cache in runs._guts.
_names = [T.name, U.name, V.name]
_constraints = Array(_contents: T.runBoundaries, U.runBoundaries, V.runBoundaries)
}
public struct Iterator: IteratorProtocol, Sendable {
// Note: This is basically equivalent to `IndexingIterator`.
public typealias Element = AttributesSlice3.Element
let _slice: AttributesSlice3
var _index: AttributedString.Index
internal init(_ slice: AttributesSlice3) {
self._slice = slice
self._index = slice.startIndex
}
public mutating func next() -> Element? {
if _index == _slice.endIndex {
return nil
}
let run = _slice.runs[_index]
let next = _slice.index(after: _index)
let range = _index ..< next
_index = next
return (
run._attributes[T.self],
run._attributes[U.self],
run._attributes[V.self],
range)
}
}
public func makeIterator() -> Iterator {
Iterator(self)
}
public var startIndex: Index {
Index(runs._strBounds.lowerBound)
}
public var endIndex: Index {
Index(runs._strBounds.upperBound)
}
public func index(before i: Index) -> Index {
runs._slicedRunBoundary(
before: i,
attributeNames: _names,
constraints: _constraints)
}
public func index(after i: Index) -> Index {
runs._slicedRunBoundary(
after: i,
attributeNames: _names,
constraints: _constraints)
}
public subscript(position: AttributedString.Index) -> Element {
let (start, runIndex) = runs._slicedRunBoundary(
roundingDown: position,
attributeNames: _names,
constraints: _constraints)
let end = self.index(after: position)
let attributes = runs._guts.runs[runIndex].attributes
return (attributes[T.self], attributes[U.self], attributes[V.self], start ..< end)
}
// FIXME: This is a collection with potentially unaligned indices that uses Slice as its
// SubSequence. Slicing the collection on such an index will produce spurious crashes.
// Add a custom implementation for the range subscript that forcibly rounds the given bounds
// down to the nearest valid indices.
}
public subscript <
T : AttributedStringKey,
U : AttributedStringKey,
V : AttributedStringKey
> (
_ t: KeyPath<AttributeDynamicLookup, T>,
_ u: KeyPath<AttributeDynamicLookup, U>,
_ v: KeyPath<AttributeDynamicLookup, V>
) -> AttributesSlice3<T, U, V> {
return AttributesSlice3<T, U, V>(runs: self)
}
public subscript <
T : AttributedStringKey,
U : AttributedStringKey,
V : AttributedStringKey
> (
_ t: T.Type,
_ u: U.Type,
_ v: V.Type
) -> AttributesSlice3<T, U, V> {
return AttributesSlice3<T, U, V>(runs: self)
}
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString.Runs {
@preconcurrency
public struct AttributesSlice4<
T : AttributedStringKey,
U : AttributedStringKey,
V : AttributedStringKey,
W : AttributedStringKey
> : BidirectionalCollection, Sendable
where
T.Value : Sendable,
U.Value : Sendable,
V.Value : Sendable,
W.Value : Sendable
{
public typealias Index = AttributedString.Index
// FIXME: Why no labels?
public typealias Element = (T.Value?, U.Value?, V.Value?, W.Value?, Range<AttributedString.Index>)
internal typealias Runs = AttributedString.Runs
let runs : Runs
let _names: [String]
let _constraints: [AttributeRunBoundaries]
init(runs: Runs) {
self.runs = runs
// FIXME: Get these from a proper cache in runs._guts.
_names = [T.name, U.name, V.name, W.name]
_constraints = Array(
_contents: T.runBoundaries, U.runBoundaries, V.runBoundaries, W.runBoundaries)
}
public struct Iterator: IteratorProtocol, Sendable {
// Note: This is basically equivalent to `IndexingIterator`.
public typealias Element = AttributesSlice4.Element
let _slice: AttributesSlice4
var _index: AttributedString.Index
internal init(_ slice: AttributesSlice4) {
self._slice = slice
self._index = slice.startIndex
}
public mutating func next() -> Element? {
if _index == _slice.endIndex {
return nil
}
let run = _slice.runs[_index]
let next = _slice.index(after: _index)
let range = _index ..< next
_index = next
return (
run._attributes[T.self],
run._attributes[U.self],
run._attributes[V.self],
run._attributes[W.self],
range)
}
}
public func makeIterator() -> Iterator {
Iterator(self)
}
public var startIndex: Index {
Index(runs._strBounds.lowerBound)
}
public var endIndex: Index {
Index(runs._strBounds.upperBound)
}
public func index(before i: Index) -> Index {
runs._slicedRunBoundary(
before: i,
attributeNames: _names,
constraints: _constraints)
}
public func index(after i: Index) -> Index {
runs._slicedRunBoundary(
after: i,
attributeNames: _names,
constraints: _constraints)
}
public subscript(position: AttributedString.Index) -> Element {
let (start, runIndex) = runs._slicedRunBoundary(
roundingDown: position,
attributeNames: _names,
constraints: _constraints)
let end = self.index(after: position)
let attributes = runs._guts.runs[runIndex].attributes
return (
attributes[T.self],
attributes[U.self],
attributes[V.self],
attributes[W.self],
start ..< end)
}
// FIXME: This is a collection with potentially unaligned indices that uses Slice as its
// SubSequence. Slicing the collection on such an index will produce spurious crashes.
// Add a custom implementation for the range subscript that forcibly rounds the given bounds
// down to the nearest valid indices.
}
public subscript <
T : AttributedStringKey,
U : AttributedStringKey,
V : AttributedStringKey,
W : AttributedStringKey
> (
_ t: KeyPath<AttributeDynamicLookup, T>,
_ u: KeyPath<AttributeDynamicLookup, U>,
_ v: KeyPath<AttributeDynamicLookup, V>,
_ w: KeyPath<AttributeDynamicLookup, W>
) -> AttributesSlice4<T, U, V, W> {
return AttributesSlice4<T, U, V, W>(runs: self)
}
public subscript <
T : AttributedStringKey,
U : AttributedStringKey,
V : AttributedStringKey,
W : AttributedStringKey
> (
_ t: T.Type,
_ u: U.Type,
_ v: V.Type,
_ w: W.Type
) -> AttributesSlice4<T, U, V, W> {
return AttributesSlice4<T, U, V, W>(runs: self)
}
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString.Runs {
@preconcurrency
public struct AttributesSlice5<
T : AttributedStringKey,
U : AttributedStringKey,
V : AttributedStringKey,
W : AttributedStringKey,
X : AttributedStringKey
> : BidirectionalCollection, Sendable
where
T.Value : Sendable,
U.Value : Sendable,
V.Value : Sendable,
W.Value : Sendable,
X.Value : Sendable
{
public typealias Index = AttributedString.Index
// FIXME: Why no labels?
public typealias Element = (T.Value?, U.Value?, V.Value?, W.Value?, X.Value?, Range<AttributedString.Index>)
internal typealias Runs = AttributedString.Runs
let runs : Runs
let _names: [String]
let _constraints: [AttributeRunBoundaries]
init(runs: Runs) {
self.runs = runs
// FIXME: Get these from a proper cache in runs._guts.
_names = [T.name, U.name, V.name, W.name]
_constraints = Array(
_contents: T.runBoundaries,
U.runBoundaries,
V.runBoundaries,
W.runBoundaries,
X.runBoundaries)
}
public struct Iterator: IteratorProtocol, Sendable {
public typealias Element = AttributesSlice5.Element
let _slice: AttributesSlice5
var _index: AttributedString.Index
internal init(_ slice: AttributesSlice5) {
self._slice = slice
self._index = slice.startIndex
}
public mutating func next() -> Element? {
if _index == _slice.endIndex {
return nil
}
let run = _slice.runs[_index]
let next = _slice.index(after: _index)
let range = _index ..< next
_index = next
return (
run._attributes[T.self],
run._attributes[U.self],
run._attributes[V.self],
run._attributes[W.self],
run._attributes[X.self],
range)
}
}
public func makeIterator() -> Iterator {
Iterator(self)
}
public var startIndex: Index {
Index(runs._strBounds.lowerBound)
}
public var endIndex: Index {
Index(runs._strBounds.upperBound)
}
public func index(before i: Index) -> Index {
runs._slicedRunBoundary(
before: i,
attributeNames: _names,
constraints: _constraints)
}
public func index(after i: Index) -> Index {
runs._slicedRunBoundary(
after: i,
attributeNames: _names,
constraints: _constraints)
}
public subscript(position: AttributedString.Index) -> Element {
let (start, runIndex) = runs._slicedRunBoundary(
roundingDown: position,
attributeNames: _names,
constraints: _constraints)
let end = self.index(after: position)
let attributes = runs._guts.runs[runIndex].attributes
return (
attributes[T.self],
attributes[U.self],
attributes[V.self],
attributes[W.self],
attributes[X.self],
start ..< end)
}
// FIXME: This is a collection with potentially unaligned indices that uses Slice as its
// SubSequence. Slicing the collection on such an index will produce spurious crashes.
// Add a custom implementation for the range subscript that forcibly rounds the given bounds
// down to the nearest valid indices.
}
public subscript <
T : AttributedStringKey,
U : AttributedStringKey,
V : AttributedStringKey,
W : AttributedStringKey,
X : AttributedStringKey
> (
_ t: KeyPath<AttributeDynamicLookup, T>,
_ u: KeyPath<AttributeDynamicLookup, U>,
_ v: KeyPath<AttributeDynamicLookup, V>,
_ w: KeyPath<AttributeDynamicLookup, W>,
_ x: KeyPath<AttributeDynamicLookup, X>
) -> AttributesSlice5<T, U, V, W, X> {
return AttributesSlice5<T, U, V, W, X>(runs: self)
}
public subscript <
T : AttributedStringKey,
U : AttributedStringKey,
V : AttributedStringKey,
W : AttributedStringKey,
X : AttributedStringKey
> (
_ t: T.Type,
_ u: U.Type,
_ v: V.Type,
_ w: W.Type,
_ x: X.Type
) -> AttributesSlice5<T, U, V, W, X> {
return AttributesSlice5<T, U, V, W, X>(runs: self)
}
}
#if FOUNDATION_FRAMEWORK
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension AttributedString.Runs {
@_spi(AttributedString)
public struct NSAttributesSlice : BidirectionalCollection, Sendable {
public typealias Index = AttributedString.Index
// FIXME: Why no labels?
public typealias Element = (AttributeContainer, Range<AttributedString.Index>)
internal typealias Runs = AttributedString.Runs
internal let _runs: Runs
private let _names: [String]
internal init(runs: Runs, names: [String]) {
self._runs = runs
self._names = names
}
public struct Iterator: IteratorProtocol, Sendable {
// Note: This is basically equivalent to `IndexingIterator`.
public typealias Element = NSAttributesSlice.Element
let _slice: NSAttributesSlice
var _index: AttributedString.Index
internal init(_ slice: NSAttributesSlice) {
self._slice = slice
self._index = slice.startIndex
}
public mutating func next() -> Element? {
if _index == _slice.endIndex {
return nil
}
let run = _slice._runs[_index]
let next = _slice.index(after: _index)
let range = _index ..< next
_index = next
return (_slice.buildContainer(from: run._attributes), range)
}
}
public func makeIterator() -> Iterator {
Iterator(self)
}
public var startIndex: Index {
Index(_runs._strBounds.lowerBound)
}
public var endIndex: Index {
Index(_runs._strBounds.upperBound)
}
public func index(before i: Index) -> Index {
_runs._slicedRunBoundary(
before: i,
attributeNames: _names,
constraints: [])
}
public func index(after i: Index) -> Index {
_runs._slicedRunBoundary(
after: i,
attributeNames: _names,
constraints: [])
}
public subscript(position: AttributedString.Index) -> Element {
let (start, runIndex) = _runs._slicedRunBoundary(
roundingDown: position,
attributeNames: _names,
constraints: [])
let end = self.index(after: position)
let attributes = _runs._guts.runs[runIndex].attributes
return (buildContainer(from: attributes), start ..< end)
}
// FIXME: This is a collection with potentially unaligned indices that uses Slice as its
// SubSequence. Slicing the collection on such an index will produce spurious crashes.
// Add a custom implementation for the range subscript that forcibly rounds the given bounds
// down to the nearest valid indices.
private func buildContainer(from storage: AttributedString._AttributeStorage) -> AttributeContainer {
AttributeContainer(storage.filterWithoutInvalidatingDependents { _names.contains($0.key) })
}
}
@_spi(AttributedString)
public subscript(nsAttributedStringKeys keys: NSAttributedString.Key...) -> NSAttributesSlice {
return NSAttributesSlice(runs: self, names: keys.map { $0.rawValue })
}
}
#endif // FOUNDATION_FRAMEWORK
extension RangeReplaceableCollection {
internal init(_contents item1: Element?) {
self.init()
if let item1 { self.append(item1) }
}
internal init(_contents item1: Element?, _ item2: Element?) {
self.init()
var c = 0
if item1 != nil { c &+= 1 }
if item2 != nil { c &+= 1 }
guard c > 0 else { return }
self.reserveCapacity(c)
if let item1 { self.append(item1) }
if let item2 { self.append(item2) }
}
internal init(_contents item1: Element?, _ item2: Element?, _ item3: Element?) {
self.init()
var c = 0
if item1 != nil { c &+= 1 }
if item2 != nil { c &+= 1 }
if item3 != nil { c &+= 1 }
guard c > 0 else { return }
self.reserveCapacity(c)
if let item1 { self.append(item1) }
if let item2 { self.append(item2) }
if let item3 { self.append(item3) }
}
internal init(
_contents item1: Element?, _ item2: Element?, _ item3: Element?, _ item4: Element?
) {
self.init()
var c = 0
if item1 != nil { c &+= 1 }
if item2 != nil { c &+= 1 }
if item3 != nil { c &+= 1 }
if item4 != nil { c &+= 1 }
guard c > 0 else { return }
self.reserveCapacity(c)
if let item1 { self.append(item1) }
if let item2 { self.append(item2) }
if let item3 { self.append(item3) }
if let item4 { self.append(item4) }
}
internal init(
_contents item1: Element?,
_ item2: Element?,
_ item3: Element?,
_ item4: Element?,
_ item5: Element?
) {
self.init()
var c = 0
if item1 != nil { c &+= 1 }
if item2 != nil { c &+= 1 }
if item3 != nil { c &+= 1 }
if item4 != nil { c &+= 1 }
if item5 != nil { c &+= 1 }
guard c > 0 else { return }
self.reserveCapacity(c)
if let item1 { self.append(item1) }
if let item2 { self.append(item2) }
if let item3 { self.append(item3) }
if let item4 { self.append(item4) }
if let item5 { self.append(item5) }
}
}