Skip to content

Commit 655dd5e

Browse files
authored
AttributedString character insertion doesn't invalidate text dependent attributes (#1256)
1 parent edcf9ac commit 655dd5e

File tree

2 files changed

+29
-7
lines changed

2 files changed

+29
-7
lines changed

Sources/FoundationEssentials/AttributedString/AttributedStringAttributeConstrainingBehavior.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,18 @@ extension AttributedString.Guts {
183183
/// - Returns: The UTF-8 range that was modified during this invalidation.
184184
/// (If no modification took place, then the result is `range`.)
185185
func enforceAttributeConstraintsBeforeMutation(to utf8Range: Range<Int>) -> Range<Int> {
186-
guard !utf8Range.isEmpty else { return utf8Range }
186+
var utf8Start = utf8Range.lowerBound
187+
var utf8End = utf8Range.upperBound
188+
189+
// Eagerly record the attributes at the end of the mutation as invalidating attributes at the start may change attributes at the end (if the mutation is within a run)
190+
let originalEndingAttributes = if utf8End > 0 {
191+
_characterInvalidatedAttributes(at: utf8End - 1)
192+
} else {
193+
_AttributeStorage()
194+
}
187195

188196
// Invalidate attributes preceding the range.
189-
var utf8Start = utf8Range.lowerBound
190-
do {
197+
if utf8Start < string.utf8.count {
191198
let attributes = _characterInvalidatedAttributes(at: utf8Start)
192199
var remainingKeys = Set(attributes.keys)
193200
let runs = runs(in: 0 ..< utf8Start)
@@ -210,9 +217,8 @@ extension AttributedString.Guts {
210217
}
211218

212219
// Invalidate attributes following the range.
213-
var utf8End = utf8Range.upperBound
214-
do {
215-
let attributes = _characterInvalidatedAttributes(at: utf8End - 1)
220+
if utf8End > 0 {
221+
let attributes = originalEndingAttributes
216222
var remainingKeys = Set(attributes.keys)
217223
let runs = runs(in: utf8End ..< string.utf8.count)
218224
var i = runs.startIndex

Tests/FoundationEssentialsTests/AttributedString/AttributedStringConstrainingBehaviorTests.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,14 @@ class TestAttributedStringConstrainingBehavior: XCTestCase {
508508
result.characters.append(contentsOf: "ABC")
509509
verify(string: result, matches: [("Hello, world", 1, 2), ("ABC", 1, nil)], for: \.testInt, \.testCharacterDependent)
510510

511+
result = str
512+
result.characters.insert("A", at: result.startIndex)
513+
verify(string: result, matches: [("A", 1, nil), ("Hello, world", 1, 2)], for: \.testInt, \.testCharacterDependent)
514+
515+
result = str
516+
result.characters.insert("A", at: result.index(afterCharacter: result.startIndex))
517+
verify(string: result, matches: [("HAello, world", 1, nil)], for: \.testInt, \.testCharacterDependent)
518+
511519
result = str
512520
result.characters.removeSubrange(result.index(afterCharacter: result.startIndex) ..< result.index(beforeCharacter: result.endIndex))
513521
verify(string: result, matches: [("Hd", 1, nil)], for: \.testInt, \.testCharacterDependent)
@@ -566,5 +574,13 @@ class TestAttributedStringConstrainingBehavior: XCTestCase {
566574
result[result.startIndex ..< result.index(afterCharacter: result.startIndex)] = replacement[replacement.startIndex ..< replacement.index(afterCharacter: str.startIndex)]
567575
verify(string: result, matches: [("H", nil, nil, "Hello"), ("ello, world", 1, nil, nil)], for: \.testInt, \.testCharacterDependent, \.testString)
568576
}
569-
577+
578+
func testInvalidationCharacterInsertionBetweenRuns() {
579+
var str = AttributedString("Hello", attributes: .init().testInt(1).testCharacterDependent(2))
580+
str += AttributedString("World", attributes: .init().testInt(1).testCharacterDependent(3))
581+
582+
// Inserting text between two runs should not invalidate text dependent attributes in either of the surrounding runs
583+
str.characters.insert("|", at: str.index(str.startIndex, offsetByCharacters: 5))
584+
verify(string: str, matches: [("Hello", 1, 2), ("|", 1, nil), ("World", 1, 3)], for: \.testInt, \.testCharacterDependent)
585+
}
570586
}

0 commit comments

Comments
 (0)