Skip to content

Commit 71c2b7f

Browse files
committed
Fixed bug when delegate wasn't notified of edited paragraphs or some part of a paragraph if users pasted a string over selected text which has the same length as the inserted string
1 parent 3bb8c8a commit 71c2b7f

File tree

2 files changed

+104
-2
lines changed

2 files changed

+104
-2
lines changed

Sources/ParagraphTextKit/ParagraphTextStorage.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,15 @@ open class ParagraphTextStorage: NSTextStorage {
4848
/// Helper var indicating that after the editing the resulting range of the next paragraph will be equal to the first edited paragraph
4949
/// and that might confuse the diff algorhythm.
5050
///
51-
/// Confusing results in the case when the diff algorhythm will not notify the delegate that the next paragraph was edited,
51+
/// That confusing results in the case when the diff algorhythm will not notify the delegate that the next paragraph was edited,
5252
/// because the algorhythm will consider that if the range is the same, then there was no change at all
5353
private var nextEditedParagraphWillHaveRangeEqualWithFirst = false
54+
55+
/// Helper var for the case when a user pastes some text and that text length is equal to the selected text within the text view.
56+
///
57+
/// It is important, because if the edit with the same length of the selected text , the diff algrothythm
58+
/// won't recognize it as a change, since the result text storage will have the same paragraph lengths
59+
private var editHasSameLength = false
5460

5561
/// Subscriber to the NSTextStorage.willProcessEditingNotification
5662
private var processingSubscriber: AnyCancellable?
@@ -93,6 +99,12 @@ open class ParagraphTextStorage: NSTextStorage {
9399

94100
indexesBeforeEditing = paragraphIndexes(in: range)
95101

102+
if delta == 0 && str.length > 0 {
103+
editHasSameLength = true
104+
} else {
105+
editHasSameLength = false
106+
}
107+
96108
if indexesBeforeEditing.count > 1 && delta < 0 {
97109
let firstRange = paragraphRanges[indexesBeforeEditing[0]]
98110

@@ -158,6 +170,22 @@ open class ParagraphTextStorage: NSTextStorage {
158170
baseParagraphRange: paragraphsBefore.first!,
159171
insertionLocation: editedRange.location)
160172

173+
// if delta of edits is zero (user could just paste the same-length text over the same-length selected text)
174+
// make sure that the delegate will be notified of those changes, since the diff algorhythm won't recognize
175+
// any changes in that case
176+
guard changes.count != 0 && !editHasSameLength else {
177+
indexesBeforeEditing.forEach{
178+
changes.append(ParagraphRangeChange.editedParagraph(index: $0, range: paragraphRanges[$0]))
179+
}
180+
181+
if let existingDelegate = paragraphDelegate {
182+
let descriptedChanges = ParagraphChange.from(rangeChanges: changes, textStorage: self)
183+
existingDelegate.textStorage(self, didChangeParagraphs: descriptedChanges)
184+
}
185+
186+
return
187+
}
188+
161189
var hasEditedChange = false
162190
changes.forEach{ change in
163191
if case ParagraphRangeChange.editedParagraph(index: _, range: _) = change { hasEditedChange = true }
@@ -251,7 +279,10 @@ open class ParagraphTextStorage: NSTextStorage {
251279
guard length > 0 else { return [0] }
252280

253281
// get paragraphs from the range
254-
let paragraphs = attributedSubstring(from: range).string.paragraphs
282+
var paragraphs = attributedSubstring(from: range).string.paragraphs
283+
if paragraphs.last?.isEmpty == true && editHasSameLength {
284+
paragraphs = paragraphs.dropLast()
285+
}
255286

256287
// get start/end indexes for substring the existing paragraphs array..
257288
// .. it's better than iterating through all the paragraphs, especially if text is huge

Tests/ParagraphTextKitTests/ParagraphTextStorageTests.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,77 @@ final class ParagraphTextStorageTests: XCTestCase {
716716
"ParagraphTextStorage paragraph ranges should match the delegate ranges")
717717
}
718718

719+
func testParagraphTextStorage_EditParagraphWithSameLengthEdit() {
720+
let string = "First paragraph\nSecond paragraph"
721+
let editString = "additions"
722+
723+
textStorage.beginEditing()
724+
textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string)
725+
textStorage.endEditing()
726+
727+
textStorage.beginEditing()
728+
textStorage.replaceCharacters(in: NSRange(location: 6, length: 9), with: editString)
729+
textStorage.endEditing()
730+
731+
XCTAssertTrue(delegate.insertions.count == 1 && delegate.editions.count == 2 && delegate.removals.count == 0,
732+
"ParagraphTextStorage paragraph delegate should be notified of exact changes")
733+
734+
XCTAssertTrue(delegate.insertions[0] == 1)
735+
XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0)
736+
737+
let endString = "First additions\nSecond paragraph"
738+
739+
XCTAssertTrue(textStorage.paragraphRanges.count == 2,
740+
"ParagraphTextStorage should now have 2 paragraphs")
741+
742+
let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length)
743+
let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length)
744+
745+
XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange &&
746+
textStorage.paragraphRanges[1] == secondRange,
747+
"ParagraphTextStorage paragraph ranges should be correct")
748+
749+
XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges,
750+
"ParagraphTextStorage paragraph ranges should match the delegate ranges")
751+
}
752+
753+
func testParagraphTextStorage_EditMultipleParagraphWithSameLengthEdit() {
754+
let string = "First paragraph\nSecond paragraph\nThird paragraph"
755+
let editString = "First paragraph\nSecond paragraph\n"
756+
757+
textStorage.beginEditing()
758+
textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string)
759+
textStorage.endEditing()
760+
761+
textStorage.beginEditing()
762+
textStorage.replaceCharacters(in: NSRange(location: 0, length: 33), with: editString)
763+
textStorage.endEditing()
764+
765+
XCTAssertTrue(delegate.insertions.count == 2 && delegate.editions.count == 4 && delegate.removals.count == 0,
766+
"ParagraphTextStorage paragraph delegate should be notified of exact changes")
767+
768+
XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2)
769+
XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0 && delegate.editions[2] == 1 && delegate.editions[3] == 2)
770+
771+
let endString = "First paragraph\nSecond paragraph\nThird paragraph"
772+
773+
XCTAssertTrue(textStorage.paragraphRanges.count == 3,
774+
"ParagraphTextStorage should now have 3 paragraphs")
775+
776+
let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length)
777+
let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length)
778+
let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length)
779+
780+
XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange &&
781+
textStorage.paragraphRanges[1] == secondRange &&
782+
textStorage.paragraphRanges[2] == thirdRange,
783+
"ParagraphTextStorage paragraph ranges should be correct")
784+
785+
XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges,
786+
"ParagraphTextStorage paragraph ranges should match the delegate ranges")
787+
}
788+
789+
719790
func testParagraphTextStorage_EditParagraphAtEnd() {
720791
let string = "First paragraph\nSecond paragraph"
721792
let editString = "addition"

0 commit comments

Comments
 (0)