├── Tests ├── LinuxMain.swift └── ParagraphTextKitTests │ ├── XCTestManifests.swift │ └── ParagraphTextStorageTests.swift ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ ├── xcuserdata │ └── cinetagonist.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist │ └── xcshareddata │ └── xcschemes │ └── ParagraphTextKit.xcscheme ├── Sources └── ParagraphTextKit │ ├── Extensions │ ├── NSAttributedString (extended).swift │ ├── NSRange (extended).swift │ └── String (extended).swift │ ├── Protocols │ └── ParagraphTextStorageDelegate.swift │ ├── Structs │ ├── ParagraphDescriptor.swift │ ├── ParagraphChange.swift │ └── ParagraphRangeChange.swift │ └── ParagraphTextStorage.swift ├── Package.swift ├── LICENSE.txt └── README.md /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ParagraphTextStorageTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += ParagraphTextStorageTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/cinetagonist.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /Tests/ParagraphTextKitTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(ParagraphTextStorageTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/ParagraphTextKit/Extensions/NSAttributedString (extended).swift: -------------------------------------------------------------------------------- 1 | //// 2 | // NSAttributedString (extended).swift 3 | // ParagraphTextKit 4 | // 5 | // Copyright (c) 2020 Vitalii Vashchenko 6 | // 7 | // This software is released under the MIT License. 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // Created by Vitalii Vashchenko on 11/21/20. 11 | // 12 | 13 | import Foundation 14 | 15 | public extension NSAttributedString { 16 | var range: NSRange { 17 | NSRange(location: 0, length: length) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ParagraphTextKit/Protocols/ParagraphTextStorageDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParagraphTextStorageDelegate.swift 3 | // ParagraphTextKit 4 | // 5 | // Copyright (c) 2020 Vitalii Vashchenko 6 | // 7 | // This software is released under the MIT License. 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // Created by Vitalii Vashchenko on 2/11/19. 11 | // 12 | 13 | import Foundation 14 | 15 | /// Protocol defines methods that are invoked if the storage has been edited 16 | public protocol ParagraphTextStorageDelegate: class { 17 | var presentedParagraphs: [NSAttributedString] { get } 18 | func textStorage(_ textStorage: ParagraphTextStorage, didChangeParagraphs changes: [ParagraphTextStorage.ParagraphChange]) 19 | } 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ParagraphTextKit", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | .iOS(.v13) 11 | ], 12 | products: [ 13 | .library( 14 | name: "ParagraphTextKit", 15 | targets: ["ParagraphTextKit"]), 16 | ], 17 | dependencies: [ 18 | 19 | ], 20 | targets: [ 21 | .target( 22 | name: "ParagraphTextKit", 23 | dependencies: []), 24 | .testTarget( 25 | name: "ParagraphTextKitTests", 26 | dependencies: ["ParagraphTextKit"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Sources/ParagraphTextKit/Extensions/NSRange (extended).swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSRange (extended).swift 3 | // ParagraphTextKit 4 | // 5 | // Copyright (c) 2020 Vitalii Vashchenko 6 | // 7 | // This software is released under the MIT License. 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // Created by Vitalii Vashchenko on 5/7/2020. 11 | // 12 | 13 | import Foundation 14 | 15 | public extension NSRange { 16 | var startIndex:Int { get { return location } } 17 | var endIndex:Int { get { return location + length } } 18 | 19 | var asRange:CountableRange { get { return location.. Bool { 32 | return index >= location && index < endIndex 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vitalii Vashchenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/cinetagonist.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ParagraphTextKit.xcscheme_^#shared#^_ 8 | 9 | isShown 10 | 11 | orderHint 12 | 0 13 | 14 | ParagraphTextStorage.xcscheme_^#shared#^_ 15 | 16 | orderHint 17 | 0 18 | 19 | 20 | SuppressBuildableAutocreation 21 | 22 | ParagraphTextKit 23 | 24 | primary 25 | 26 | 27 | ParagraphTextKitTests 28 | 29 | primary 30 | 31 | 32 | ParagraphTextStorage 33 | 34 | primary 35 | 36 | 37 | ParagraphTextStorageTests 38 | 39 | primary 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Sources/ParagraphTextKit/Structs/ParagraphDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParagraphDescriptor.swift 3 | // ParagraphTextKit 4 | // 5 | // Copyright (c) 2020 Vitalii Vashchenko 6 | // 7 | // This software is released under the MIT License. 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // Created by Vitalii Vashchenko on 5/7/2020. 11 | // 12 | 13 | import Foundation 14 | 15 | public extension ParagraphTextStorage { 16 | /// ParagraphDescriptor structure describes a text paragraph. 17 | /// 18 | /// The paragraph could be either a standalone instance, or might exist in some external NSTextStroage object or any of its subclasses. 19 | /// 20 | /// Origin of the paragraph is errelevant, since ParagraphDescriptor structure has all the neccessary data to describe a paragraph. 21 | struct ParagraphDescriptor: Equatable { 22 | 23 | /// Range of the paragraph descriptor in a text storage 24 | public internal(set) var storageRange: NSRange 25 | 26 | /// Text representation of the descriptor's range in the text storage 27 | public internal(set) var attributedString: NSAttributedString 28 | 29 | /// Text content of the paragraph 30 | public var text: String { 31 | attributedString.string 32 | } 33 | 34 | public init(attributedString: NSAttributedString, storageRange: NSRange) { 35 | self.attributedString = attributedString 36 | self.storageRange = storageRange 37 | } 38 | 39 | public static func ==(lhs: ParagraphDescriptor, rhs: ParagraphDescriptor) -> Bool { 40 | return lhs.attributedString == rhs.attributedString && lhs.storageRange == rhs.storageRange 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/ParagraphTextKit/Structs/ParagraphChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParagraphChange.swift 3 | // ParagraphTextKit 4 | // 5 | // Copyright (c) 2020 Vitalii Vashchenko 6 | // 7 | // This software is released under the MIT License. 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // Created by Vitalii Vashchenko on 5/7/2020. 11 | // 12 | 13 | import Foundation 14 | 15 | public extension ParagraphTextStorage { 16 | /// Describes the changes been made to the text storage. 17 | /// 18 | /// The difference from the ParagraphRangeChange enum is that this enum does a substring from the text storage 19 | /// to create a ParagraphDescriptor, which is a little more time consuming. 20 | /// Therefore ParagraphChange is using only for the delegate notifications, not for the internal calculations 21 | enum ParagraphChange { 22 | case insertedParagraph(index: Int, descriptor: ParagraphDescriptor) 23 | case removedParagraph(index: Int) 24 | case editedParagraph(index: Int, descriptor: ParagraphDescriptor) 25 | 26 | static func from(rangeChanges: [ParagraphRangeChange], textStorage: ParagraphTextStorage) -> [Self] { 27 | var changes = [Self]() 28 | guard !rangeChanges.isEmpty else { return changes } 29 | 30 | for rangeChange in rangeChanges { 31 | switch rangeChange { 32 | case .insertedParagraph(index: let index, range: let range): 33 | let attrString = textStorage.attributedSubstring(from: range) 34 | let paragraphDescriptor = ParagraphDescriptor(attributedString: attrString, storageRange: range) 35 | changes.append(insertedParagraph(index: index, descriptor: paragraphDescriptor)) 36 | case .removedParagraph(index: let index): 37 | changes.append(removedParagraph(index: index)) 38 | case .editedParagraph(index: let index, range: let range): 39 | let attrString = textStorage.attributedSubstring(from: range) 40 | let paragraphDescriptor = ParagraphDescriptor(attributedString: attrString, storageRange: range) 41 | changes.append(editedParagraph(index: index, descriptor: paragraphDescriptor)) 42 | } 43 | } 44 | 45 | return changes 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/ParagraphTextKit/Extensions/String (extended).swift: -------------------------------------------------------------------------------- 1 | // 2 | // String (extended).swift 3 | // ParagraphTextKit 4 | // 5 | // Copyright (c) 2020 Vitalii Vashchenko 6 | // 7 | // This software is released under the MIT License. 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // Created by Vitalii Vashchenko on 5/17/16. 11 | // 12 | 13 | import Foundation 14 | 15 | extension String { 16 | public var attributedPresentation: NSAttributedString { 17 | NSAttributedString(string: self) 18 | } 19 | } 20 | 21 | public extension String { 22 | var floatValue: Float { 23 | return (self as NSString).floatValue 24 | } 25 | 26 | var doubleValue: Double { 27 | return (self as NSString).doubleValue 28 | } 29 | 30 | var boolValue: Bool { 31 | return (self as NSString).boolValue 32 | } 33 | 34 | var integerValue: Int { 35 | return (self as NSString).integerValue 36 | } 37 | 38 | var unsignedValue: UInt { 39 | return UInt((self as NSString).integerValue) 40 | } 41 | 42 | var range: NSRange { 43 | NSRange(location: 0, length: length) 44 | } 45 | 46 | var endsWithNewline: Bool { 47 | !(last == nil || last?.isNewline == false) 48 | } 49 | 50 | func contains(_ set: CharacterSet) -> Bool { 51 | components(separatedBy: set).count > 1 52 | } 53 | 54 | subscript(range: NSRange) -> Substring { 55 | get { 56 | if range.location == NSNotFound { 57 | return "" 58 | } else { 59 | let swiftRange = Range(range, in: self)! 60 | return self[swiftRange] 61 | } 62 | } 63 | } 64 | 65 | /// Array of paragraphs of the string. Each paragraph except the last one ends with 'new line' character 66 | var paragraphs: [String] { 67 | var paragraphs = components(separatedBy: .newlines) 68 | for i in 0 ..< paragraphs.count { 69 | if i != paragraphs.count - 1 { 70 | paragraphs[i] += "\n" 71 | } 72 | } 73 | return paragraphs 74 | } 75 | 76 | /// Raw length of the string; unicode characters counts as sum of its scalars 77 | var length: Int { 78 | self.utf16.count 79 | } 80 | 81 | func utfParagraphRange(at location: Int) -> NSRange { 82 | (self as NSString).lineRange(for: NSRange(location: location, length: 0)) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/ParagraphTextKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ParagraphTextKit 2 | 3 | ParagraphTextStorage is a subclass of NSTextStorage class. It operates the whole paragraphs of text and notifies its paragraph delegate if user made any changes to any paragraph. Delegate receives only touched paragraph descriptors. 4 | 5 | This behavior is important when the paragraph entity represents a specific object in your model. In many text editor cases it's much easier to operate individual paragraphs, not the complete attributed string. So, in case of ParagraphTextStorage, every single change to the text storage can be reflected in the specific object of your model. 6 | 7 | As a result, you now get an opportunity to track changes paragraph-by-paragraph and reflect those changes in your model. That will make it easy not only to build a custom business logic with your model, but also to convert that model into a persistant state using Core Data, for example. 8 | 9 | This project is licensed under the terms of the MIT license. 10 | 11 | ### Requirements 12 | 13 | - iOS 13.0+ / macOS 10.15+ 14 | - Swift 5.1+ 15 | - Xcode 11.0+ 16 | 17 | ## Usage: 18 | Basic code to make it work: 19 | 20 | // setup the system 21 | let textStorage = ParagraphTextStorage() 22 | textStorage.paragraphDelegate = self 23 | 24 | let layoutManager = NSLayoutManager() 25 | textStorage.addLayoutManager(layoutManager) 26 | 27 | let textContainer = NSTextContainer() 28 | layoutManager.addTextContainer(textContainer) 29 | 30 | let textView = NSTextView(frame: someFrame, textContainer: textContainer) 31 | someScrollView.documentView = textView 32 | 33 | 34 | If you need to sync your model with ParagraphTextStorage content, set the paragraphDelegate to adopt the ParagraphTextStorageDelegate protocol. 35 | It's simple: 36 | 37 | var presentedParagraphs: [NSAttributedString] { 38 | yourModel.paragraphs 39 | } 40 | 41 | func textStorage(_ textStorage: ParagraphTextStorage, didChangeParagraphs changes: [ParagraphTextStorage.ParagraphChange]) { 42 | for change in changes { 43 | switch change { 44 | case .insertedParagraph(index: let index, descriptor: let paragraphDescriptor): 45 | yourModel.insert(paragraphDescriptor.text, at: index) 46 | 47 | case .removedParagraph(index: let index): 48 | yourModel.remove(at: index) 49 | 50 | case .editedParagraph(index: let index, descriptor: let paragraphDescriptor): 51 | yourModel[index] = paragraphDescriptor.text 52 | } 53 | } 54 | } 55 | 56 | Finally, set the paragraphDelegate property of the ParagraphTextStorage instance. 57 | 58 | textStorage.paragraphDelegate = yourDelegateObject 59 | 60 | That's all you need to implement to make things work. 61 | 62 | ### Important changes in version 1.2: 63 | Updated delegate protocol with new requrements of the model which now should be able to present its paragraphs as array of NSAttributedStrings. That allows ParagraphTextKit to load the initial state of your model into its underlying text storage property. 64 | 65 | In previous versions ParagraphTextKit was able to do its job only if your initial text model was empty. If you've already had some paragraphs in your model then you'd need to set ParagraphTextStorage content by yourself right after the initialization. Now ParagraphTextKit takes that onto itself completely. 66 | 67 | Note: the required **paragraphCount()** method is now replaced with the **presentedParagraphs** computed property. 68 | 69 | ### Important changes in version 1.1: 70 | In version 1.0 paragraph diffs algorhythm was too basic. It did the job but its flaw was its abstraction, since the purpose of the algorhythm was not the accuracy of paragraph indexes in diff calculation but the integrity of delegate notifications that end up equal with NSTextStorage edited ranges. It means that paragraph indexes during the storage mutation waere calculated with sacrification of the exact accuracy of paragraph indexes in which changes had been made, but the ending delegate notofications still guaranteed the sync with the NSTextStorage object. 71 | 72 | But that logic leads to some confusing behaviour when you need to update your model with exact paragraph indexes and, for example, calculate the style for next paragraph. 73 | 74 | In version 1.1 that flaw is fixed. Now your delegate object gets notified of the exact changes happened in the NSTextStorage object. 75 | 76 | And even more good news: unit tests now track aforementioned integrity of operations. 77 | 78 | ## Installation: 79 | ### Swift Package Manager 80 | To integrate using Apple's Swift package manager, add the following as a dependency to your Package.swift: 81 | 82 | .package(url: "https://github.com/CineDev/ParagraphTextKit.git", .upToNextMajor(from: "1.0.0")) 83 | 84 | Then, specify "ParagraphTextKit" as a dependency of the Target in which you wish to use ParagraphTextKit. 85 | 86 | Lastly, run the following command: 87 | 88 | swift package update 89 | 90 | -------------------------------------------------------------------------------- /Sources/ParagraphTextKit/Structs/ParagraphRangeChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParagraphRangeChange.swift 3 | // ParagraphTextKit 4 | // 5 | // Copyright (c) 2020 Vitalii Vashchenko 6 | // 7 | // This software is released under the MIT License. 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // Created by Vitalii Vashchenko on 5/7/2020. 11 | // 12 | 13 | import Foundation 14 | 15 | internal extension ParagraphTextStorage { 16 | /// ParagraphRangeChange structure describes the changes been made to the text storage. 17 | /// 18 | /// The difference from the ParagraphChange enum is that this operates with NSRanges only, not with 19 | /// substrings from the text storage, because it is slightly more time consuming operation. 20 | /// 21 | /// Therefore ParagraphRangeChange structur is used only for the internal calculations 22 | enum ParagraphRangeChange { 23 | case insertedParagraph(index: Int, range: NSRange) 24 | case removedParagraph(index: Int) 25 | case editedParagraph(index: Int, range: NSRange) 26 | 27 | static func from(difference: CollectionDifference, 28 | baseOffset: Int, baseParagraphRange: NSRange, 29 | insertionLocation: Int) -> [Self] { 30 | guard !difference.isEmpty else { return [] } 31 | var changes = [Self]() 32 | 33 | // edited index will always be the first one, no matter if paragraphs was added or removes, 34 | // because that's how Apple's TextKit works 35 | var editedIndex: Int? 36 | var lastIndexEdited: Bool = false 37 | 38 | // make sure that some paragraph was edited 39 | if let insertion = difference.insertions.first { 40 | 41 | switch insertion { 42 | case .insert(offset: let insertOffset, element: let insertionRange, associatedWith: _): 43 | guard let deletion = difference.removals.first else { break } 44 | 45 | switch deletion { 46 | case .remove(offset: let removeOffset, element: let removedRange, associatedWith: _): 47 | 48 | // edited paragraph means that the removing and inserting changes are the same 49 | if insertOffset == removeOffset { 50 | // but if the first change happens outside of the first paragraph range, 51 | // then this is actually an exception from the rule that 'edited index will always be the first one' 52 | // and in this case the LAST index should be notified as 'edited index' 53 | if insertionLocation == baseParagraphRange.location && removedRange == baseParagraphRange && baseParagraphRange.max > 0 { 54 | if insertionRange.location == baseParagraphRange.location && difference.insertions.count > 1 || 55 | removedRange.location == baseParagraphRange.location && difference.removals.count > 1 { 56 | // except the case when the whole text storage content has been deleted... 57 | // ... like when the user selects all the text and hits 'Delete' 58 | if removedRange.max > 0 && insertionRange.max == 0 { } else { 59 | lastIndexEdited = true 60 | break 61 | } 62 | } 63 | } 64 | 65 | // edited paragraph is alway corresponds to inserted change 66 | let paragraphIndex = insertOffset + baseOffset 67 | changes.append(.editedParagraph(index: paragraphIndex, range: insertionRange)) 68 | editedIndex = insertOffset 69 | } 70 | default: 71 | break 72 | } 73 | default: 74 | break 75 | } 76 | } 77 | 78 | for change in difference { 79 | switch change { 80 | case .insert(offset: let offset, element: let insertionRange, associatedWith: _): 81 | let paragraphIndex = offset + baseOffset 82 | 83 | if lastIndexEdited && difference.insertions.last == change && difference.insertions.count > difference.removals.count || 84 | lastIndexEdited && difference.insertions.first == change && difference.removals.count > difference.insertions.count { 85 | // append edited paragraph only if it has really changed 86 | if insertionRange.length != baseParagraphRange.length && difference.insertions.count > difference.removals.count { 87 | changes.append(.editedParagraph(index: paragraphIndex, range: insertionRange)) 88 | } 89 | continue 90 | } 91 | 92 | guard offset != editedIndex else { continue } 93 | changes.append(.insertedParagraph(index: paragraphIndex, range: insertionRange)) 94 | 95 | case .remove(offset: let offset, element: _, associatedWith: _): 96 | let paragraphIndex = offset + baseOffset 97 | 98 | if lastIndexEdited && difference.removals.first == change && difference.insertions.count > difference.removals.count || 99 | lastIndexEdited && difference.removals.last == change && difference.removals.count > difference.insertions.count { 100 | if difference.removals.count > difference.insertions.count, 101 | let firstInsertion = difference.insertions.first, 102 | let lastTouched = difference.removals.last, 103 | case CollectionDifference.Change.insert(offset: _, element: let range, associatedWith: _) = firstInsertion, 104 | case CollectionDifference.Change.remove(offset: _, element: let touchedRange, associatedWith: _) = lastTouched { 105 | if touchedRange.length != range.length { 106 | changes.append(.editedParagraph(index: paragraphIndex, range: range)) 107 | } 108 | } 109 | continue 110 | } 111 | 112 | guard offset != editedIndex else { continue } 113 | changes.append(.removedParagraph(index: paragraphIndex)) 114 | } 115 | } 116 | 117 | return changes 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/ParagraphTextKit/ParagraphTextStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParagraphTextStorage.swift 3 | // ParagraphTextKit 4 | // 5 | // Copyright (c) 2020 Vitalii Vashchenko 6 | // 7 | // This software is released under the MIT License. 8 | // https://opensource.org/licenses/MIT 9 | // 10 | // Created by Vitalii Vashchenko on 04/16/20. 11 | // 12 | 13 | 14 | #if os(macOS) 15 | import Cocoa 16 | #elseif os(iOS) 17 | import UIKit 18 | #endif 19 | import Combine 20 | 21 | // ParagraphTextStorage is a subclass of NSTextStorage class. 22 | // It works with whole text paragraphs and notifies its 23 | // paragraph delegate of any changes in any paragraph. 24 | // Delegate receives the touched paragraph descriptors and their indexes. 25 | open class ParagraphTextStorage: NSTextStorage { 26 | private let storage = NSMutableAttributedString() 27 | 28 | public override var length: Int { 29 | storage.length 30 | } 31 | 32 | /// Text storage string representation with read-only access 33 | public override var string : String { 34 | storage.mutableString as String 35 | } 36 | 37 | /// The array of the text storage paragraphs 38 | public fileprivate(set) var paragraphRanges: [NSRange] = [.zero] 39 | 40 | /// When a delegate is set ParagraphTextStorage initiates the syncing process with its paragraph model. 41 | /// And it's crucial that delegate notifications are disbled during this process. 42 | /// This property serves exactly this purpose. 43 | fileprivate var isSyncingWithDelegate = false 44 | 45 | /// Delegate watches for any edits in the storage paragraphs 46 | public weak var paragraphDelegate: ParagraphTextStorageDelegate? { 47 | didSet { 48 | sync(with: paragraphDelegate) 49 | } 50 | } 51 | 52 | /// Helper array to store paragraph data before editing to compare with actual changes after they've being made 53 | private var indexesBeforeEditing = [Int]() 54 | 55 | /// Helper var indicating that after the editing the resulting range of the next paragraph will be equal to the first edited paragraph 56 | /// and that might confuse the diff algorhythm. 57 | /// 58 | /// That confusing results in the case when the diff algorhythm will not notify the delegate that the next paragraph was edited, 59 | /// because the algorhythm will consider that if the range is the same, then there was no change at all 60 | private var nextEditedParagraphWillHaveRangeEqualWithFirst = false 61 | 62 | /// 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. 63 | /// 64 | /// It is important, because if the edit with the same length of the selected text , the diff algrothythm 65 | /// won't recognize it as a change, since the result text storage will have the same paragraph lengths 66 | private var editHasSameLength = false 67 | 68 | /// Subscriber to the NSTextStorage.willProcessEditingNotification 69 | private var processingSubscriber: AnyCancellable? 70 | 71 | deinit { 72 | processingSubscriber?.cancel() 73 | } 74 | 75 | /// Method is crusial for correct calculations of paragraph ranges. 76 | /// 77 | /// Its importans is due to the fact that any touches to the text storage during the processEditing private methods involved results in 78 | /// random failures when text is layouting. And we can't just override the processEditing method and fix paragraph ranges after calling super, 79 | /// because when the processEditing method is finished it calles some TextKit private APIs. 80 | /// 81 | /// The NSTextStorage.willProcessEditingNotification notification ensures that we call fixParagraphRanges method at the right time, 82 | /// when all the private APIs have done their job. 83 | private final func startProcessingSubscriber() { 84 | processingSubscriber = NotificationCenter.default.publisher(for: NSTextStorage.willProcessEditingNotification) 85 | .compactMap{ $0.object as? ParagraphTextStorage } 86 | .sink { sender in 87 | if sender == self { 88 | self.fixParagraphRanges() 89 | } 90 | } 91 | } 92 | 93 | private final func sync(with theDelegate: ParagraphTextStorageDelegate?) { 94 | guard var paragraphsAvailable = theDelegate?.presentedParagraphs else { return } 95 | 96 | defer { 97 | isSyncingWithDelegate = false 98 | } 99 | 100 | // make sure that the delegate becomes in sync with paragraphs, when initialized with empty storage 101 | if length == 0 && paragraphsAvailable.count == 0 { 102 | paragraphDelegate?.textStorage(self, didChangeParagraphs: [ParagraphChange.insertedParagraph(index: 0, descriptor: paragraphDescriptor(atParagraphIndex: 0))]) 103 | return 104 | } 105 | 106 | isSyncingWithDelegate = true 107 | 108 | // first, make sure the there's no errors in delegate's paragraphs and all of them end 109 | // with newline character (except the last one) 110 | var changes: [ParagraphChange] = [] 111 | paragraphsAvailable = fixParagraphs(paragraphsAvailable, changes: &changes) 112 | 113 | // make sure we won't reload the content that is still actual 114 | if paragraphRanges.count == paragraphsAvailable.count { 115 | for (index, range) in paragraphRanges.enumerated() { 116 | let substring = attributedSubstring(from: range) 117 | let modelString = paragraphsAvailable[index] 118 | 119 | if substring != modelString { 120 | beginEditing() 121 | replaceCharacters(in: range, with: modelString) 122 | endEditing() 123 | } 124 | } 125 | } else { 126 | // get the whole linear attributed string 127 | let completeString = NSMutableAttributedString() 128 | paragraphsAvailable.forEach{ completeString.append($0) } 129 | 130 | // replace the self content with a new one 131 | beginEditing() 132 | replaceCharacters(in: NSRange(location: 0, length: length), with: completeString) 133 | endEditing() 134 | } 135 | 136 | if !changes.isEmpty { 137 | // notify the delegate that the changes have been made to the initial storage to fix paragraph newlines 138 | theDelegate?.textStorage(self, didChangeParagraphs: changes) 139 | } 140 | } 141 | 142 | private final func fixParagraphs(_ paragraphs: [NSAttributedString], changes: inout [ParagraphChange]) -> [NSAttributedString] { 143 | var currentRange = NSRange.zero 144 | var fixedParagraphs = paragraphs 145 | 146 | for (index, attrString) in paragraphs.enumerated() { 147 | currentRange.location = currentRange.max 148 | currentRange.length = attrString.length 149 | 150 | // if non-last paragraphs haven't newline character at the end, append it 151 | if !attrString.string.endsWithNewline && index < paragraphs.count - 1 { 152 | let fixedAttrString = NSMutableAttributedString(attributedString: attrString) 153 | fixedAttrString.beginEditing() 154 | fixedAttrString.replaceCharacters(in: _NSRange(location: fixedAttrString.range.max, length: 0), with: "\n") 155 | fixedAttrString.endEditing() 156 | fixedParagraphs[index] = fixedAttrString 157 | 158 | let descriptor = ParagraphDescriptor(attributedString: fixedAttrString, storageRange: currentRange) 159 | changes.append(ParagraphChange.editedParagraph(index: index, descriptor: descriptor)) 160 | } 161 | 162 | // if last paragraph have newline character at the end, remove it 163 | if attrString.string.endsWithNewline && index == paragraphs.count - 1 { 164 | let fixedAttrString = NSMutableAttributedString(attributedString: attrString) 165 | let deleteRange = NSRange(location: fixedAttrString.range.max - 1, length: 1) 166 | fixedAttrString.beginEditing() 167 | fixedAttrString.deleteCharacters(in: deleteRange) 168 | fixedAttrString.endEditing() 169 | fixedParagraphs[index] = fixedAttrString 170 | 171 | let paragraphRange = NSRange(location: currentRange.location, length: currentRange.length - 1) 172 | let descriptor = ParagraphDescriptor(attributedString: fixedAttrString, storageRange: paragraphRange) 173 | changes.append(ParagraphChange.editedParagraph(index: index, descriptor: descriptor)) 174 | } 175 | } 176 | 177 | return fixedParagraphs 178 | } 179 | 180 | 181 | // MARK: - NSTextStorage Primitive Methods 182 | 183 | open override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] { 184 | storage.attributes(at: location, effectiveRange: range) 185 | } 186 | 187 | open override func replaceCharacters(in range: NSRange, with str: String) { 188 | if processingSubscriber == nil { 189 | startProcessingSubscriber() 190 | } 191 | 192 | let delta = str.length - range.length 193 | 194 | indexesBeforeEditing = paragraphIndexes(in: range) 195 | 196 | if delta == 0 && str.length > 0 { 197 | editHasSameLength = true 198 | } else { 199 | editHasSameLength = false 200 | } 201 | 202 | if indexesBeforeEditing.count > 1 && delta < 0 { 203 | let firstRange = paragraphRanges[indexesBeforeEditing[0]] 204 | 205 | if firstRange.location == range.location && range.max > firstRange.max { 206 | let affectedRanges = indexesBeforeEditing.map{ paragraphRanges[$0] } 207 | let checkRanges = Array(affectedRanges.dropLast()) 208 | let sum = checkRanges.reduce(0, { $0 + $1.length }) 209 | 210 | if affectedRanges.last!.length - abs(delta + sum) == firstRange.length { 211 | nextEditedParagraphWillHaveRangeEqualWithFirst = true 212 | } 213 | } 214 | } 215 | 216 | beginEditing() 217 | storage.replaceCharacters(in: range, with: str) 218 | edited(.editedCharacters, range: range, changeInLength: delta) 219 | endEditing() 220 | } 221 | 222 | open override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) { 223 | beginEditing() 224 | storage.setAttributes(attrs, range: range) 225 | edited(.editedAttributes, range: range, changeInLength: 0) 226 | endEditing() 227 | } 228 | 229 | 230 | // MARK: - Paragraph Management 231 | 232 | private final func fixParagraphRanges() { 233 | defer { 234 | nextEditedParagraphWillHaveRangeEqualWithFirst = false 235 | indexesBeforeEditing.removeAll() 236 | } 237 | 238 | // empty indexes before editing means that there was no editing happened; 239 | // it indicates that text storage is just updating text attributes, not changing the characters 240 | guard !indexesBeforeEditing.isEmpty else { 241 | 242 | // check if there was attribute changing in progress 243 | if editedMask.contains(.editedAttributes), let existingDelegate = paragraphDelegate { 244 | var changes = [ParagraphChange]() 245 | 246 | // and if true, tell the delegate that some paragraphs were changed 247 | substringParagraphRanges(from: editedRange).forEach { paragraphRange in 248 | let idx = paragraphIndex(at: paragraphRange.location) 249 | let descriptor = paragraphDescriptor(atParagraphIndex: idx) 250 | changes.append(.editedParagraph(index: idx, descriptor: descriptor)) 251 | } 252 | 253 | existingDelegate.textStorage(self, didChangeParagraphs: changes) 254 | } 255 | return 256 | } 257 | 258 | let paragraphsBefore = indexesBeforeEditing.map{ paragraphRanges[$0] } 259 | let paragraphsAfter = substringParagraphRanges(from: editedRange) 260 | 261 | let difference = paragraphsAfter.difference(from: paragraphsBefore) 262 | var changes = ParagraphRangeChange.from(difference: difference, 263 | baseOffset: indexesBeforeEditing.first!, 264 | baseParagraphRange: paragraphsBefore.first!, 265 | insertionLocation: editedRange.location) 266 | 267 | // if delta of edits is zero (user could just paste the same-length text over the same-length selected text) 268 | // make sure that the delegate will be notified of those changes, since the diff algorhythm won't recognize 269 | // any changes in that case 270 | guard changes.count != 0 && !editHasSameLength else { 271 | indexesBeforeEditing.forEach{ 272 | changes.append(ParagraphRangeChange.editedParagraph(index: $0, range: paragraphRanges[$0])) 273 | } 274 | 275 | if let existingDelegate = paragraphDelegate, !isSyncingWithDelegate { 276 | let descriptedChanges = ParagraphChange.from(rangeChanges: changes, textStorage: self) 277 | existingDelegate.textStorage(self, didChangeParagraphs: descriptedChanges) 278 | } 279 | 280 | return 281 | } 282 | 283 | var hasEditedChange = false 284 | changes.forEach{ change in 285 | if case ParagraphRangeChange.editedParagraph(index: _, range: _) = change { hasEditedChange = true } 286 | } 287 | 288 | // if there's 'next edited paragraph has same range as the first one' situation ... 289 | if nextEditedParagraphWillHaveRangeEqualWithFirst && !hasEditedChange { 290 | // we need to decrement removed indexes ... 291 | for (i, change) in changes.enumerated() { 292 | if case ParagraphRangeChange.removedParagraph(index: let index) = change { 293 | changes[i] = .removedParagraph(index: index - 1) 294 | } 295 | } 296 | // and to add the 'editedParagraph' change, so the delegate will be notified of edited paragraph 297 | changes.append(ParagraphRangeChange.editedParagraph(index: indexesBeforeEditing.first!, range: paragraphsAfter.first!)) 298 | } 299 | 300 | var lastEditedIndex = 0 301 | for change in changes { 302 | switch change { 303 | case .removedParagraph(index: let index): 304 | paragraphRanges.remove(at: index) 305 | lastEditedIndex = lastEditedIndex == 0 ? 0 : lastEditedIndex - 1 306 | case .insertedParagraph(index: let index, range: let range): 307 | paragraphRanges.insert(range, at: index) 308 | lastEditedIndex = index 309 | case .editedParagraph(index: let index, range: let range): 310 | paragraphRanges[index] = range 311 | lastEditedIndex = index 312 | } 313 | } 314 | // ensure that first paragraph starts from 0 (in case if the first paragraph was deleted) 315 | paragraphRanges[0].location = 0 316 | var lastEditedParagraph = paragraphRanges[lastEditedIndex] 317 | 318 | // update location of paragraph ranges following the edited ones 319 | for (i, _) in paragraphRanges.dropFirst(lastEditedIndex + 1).enumerated() { 320 | let normalizedIndex = i + lastEditedIndex + 1 321 | paragraphRanges[normalizedIndex].location = lastEditedParagraph.max 322 | lastEditedParagraph = paragraphRanges[normalizedIndex] 323 | } 324 | 325 | // notify the delegate of changes being made 326 | if let existingDelegate = paragraphDelegate, !isSyncingWithDelegate { 327 | let descriptedChanges = ParagraphChange.from(rangeChanges: changes, textStorage: self) 328 | existingDelegate.textStorage(self, didChangeParagraphs: descriptedChanges) 329 | } 330 | } 331 | 332 | 333 | // MARK: - Paragraph Seeking 334 | 335 | private final func paragraphRanges(from range: NSRange) -> [NSRange] { 336 | paragraphIndexes(in: range).map{ paragraphRanges[$0] } 337 | } 338 | 339 | private final func substringParagraphRanges(from range: NSRange) -> [NSRange] { 340 | let paragraphs = attributedSubstring(from: range).string.paragraphs 341 | let startingParagraphRange = string.utfParagraphRange(at: range.location) 342 | 343 | var ranges = [startingParagraphRange] 344 | 345 | // for inserted (not appended) paragraphs we need this hack 346 | if paragraphs.count > 1 && range.max < length { 347 | var nextLocation = startingParagraphRange.max 348 | 349 | for _ in 1 ..< paragraphs.count { 350 | let paragraph = string.utfParagraphRange(at: nextLocation) 351 | ranges.append(paragraph) 352 | nextLocation = paragraph.max 353 | } 354 | } else { 355 | for paragraph in paragraphs.dropFirst() { 356 | ranges.append( NSRange(location: ranges.last!.max, length: paragraph.length) ) 357 | } 358 | } 359 | 360 | return ranges 361 | } 362 | 363 | 364 | public func paragraphIndex(at location: Int) -> Int { 365 | guard self.length > 0 else { return 0 } 366 | guard location < self.length else { return paragraphRanges.count - 1 } 367 | 368 | return paragraphRanges.firstIndex(where: { $0.contains(location) })! 369 | } 370 | 371 | 372 | public func paragraphIndexes(in range: NSRange) -> [Int] { 373 | guard length > 0 else { return [0] } 374 | 375 | // get paragraphs from the range 376 | var paragraphs = attributedSubstring(from: range).string.paragraphs 377 | if paragraphs.last?.isEmpty == true && editHasSameLength { 378 | paragraphs = paragraphs.dropLast() 379 | } 380 | 381 | // get start/end indexes for substring the existing paragraphs array.. 382 | // .. it's better than iterating through all the paragraphs, especially if text is huge 383 | let firstTouchedIndex = paragraphIndex(at: range.location) 384 | let lastTouchedIndex = paragraphIndex(at: range.max) 385 | 386 | var paragraphIndexes = [Int]() 387 | 388 | // catch the paragraph indexes that are match with given range 389 | for i in firstTouchedIndex...lastTouchedIndex { 390 | 391 | // for recognizing deleted paragraphs, we need this hack 392 | if paragraphs.count > 1 && i > 0 && paragraphRanges.count - 1 >= i { 393 | paragraphIndexes.append(i) 394 | continue 395 | } 396 | 397 | let paragraph = paragraphRanges[i] 398 | 399 | if let _ = paragraph.intersection(range) { 400 | paragraphIndexes.append(i) 401 | } else if range.length == 0 && paragraph.location == range.location { 402 | paragraphIndexes.append(i) 403 | } else if range.length == 0 && paragraph.max == range.location { 404 | paragraphIndexes.append(i) 405 | } 406 | } 407 | 408 | return paragraphIndexes 409 | } 410 | 411 | 412 | // MARK: - Paragraph Descriptors 413 | 414 | public func paragraphDescriptor(atParagraphIndex index: Int) -> ParagraphDescriptor { 415 | let range = paragraphRanges[index] 416 | let string = attributedSubstring(from: range) 417 | return ParagraphDescriptor(attributedString: string, storageRange: range) 418 | } 419 | 420 | public func paragraphDescriptor(atCharacterIndex characterIndex: Int) -> ParagraphDescriptor { 421 | let index = paragraphIndex(at: characterIndex) 422 | return paragraphDescriptor(atParagraphIndex: index) 423 | } 424 | 425 | public func paragraphDescriptors(in range: NSRange) -> [ParagraphDescriptor] { 426 | let indexes = paragraphIndexes(in: range) 427 | return indexes.map{ paragraphDescriptor(atParagraphIndex: $0) } 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /Tests/ParagraphTextKitTests/ParagraphTextStorageTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ParagraphTextKit 3 | 4 | class Delegate: ParagraphTextStorageDelegate { 5 | var paragraphs: [String] = [] 6 | var attributes: [[NSAttributedString.Key: Any]] = [] 7 | 8 | var insertions: [Int] = [] 9 | var removals: [Int] = [] 10 | var editions: [Int] = [] 11 | 12 | private var firstInit = true 13 | 14 | var ranges: [NSRange] { 15 | var location = 0 16 | 17 | return paragraphs.map { string -> NSRange in 18 | let range = NSRange(location: location, length: string.length) 19 | location = range.max 20 | return range 21 | } 22 | } 23 | 24 | var presentedParagraphs: [NSAttributedString] { 25 | paragraphs.map{ $0.attributedPresentation } 26 | } 27 | 28 | func textStorage(_ textStorage: ParagraphTextStorage, didChangeParagraphs changes: [ParagraphTextStorage.ParagraphChange]) { 29 | for change in changes { 30 | switch change { 31 | case .insertedParagraph(index: let index, descriptor: let paragraphDescriptor): 32 | if firstInit { 33 | firstInit = false 34 | } else { 35 | insertions.append(index) 36 | } 37 | 38 | paragraphs.insert(paragraphDescriptor.text, at: index) 39 | attributes.insert(attributes(from: paragraphDescriptor), at: index) 40 | 41 | case .removedParagraph(index: let index): 42 | paragraphs.remove(at: index) 43 | attributes.remove(at: index) 44 | removals.append(index) 45 | 46 | case .editedParagraph(index: let index, descriptor: let paragraphDescriptor): 47 | paragraphs[index] = paragraphDescriptor.text 48 | attributes[index] = attributes(from: paragraphDescriptor) 49 | editions.append(index) 50 | } 51 | } 52 | } 53 | 54 | func attributes(from paragraphDescriptor: ParagraphTextStorage.ParagraphDescriptor) -> [NSAttributedString.Key: Any] { 55 | if !paragraphDescriptor.text.isEmpty { 56 | return paragraphDescriptor.attributedString.attributes(at: 0, effectiveRange: nil) 57 | } 58 | return [:] 59 | } 60 | } 61 | 62 | 63 | // MARK: - Delegate Syncing Tests 64 | 65 | final class ParagraphDelegateHasThreeCorrectInitialParagraphTest: XCTestCase { 66 | let textStorage = ParagraphTextStorage() 67 | let delegate = Delegate() 68 | 69 | override func setUp() { 70 | super.setUp() 71 | 72 | delegate.paragraphs.append("1\n") 73 | delegate.paragraphs.append("2\n") 74 | delegate.paragraphs.append("3") 75 | delegate.attributes.append([:]) 76 | delegate.attributes.append([:]) 77 | delegate.attributes.append([:]) 78 | 79 | textStorage.paragraphDelegate = delegate 80 | } 81 | 82 | override func tearDown() { 83 | super.tearDown() 84 | } 85 | 86 | func testParagraphTextStorageDelegate() { 87 | XCTAssertTrue(textStorage.paragraphRanges.isEmpty == false, 88 | "ParagraphTextStorage should have one paragraph descriptor at init") 89 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 90 | "ParagraphTextStorageDelegate should have three paragraphs after the syncing") 91 | XCTAssertTrue(delegate.paragraphs[0] == "1\n", 92 | "ParagraphTextStorageDelegate first paragraph should contain '1\n' string after the syncing") 93 | XCTAssertTrue(delegate.paragraphs[1] == "2\n", 94 | "ParagraphTextStorageDelegate second paragraph should contain '2\n' string after the syncing") 95 | XCTAssertTrue(delegate.paragraphs[2] == "3", 96 | "ParagraphTextStorageDelegate third paragraph should contain '3' string after the syncing") 97 | XCTAssertTrue(textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string == "1\n", 98 | "ParagraphTextStorage first paragraph should contain '1\n' string after the syncing") 99 | XCTAssertTrue(textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string == "2\n", 100 | "ParagraphTextStorage second paragraph should contain '2\n' string after the syncing") 101 | XCTAssertTrue(textStorage.attributedSubstring(from: textStorage.paragraphRanges[2]).string == "3", 102 | "ParagraphTextStorage third paragraph should contain '3' string after the syncing") 103 | } 104 | } 105 | 106 | final class ParagraphDelegateHasThreeMixedInitialParagraphTest: XCTestCase { 107 | let textStorage = ParagraphTextStorage() 108 | let delegate = Delegate() 109 | 110 | override func setUp() { 111 | super.setUp() 112 | 113 | delegate.paragraphs.append("1") 114 | delegate.paragraphs.append("2\n") 115 | delegate.paragraphs.append("3") 116 | delegate.attributes.append([:]) 117 | delegate.attributes.append([:]) 118 | delegate.attributes.append([:]) 119 | 120 | textStorage.paragraphDelegate = delegate 121 | } 122 | 123 | override func tearDown() { 124 | super.tearDown() 125 | } 126 | 127 | func testParagraphTextStorageDelegate() { 128 | XCTAssertTrue(textStorage.paragraphRanges.isEmpty == false, 129 | "ParagraphTextStorage should have one paragraph descriptor at init") 130 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 131 | "ParagraphTextStorageDelegate should have three paragraphs after the syncing") 132 | XCTAssertTrue(delegate.paragraphs[0] == "1\n", 133 | "ParagraphTextStorageDelegate first paragraph should contain '1\n' string after the syncing") 134 | XCTAssertTrue(delegate.paragraphs[1] == "2\n", 135 | "ParagraphTextStorageDelegate second paragraph should contain '2\n' string after the syncing") 136 | XCTAssertTrue(delegate.paragraphs[2] == "3", 137 | "ParagraphTextStorageDelegate third paragraph should contain '3' string after the syncing") 138 | XCTAssertTrue(textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string == "1\n", 139 | "ParagraphTextStorage first paragraph should contain '1\n' string after the syncing") 140 | XCTAssertTrue(textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string == "2\n", 141 | "ParagraphTextStorage second paragraph should contain '2\n' string after the syncing") 142 | XCTAssertTrue(textStorage.attributedSubstring(from: textStorage.paragraphRanges[2]).string == "3", 143 | "ParagraphTextStorage third paragraph should contain '3' string after the syncing") 144 | } 145 | } 146 | 147 | 148 | final class ParagraphDelegateHasTwoInitialParagraphTest: XCTestCase { 149 | let textStorage = ParagraphTextStorage() 150 | let delegate = Delegate() 151 | 152 | override func setUp() { 153 | super.setUp() 154 | 155 | delegate.paragraphs.append("1") 156 | delegate.paragraphs.append("2") 157 | delegate.attributes.append([:]) 158 | delegate.attributes.append([:]) 159 | 160 | textStorage.paragraphDelegate = delegate 161 | } 162 | 163 | override func tearDown() { 164 | super.tearDown() 165 | } 166 | 167 | func testParagraphTextStorageDelegate() { 168 | XCTAssertTrue(textStorage.paragraphRanges.isEmpty == false, 169 | "ParagraphTextStorage should have one paragraph descriptor at init") 170 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 171 | "ParagraphTextStorage should have two paragraphs after the syncing") 172 | XCTAssertTrue(delegate.paragraphs[0] == "1\n", 173 | "ParagraphTextStorageDelegate first paragraph should contain '1\n' string after the syncing") 174 | XCTAssertTrue(delegate.paragraphs[1] == "2", 175 | "ParagraphTextStorageDelegate second paragraph should contain '2' string after the syncing") 176 | XCTAssertTrue(textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string == "1\n", 177 | "ParagraphTextStorage first paragraph should contain '1\n' string after the syncing") 178 | XCTAssertTrue(textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string == "2", 179 | "ParagraphTextStorage second paragraph should contain '2' string after the syncing") 180 | } 181 | } 182 | 183 | final class ParagraphDelegateHasTwoInitialIncorrectParagraphsTest: XCTestCase { 184 | let textStorage = ParagraphTextStorage() 185 | let delegate = Delegate() 186 | 187 | override func setUp() { 188 | super.setUp() 189 | 190 | delegate.paragraphs.append("1") 191 | delegate.paragraphs.append("2\n") 192 | delegate.attributes.append([:]) 193 | delegate.attributes.append([:]) 194 | 195 | textStorage.paragraphDelegate = delegate 196 | } 197 | 198 | override func tearDown() { 199 | super.tearDown() 200 | } 201 | 202 | func testParagraphTextStorageDelegate() { 203 | XCTAssertTrue(textStorage.paragraphRanges.isEmpty == false, 204 | "ParagraphTextStorage should have one paragraph descriptor at init") 205 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 206 | "ParagraphTextStorage should have two paragraphs after the syncing") 207 | XCTAssertTrue(delegate.paragraphs[0] == "1\n", 208 | "ParagraphTextStorageDelegate first paragraph should contain '1\n' string after the syncing") 209 | XCTAssertTrue(delegate.paragraphs[1] == "2", 210 | "ParagraphTextStorageDelegate second paragraph should contain '2' string after the syncing") 211 | XCTAssertTrue(textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string == "1\n", 212 | "ParagraphTextStorage first paragraph should contain '1\n' string after the syncing") 213 | XCTAssertTrue(textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string == "2", 214 | "ParagraphTextStorage second paragraph should contain '2' string after the syncing") 215 | } 216 | } 217 | 218 | final class ParagraphDelegateHasOneInitialParagraphTest: XCTestCase { 219 | let textStorage = ParagraphTextStorage() 220 | let delegate = Delegate() 221 | 222 | override func setUp() { 223 | super.setUp() 224 | 225 | delegate.paragraphs.append("1") 226 | delegate.attributes.append([:]) 227 | 228 | textStorage.paragraphDelegate = delegate 229 | } 230 | 231 | override func tearDown() { 232 | super.tearDown() 233 | } 234 | 235 | func testParagraphTextStorageDelegate() { 236 | XCTAssertTrue(textStorage.paragraphRanges.isEmpty == false, 237 | "ParagraphTextStorage should have one paragraph descriptor at init") 238 | XCTAssertTrue(textStorage.paragraphRanges.count == 1, 239 | "ParagraphTextStorage should have one paragraph after the syncing") 240 | XCTAssertTrue(delegate.paragraphs[0] == "1", 241 | "ParagraphTextStorageDelegate first paragraph should contain '1' string after the syncing") 242 | XCTAssertTrue(textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string == "1", 243 | "ParagraphTextStorage first paragraph should contain '1' string after the syncing") 244 | } 245 | } 246 | 247 | 248 | 249 | // MARK: - ParagraphTextStorage Algorhythm Tests 250 | 251 | final class ParagraphTextStorageTests: XCTestCase { 252 | let textStorage = ParagraphTextStorage() 253 | let delegate = Delegate() 254 | 255 | override func setUp() { 256 | super.setUp() 257 | 258 | textStorage.paragraphDelegate = delegate 259 | } 260 | 261 | override func tearDown() { 262 | super.tearDown() 263 | } 264 | 265 | func testParagraphTextStorage_Initialization() { 266 | let textStorage = ParagraphTextStorage() 267 | XCTAssertTrue(textStorage.paragraphRanges.isEmpty == false, 268 | "ParagraphTextStorage should have one paragraph descriptor at init") 269 | } 270 | 271 | 272 | // MARK: - Attribute Changing Tests 273 | 274 | func testParagraphTextStorage_ChangeAttributes() { 275 | let string = "First paragraph\nSecond paragraph" 276 | XCTAssertTrue(delegate.insertions.count == 0 && delegate.editions.count == 0 && delegate.removals.count == 0, 277 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 278 | 279 | textStorage.beginEditing() 280 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 281 | textStorage.endEditing() 282 | 283 | XCTAssertTrue(delegate.insertions.count == 1 && delegate.editions.count == 1 && delegate.removals.count == 0, 284 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 285 | 286 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 287 | "ParagraphTextStorage should now have 2 paragraphs") 288 | 289 | let firstRange = NSRange(location: 0, length: string.paragraphs[0].length) 290 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: string.paragraphs[1].length) 291 | 292 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 293 | textStorage.paragraphRanges[1] == secondRange, 294 | "ParagraphTextStorage paragraph ranges should be correct") 295 | 296 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 297 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 298 | 299 | #if !os(macOS) 300 | textStorage.beginEditing() 301 | textStorage.setAttributes([.foregroundColor: UIColor.lightText], range: secondRange) 302 | textStorage.endEditing() 303 | 304 | XCTAssertTrue(delegate.insertions.count == 1 && delegate.editions.count == 2 && delegate.removals.count == 0, 305 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 306 | 307 | XCTAssertTrue(delegate.attributes[0].isEmpty && 308 | delegate.attributes[1][.foregroundColor] as? UIColor == UIColor.lightText, 309 | "ParagraphTextStorage delegate attributes should match the ParagraphTextStorage") 310 | 311 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 312 | textStorage.paragraphRanges[1] == secondRange, 313 | "ParagraphTextStorage paragraph ranges should be correct") 314 | 315 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 316 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 317 | 318 | #else 319 | textStorage.beginEditing() 320 | textStorage.setAttributes([.foregroundColor: NSColor.textColor], range: secondRange) 321 | textStorage.endEditing() 322 | 323 | XCTAssertTrue(delegate.insertions.count == 1 && delegate.editions.count == 2 && delegate.removals.count == 0, 324 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 325 | 326 | XCTAssertTrue(delegate.attributes[0].isEmpty && 327 | delegate.attributes[1][.foregroundColor] as? NSColor == NSColor.textColor, 328 | "ParagraphTextStorage delegate attributes should match the ParagraphTextStorage") 329 | 330 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 331 | textStorage.paragraphRanges[1] == secondRange, 332 | "ParagraphTextStorage paragraph ranges should be correct") 333 | 334 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 335 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 336 | 337 | #endif 338 | } 339 | 340 | 341 | // MARK: - Insertion Tests 342 | 343 | func testParagraphTextStorage_InsertNewLineInMiddleHeap() { 344 | let string = "First paragraph\nSecond paragraph\nThirdParagraph\nFourthParagraph\nFifthParagraph" 345 | let editString = "\n" 346 | 347 | textStorage.beginEditing() 348 | textStorage.replaceCharacters(in: NSRange.zero, with: string) 349 | textStorage.endEditing() 350 | 351 | XCTAssertTrue(delegate.insertions.count == 4 && delegate.editions.count == 1 && delegate.removals.count == 0, 352 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 353 | 354 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3 && delegate.insertions[3] == 4) 355 | XCTAssertTrue(delegate.editions[0] == 0) 356 | 357 | textStorage.beginEditing() 358 | textStorage.replaceCharacters(in: NSRange(location: 33, length: 0), with: editString) 359 | textStorage.endEditing() 360 | 361 | XCTAssertTrue(delegate.insertions.count == 5 && delegate.editions.count == 1 && delegate.removals.count == 0, 362 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 363 | 364 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3 && delegate.insertions[3] == 4 && delegate.insertions[4] == 2) 365 | XCTAssertTrue(delegate.editions[0] == 0) 366 | 367 | let endString = "First paragraph\nSecond paragraph\n\nThirdParagraph\nFourthParagraph\nFifthParagraph" 368 | 369 | XCTAssertTrue(textStorage.paragraphRanges.count == 6, 370 | "ParagraphTextStorage should now have 6 paragraphs") 371 | 372 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 373 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 374 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 375 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 376 | let fifthRange = NSRange(location: NSMaxRange(fourthRange), length: endString.paragraphs[4].length) 377 | let sixthRange = NSRange(location: NSMaxRange(fifthRange), length: endString.paragraphs[5].length) 378 | 379 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 380 | textStorage.paragraphRanges[1] == secondRange && 381 | textStorage.paragraphRanges[2] == thirdRange && 382 | textStorage.paragraphRanges[3] == fourthRange && 383 | textStorage.paragraphRanges[4] == fifthRange && 384 | textStorage.paragraphRanges[5] == sixthRange, 385 | "ParagraphTextStorage paragraph ranges should be correct") 386 | 387 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 388 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 389 | } 390 | 391 | func testParagraphTextStorage_InsertThreeNewLinesInMiddleHeap() { 392 | let string = "First paragraph\nSecond paragraph\nThirdParagraph\nFourthParagraph\nFifthParagraph" 393 | let editString = "\n\n\n" 394 | 395 | textStorage.beginEditing() 396 | textStorage.replaceCharacters(in: NSRange.zero, with: string) 397 | textStorage.endEditing() 398 | 399 | textStorage.beginEditing() 400 | textStorage.replaceCharacters(in: NSRange(location: 33, length: 0), with: editString) 401 | textStorage.endEditing() 402 | 403 | XCTAssertTrue(delegate.insertions.count == 7 && delegate.editions.count == 1 && delegate.removals.count == 0, 404 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 405 | 406 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3 && delegate.insertions[3] == 4 && delegate.insertions[4] == 2 && delegate.insertions[5] == 3 && delegate.insertions[6] == 4) 407 | XCTAssertTrue(delegate.editions[0] == 0) 408 | 409 | let endString = "First paragraph\nSecond paragraph\n\n\n\nThirdParagraph\nFourthParagraph\nFifthParagraph" 410 | 411 | XCTAssertTrue(textStorage.paragraphRanges.count == 8, 412 | "ParagraphTextStorage should now have 8 paragraphs") 413 | 414 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 415 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 416 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 417 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 418 | let fifthRange = NSRange(location: NSMaxRange(fourthRange), length: endString.paragraphs[4].length) 419 | let sixthRange = NSRange(location: NSMaxRange(fifthRange), length: endString.paragraphs[5].length) 420 | let seventhRange = NSRange(location: NSMaxRange(sixthRange), length: endString.paragraphs[6].length) 421 | let eighthRange = NSRange(location: NSMaxRange(seventhRange), length: endString.paragraphs[7].length) 422 | 423 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 424 | textStorage.paragraphRanges[1] == secondRange && 425 | textStorage.paragraphRanges[2] == thirdRange && 426 | textStorage.paragraphRanges[3] == fourthRange && 427 | textStorage.paragraphRanges[4] == fifthRange && 428 | textStorage.paragraphRanges[5] == sixthRange && 429 | textStorage.paragraphRanges[6] == seventhRange && 430 | textStorage.paragraphRanges[7] == eighthRange, 431 | "ParagraphTextStorage paragraph ranges should be correct") 432 | 433 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 434 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 435 | } 436 | 437 | 438 | func testParagraphTextStorage_InsertFirstParagraphs() { 439 | let string = "First paragraph\nSecond paragraph" 440 | 441 | textStorage.beginEditing() 442 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 443 | textStorage.endEditing() 444 | 445 | XCTAssertTrue(delegate.insertions.count == 1 && delegate.editions.count == 1 && delegate.removals.count == 0, 446 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 447 | 448 | XCTAssertTrue(delegate.insertions[0] == 1) 449 | XCTAssertTrue(delegate.editions[0] == 0) 450 | 451 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 452 | "ParagraphTextStorage should now have 2 paragraphs") 453 | 454 | let firstRange = NSRange(location: 0, length: string.paragraphs[0].length) 455 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: string.paragraphs[1].length) 456 | 457 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 458 | textStorage.paragraphRanges[1] == secondRange, 459 | "ParagraphTextStorage paragraph ranges should be correct") 460 | 461 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 462 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 463 | } 464 | 465 | func testParagraphTextStorage_InsertEmptyAtBeginning() { 466 | let string = "First paragraph\nSecond paragraph\nThirdParagraph" 467 | let editString = "\n" 468 | 469 | textStorage.beginEditing() 470 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 471 | textStorage.endEditing() 472 | 473 | textStorage.beginEditing() 474 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: editString) 475 | textStorage.endEditing() 476 | 477 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 1 && delegate.removals.count == 0, 478 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 479 | 480 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 0) 481 | XCTAssertTrue(delegate.editions[0] == 0) 482 | 483 | let endString = "\nFirst paragraph\nSecond paragraph\nThirdParagraph" 484 | 485 | XCTAssertTrue(textStorage.paragraphRanges.count == 4, 486 | "ParagraphTextStorage should now have 4 paragraphs") 487 | 488 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 489 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 490 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 491 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 492 | 493 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 494 | textStorage.paragraphRanges[1] == secondRange && 495 | textStorage.paragraphRanges[2] == thirdRange && 496 | textStorage.paragraphRanges[3] == fourthRange, 497 | "ParagraphTextStorage paragraph ranges should be correct") 498 | 499 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 500 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 501 | } 502 | 503 | func testParagraphTextStorage_InsertNonemptyAtBeginning() { 504 | let string = "First paragraph\nSecond paragraph\nThirdParagraph" 505 | let editString = "\naddition" 506 | 507 | textStorage.beginEditing() 508 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 509 | textStorage.endEditing() 510 | 511 | textStorage.beginEditing() 512 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: editString) 513 | textStorage.endEditing() 514 | 515 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 2 && delegate.removals.count == 0, 516 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 517 | 518 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 0) 519 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 1) 520 | 521 | let endString = "\nadditionFirst paragraph\nSecond paragraph\nThirdParagraph" 522 | 523 | XCTAssertTrue(textStorage.paragraphRanges.count == 4, 524 | "ParagraphTextStorage should now have 4 paragraphs") 525 | 526 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 527 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 528 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 529 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 530 | 531 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 532 | textStorage.paragraphRanges[1] == secondRange && 533 | textStorage.paragraphRanges[2] == thirdRange && 534 | textStorage.paragraphRanges[3] == fourthRange, 535 | "ParagraphTextStorage paragraph ranges should be correct") 536 | 537 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 538 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 539 | } 540 | 541 | func testParagraphTextStorage_InsertEmptyInMiddle() { 542 | let string = "First paragraph\nSecond paragraph\nThirdParagraph" 543 | let editString = "\n" 544 | 545 | textStorage.beginEditing() 546 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 547 | textStorage.endEditing() 548 | 549 | textStorage.beginEditing() 550 | textStorage.replaceCharacters(in: NSRange(location: 3, length: 5), with: editString) 551 | textStorage.endEditing() 552 | 553 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 2 && delegate.removals.count == 0, 554 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 555 | 556 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 1) 557 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0) 558 | 559 | let endString = "Fir\nragraph\nSecond paragraph\nThirdParagraph" 560 | 561 | XCTAssertTrue(textStorage.paragraphRanges.count == 4, 562 | "ParagraphTextStorage should now have 4 paragraphs") 563 | 564 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 565 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 566 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 567 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 568 | 569 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 570 | textStorage.paragraphRanges[1] == secondRange && 571 | textStorage.paragraphRanges[2] == thirdRange && 572 | textStorage.paragraphRanges[3] == fourthRange, 573 | "ParagraphTextStorage paragraph ranges should be correct") 574 | 575 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 576 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 577 | } 578 | 579 | func testParagraphTextStorage_InsertNonemptyInMiddle() { 580 | let string = "First paragraph\nSecond paragraph\nThirdParagraph" 581 | let editString = "\naddition" 582 | 583 | textStorage.beginEditing() 584 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 585 | textStorage.endEditing() 586 | 587 | textStorage.beginEditing() 588 | textStorage.replaceCharacters(in: NSRange(location: 3, length: 5), with: editString) 589 | textStorage.endEditing() 590 | 591 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 2 && delegate.removals.count == 0, 592 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 593 | 594 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 1) 595 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0) 596 | 597 | let endString = "Fir\nadditionragraph\nSecond paragraph\nThirdParagraph" 598 | 599 | XCTAssertTrue(textStorage.paragraphRanges.count == 4, 600 | "ParagraphTextStorage should now have 4 paragraphs") 601 | 602 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 603 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 604 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 605 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 606 | 607 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 608 | textStorage.paragraphRanges[1] == secondRange && 609 | textStorage.paragraphRanges[2] == thirdRange && 610 | textStorage.paragraphRanges[3] == fourthRange, 611 | "ParagraphTextStorage paragraph ranges should be correct") 612 | 613 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 614 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 615 | } 616 | 617 | func testParagraphTextStorage_InsertEmptyBetweenParagraphs() { 618 | let string = "First paragraph\nSecond paragraph\nThirdParagraph" 619 | let editString = "\n" 620 | 621 | textStorage.beginEditing() 622 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 623 | textStorage.endEditing() 624 | 625 | textStorage.beginEditing() 626 | textStorage.replaceCharacters(in: NSRange(location: 16, length: 0), with: editString) 627 | textStorage.endEditing() 628 | 629 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 1 && delegate.removals.count == 0, 630 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 631 | 632 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 1) 633 | XCTAssertTrue(delegate.editions[0] == 0) 634 | 635 | let endString = "First paragraph\n\nSecond paragraph\nThirdParagraph" 636 | 637 | XCTAssertTrue(textStorage.paragraphRanges.count == 4, 638 | "ParagraphTextStorage should now have 4 paragraphs") 639 | 640 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 641 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 642 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 643 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 644 | 645 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 646 | textStorage.paragraphRanges[1] == secondRange && 647 | textStorage.paragraphRanges[2] == thirdRange && 648 | textStorage.paragraphRanges[3] == fourthRange, 649 | "ParagraphTextStorage paragraph ranges should be correct") 650 | 651 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 652 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 653 | } 654 | 655 | func testParagraphTextStorage_InsertEmptyBetweenParagraphs2() { 656 | let string = "First paragraph\nSecond paragraph\nThirdParagraph" 657 | let editString = "\n" 658 | 659 | textStorage.beginEditing() 660 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 661 | textStorage.endEditing() 662 | 663 | textStorage.beginEditing() 664 | textStorage.replaceCharacters(in: NSRange(location: 32, length: 0), with: editString) 665 | textStorage.endEditing() 666 | 667 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 1 && delegate.removals.count == 0, 668 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 669 | 670 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 2) 671 | XCTAssertTrue(delegate.editions[0] == 0) 672 | 673 | let endString = "First paragraph\nSecond paragraph\n\nThirdParagraph" 674 | 675 | XCTAssertTrue(textStorage.paragraphRanges.count == 4, 676 | "ParagraphTextStorage should now have 4 paragraphs") 677 | 678 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 679 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 680 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 681 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 682 | 683 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 684 | textStorage.paragraphRanges[1] == secondRange && 685 | textStorage.paragraphRanges[2] == thirdRange && 686 | textStorage.paragraphRanges[3] == fourthRange, 687 | "ParagraphTextStorage paragraph ranges should be correct") 688 | 689 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 690 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 691 | } 692 | 693 | func testParagraphTextStorage_InsertNonemptyBetweenParagraphs() { 694 | let string = "First paragraph\nSecond paragraph\nThirdParagraph" 695 | let editString = "addition\n" 696 | 697 | textStorage.beginEditing() 698 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 699 | textStorage.endEditing() 700 | 701 | textStorage.beginEditing() 702 | textStorage.replaceCharacters(in: NSRange(location: 16, length: 0), with: editString) 703 | textStorage.endEditing() 704 | 705 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 1 && delegate.removals.count == 0, 706 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 707 | 708 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 1) 709 | XCTAssertTrue(delegate.editions[0] == 0) 710 | 711 | let endString = "First paragraph\naddition\nSecond paragraph\nThirdParagraph" 712 | 713 | XCTAssertTrue(textStorage.paragraphRanges.count == 4, 714 | "ParagraphTextStorage should now have 4 paragraphs") 715 | 716 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 717 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 718 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 719 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 720 | 721 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 722 | textStorage.paragraphRanges[1] == secondRange && 723 | textStorage.paragraphRanges[2] == thirdRange && 724 | textStorage.paragraphRanges[3] == fourthRange, 725 | "ParagraphTextStorage paragraph ranges should be correct") 726 | 727 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 728 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 729 | } 730 | 731 | func testParagraphTextStorage_InsertEmptyAtEnd() { 732 | let string = "First paragraph\nSecond paragraph\nThird" 733 | let editString = "\n" 734 | 735 | textStorage.beginEditing() 736 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 737 | textStorage.endEditing() 738 | 739 | textStorage.beginEditing() 740 | textStorage.replaceCharacters(in: NSRange(location: string.length, length: 0), with: editString) 741 | textStorage.endEditing() 742 | 743 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 2 && delegate.removals.count == 0, 744 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 745 | 746 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3) 747 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 2) 748 | 749 | let endString = "First paragraph\nSecond paragraph\nThird\n" 750 | 751 | XCTAssertTrue(textStorage.paragraphRanges.count == 4, 752 | "ParagraphTextStorage should now have 4 paragraphs") 753 | 754 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 755 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 756 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 757 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 758 | 759 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 760 | textStorage.paragraphRanges[1] == secondRange && 761 | textStorage.paragraphRanges[2] == thirdRange && 762 | textStorage.paragraphRanges[3] == fourthRange, 763 | "ParagraphTextStorage paragraph ranges should be correct") 764 | 765 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 766 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 767 | } 768 | 769 | func testParagraphTextStorage_InsertNonemptyAtEnd() { 770 | let string = "First paragraph\nSecond paragraph\nThird" 771 | let editString = "\naddition" 772 | 773 | textStorage.beginEditing() 774 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 775 | textStorage.endEditing() 776 | 777 | textStorage.beginEditing() 778 | textStorage.replaceCharacters(in: NSRange(location: string.length, length: 0), with: editString) 779 | textStorage.endEditing() 780 | 781 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 2 && delegate.removals.count == 0, 782 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 783 | 784 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3) 785 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 2) 786 | 787 | let endString = "First paragraph\nSecond paragraph\nThird\naddition" 788 | 789 | XCTAssertTrue(textStorage.paragraphRanges.count == 4, 790 | "ParagraphTextStorage should now have 4 paragraphs") 791 | 792 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 793 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 794 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 795 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 796 | 797 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 798 | textStorage.paragraphRanges[1] == secondRange && 799 | textStorage.paragraphRanges[2] == thirdRange && 800 | textStorage.paragraphRanges[3] == fourthRange, 801 | "ParagraphTextStorage paragraph ranges should be correct") 802 | 803 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 804 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 805 | } 806 | 807 | 808 | 809 | // MARK: - Editing Tests 810 | 811 | func testParagraphTextStorage_EditFirstParagraph() { 812 | let string = "First paragraph" 813 | 814 | textStorage.beginEditing() 815 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 816 | textStorage.endEditing() 817 | 818 | XCTAssertTrue(delegate.insertions.count == 0 && delegate.editions.count == 1 && delegate.removals.count == 0, 819 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 820 | 821 | XCTAssertTrue(delegate.editions[0] == 0) 822 | 823 | let endString = "First paragraph" 824 | 825 | XCTAssertTrue(textStorage.paragraphRanges.count == 1, 826 | "ParagraphTextStorage should now have 1 paragraph") 827 | 828 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 829 | 830 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange, 831 | "ParagraphTextStorage paragraph ranges should be correct") 832 | 833 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 834 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 835 | } 836 | 837 | func testParagraphTextStorage_EditParagraphAtBeginning() { 838 | let string = "First paragraph\nSecond paragraph\nThirdParagraph" 839 | let editString = "addition" 840 | 841 | textStorage.beginEditing() 842 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 843 | textStorage.endEditing() 844 | 845 | textStorage.beginEditing() 846 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: editString) 847 | textStorage.endEditing() 848 | 849 | XCTAssertTrue(delegate.insertions.count == 2 && delegate.editions.count == 2 && delegate.removals.count == 0, 850 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 851 | 852 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2) 853 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0) 854 | 855 | let endString = "additionFirst paragraph\nSecond paragraph\nThirdParagraph" 856 | 857 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 858 | "ParagraphTextStorage should now have 3 paragraphs") 859 | 860 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 861 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 862 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 863 | 864 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 865 | textStorage.paragraphRanges[1] == secondRange && 866 | textStorage.paragraphRanges[2] == thirdRange, 867 | "ParagraphTextStorage paragraph ranges should be correct") 868 | 869 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 870 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 871 | } 872 | 873 | func testParagraphTextStorage_EditParagraphInMiddle() { 874 | let string = "First paragraph\nSecond paragraph" 875 | let editString = "addition" 876 | 877 | textStorage.beginEditing() 878 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 879 | textStorage.endEditing() 880 | 881 | textStorage.beginEditing() 882 | textStorage.replaceCharacters(in: NSRange(location: 3, length: 5), with: editString) 883 | textStorage.endEditing() 884 | 885 | XCTAssertTrue(delegate.insertions.count == 1 && delegate.editions.count == 2 && delegate.removals.count == 0, 886 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 887 | 888 | XCTAssertTrue(delegate.insertions[0] == 1) 889 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0) 890 | 891 | let endString = "Firadditionragraph\nSecond paragraph" 892 | 893 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 894 | "ParagraphTextStorage should now have 2 paragraphs") 895 | 896 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 897 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 898 | 899 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 900 | textStorage.paragraphRanges[1] == secondRange, 901 | "ParagraphTextStorage paragraph ranges should be correct") 902 | 903 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 904 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 905 | } 906 | 907 | func testParagraphTextStorage_EditParagraphWithSameLengthEdit() { 908 | let string = "First paragraph\nSecond paragraph" 909 | let editString = "additions" 910 | 911 | textStorage.beginEditing() 912 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 913 | textStorage.endEditing() 914 | 915 | textStorage.beginEditing() 916 | textStorage.replaceCharacters(in: NSRange(location: 6, length: 9), with: editString) 917 | textStorage.endEditing() 918 | 919 | XCTAssertTrue(delegate.insertions.count == 1 && delegate.editions.count == 2 && delegate.removals.count == 0, 920 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 921 | 922 | XCTAssertTrue(delegate.insertions[0] == 1) 923 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0) 924 | 925 | let endString = "First additions\nSecond paragraph" 926 | 927 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 928 | "ParagraphTextStorage should now have 2 paragraphs") 929 | 930 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 931 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 932 | 933 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 934 | textStorage.paragraphRanges[1] == secondRange, 935 | "ParagraphTextStorage paragraph ranges should be correct") 936 | 937 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 938 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 939 | } 940 | 941 | func testParagraphTextStorage_EditMultipleParagraphWithSameLengthEdit() { 942 | let string = "First paragraph\nSecond paragraph\nThird paragraph" 943 | let editString = "First paragraph\nSecond paragraph\n" 944 | 945 | textStorage.beginEditing() 946 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 947 | textStorage.endEditing() 948 | 949 | textStorage.beginEditing() 950 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 33), with: editString) 951 | textStorage.endEditing() 952 | 953 | XCTAssertTrue(delegate.insertions.count == 2 && delegate.editions.count == 4 && delegate.removals.count == 0, 954 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 955 | 956 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2) 957 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0 && delegate.editions[2] == 1 && delegate.editions[3] == 2) 958 | 959 | let endString = "First paragraph\nSecond paragraph\nThird paragraph" 960 | 961 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 962 | "ParagraphTextStorage should now have 3 paragraphs") 963 | 964 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 965 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 966 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 967 | 968 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 969 | textStorage.paragraphRanges[1] == secondRange && 970 | textStorage.paragraphRanges[2] == thirdRange, 971 | "ParagraphTextStorage paragraph ranges should be correct") 972 | 973 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 974 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 975 | } 976 | 977 | 978 | func testParagraphTextStorage_EditParagraphAtEnd() { 979 | let string = "First paragraph\nSecond paragraph" 980 | let editString = "addition" 981 | 982 | textStorage.beginEditing() 983 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 984 | textStorage.endEditing() 985 | 986 | textStorage.beginEditing() 987 | textStorage.replaceCharacters(in: NSRange(location: string.length, length: 0), with: editString) 988 | textStorage.endEditing() 989 | 990 | XCTAssertTrue(delegate.insertions.count == 1 && delegate.editions.count == 2 && delegate.removals.count == 0, 991 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 992 | 993 | XCTAssertTrue(delegate.insertions[0] == 1) 994 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 1) 995 | 996 | let endString = "First paragraph\nSecond paragraphaddition" 997 | 998 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 999 | "ParagraphTextStorage should now have 2 paragraphs") 1000 | 1001 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1002 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1003 | 1004 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1005 | textStorage.paragraphRanges[1] == secondRange, 1006 | "ParagraphTextStorage paragraph ranges should be correct") 1007 | 1008 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1009 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1010 | } 1011 | 1012 | func testParagraphTextStorage_EditEmptyParagraphAtEnd() { 1013 | let string = "First paragraph\nSecond paragraph\n" 1014 | let editString = "a" 1015 | 1016 | textStorage.beginEditing() 1017 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1018 | textStorage.endEditing() 1019 | 1020 | textStorage.beginEditing() 1021 | textStorage.replaceCharacters(in: NSRange(location: string.length, length: 0), with: editString) 1022 | textStorage.endEditing() 1023 | 1024 | XCTAssertTrue(delegate.insertions.count == 2 && delegate.editions.count == 2 && delegate.removals.count == 0, 1025 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1026 | 1027 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2) 1028 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 2) 1029 | 1030 | let endString = "First paragraph\nSecond paragraph\na" 1031 | 1032 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 1033 | "ParagraphTextStorage should now have 3 paragraphs") 1034 | 1035 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1036 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1037 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 1038 | 1039 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1040 | textStorage.paragraphRanges[1] == secondRange && 1041 | textStorage.paragraphRanges[2] == thirdRange, 1042 | "ParagraphTextStorage paragraph ranges should be correct") 1043 | 1044 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1045 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1046 | } 1047 | 1048 | 1049 | // MARK: - Deletion Tests 1050 | 1051 | func testParagraphTextStorage_DeleteParagraphInMiddle() { 1052 | let string = "First paragraph\nSecond paragraph\nThird paragraph\nFourth paragraph" 1053 | 1054 | textStorage.beginEditing() 1055 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1056 | textStorage.endEditing() 1057 | 1058 | textStorage.beginEditing() 1059 | textStorage.replaceCharacters(in: NSRange(location: 15, length: 5), with: "") 1060 | textStorage.endEditing() 1061 | 1062 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 2 && delegate.removals.count == 1, 1063 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1064 | 1065 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3) 1066 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0) 1067 | XCTAssertTrue(delegate.removals[0] == 1) 1068 | 1069 | let endString = "First paragraphnd paragraph\nThird paragraph\nFourth paragraph" 1070 | 1071 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 1072 | "ParagraphTextStorage should now have 3 paragraphs") 1073 | 1074 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1075 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1076 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 1077 | 1078 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1079 | textStorage.paragraphRanges[1] == secondRange && 1080 | textStorage.paragraphRanges[2] == thirdRange, 1081 | "ParagraphTextStorage paragraph ranges should be correct") 1082 | 1083 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1084 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1085 | } 1086 | 1087 | func testParagraphTextStorage_DeleteParagraphAtEnd() { 1088 | let string = "First paragraph\nSecond paragraph\nThird paragraph" 1089 | 1090 | textStorage.beginEditing() 1091 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1092 | textStorage.endEditing() 1093 | 1094 | textStorage.beginEditing() 1095 | textStorage.replaceCharacters(in: NSRange(location: 32, length: 5), with: "") 1096 | textStorage.endEditing() 1097 | 1098 | XCTAssertTrue(delegate.insertions.count == 2 && delegate.editions.count == 2 && delegate.removals.count == 1, 1099 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1100 | 1101 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2) 1102 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 1) 1103 | XCTAssertTrue(delegate.removals[0] == 2) 1104 | 1105 | let endString = "First paragraph\nSecond paragraphd paragraph" 1106 | 1107 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 1108 | "ParagraphTextStorage should now have 2 paragraphs") 1109 | 1110 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1111 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1112 | 1113 | 1114 | 1115 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1116 | textStorage.paragraphRanges[1] == secondRange, 1117 | "ParagraphTextStorage paragraph ranges should be correct") 1118 | 1119 | let storageSubstring1 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string 1120 | let testSubstring1 = String(endString[Range(firstRange, in: endString)!]) 1121 | let storageSubstring2 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string 1122 | let testSubstring2 = String(endString[Range(secondRange, in: endString)!]) 1123 | XCTAssertTrue( storageSubstring1 == testSubstring1 && 1124 | storageSubstring2 == testSubstring2, 1125 | "ParagraphTextStorage strings should match the test trings") 1126 | 1127 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1128 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1129 | } 1130 | 1131 | func testParagraphTextStorage_DeleteEmptyParagraphAtEnd() { 1132 | let string = "First paragraph\nSecond paragraph\nThird paragraph\n" 1133 | 1134 | textStorage.beginEditing() 1135 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1136 | textStorage.endEditing() 1137 | 1138 | textStorage.beginEditing() 1139 | textStorage.replaceCharacters(in: NSRange(location: string.length - 1, length: 1), with: "") 1140 | textStorage.endEditing() 1141 | 1142 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 2 && delegate.removals.count == 1, 1143 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1144 | 1145 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3) 1146 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 2) 1147 | XCTAssertTrue(delegate.removals[0] == 3) 1148 | 1149 | let endString = "First paragraph\nSecond paragraph\nThird paragraph" 1150 | 1151 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 1152 | "ParagraphTextStorage should now have 3 paragraphs") 1153 | 1154 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1155 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1156 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 1157 | 1158 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1159 | textStorage.paragraphRanges[1] == secondRange && 1160 | textStorage.paragraphRanges[2] == thirdRange, 1161 | "ParagraphTextStorage paragraph ranges should be correct") 1162 | 1163 | let storageSubstring1 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string 1164 | let testSubstring1 = String(endString[Range(firstRange, in: endString)!]) 1165 | let storageSubstring2 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string 1166 | let testSubstring2 = String(endString[Range(secondRange, in: endString)!]) 1167 | let storageSubstring3 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[2]).string 1168 | let testSubstring3 = String(endString[Range(thirdRange, in: endString)!]) 1169 | XCTAssertTrue( storageSubstring1 == testSubstring1 && 1170 | storageSubstring2 == testSubstring2 && 1171 | storageSubstring3 == testSubstring3, 1172 | "ParagraphTextStorage strings should match the test trings") 1173 | 1174 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1175 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1176 | } 1177 | 1178 | func testParagraphTextStorage_DeleteWholeParagraphAtBeginning() { 1179 | let string = "First paragraph\nSecond paragraph\nThird paragraph" 1180 | 1181 | textStorage.beginEditing() 1182 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1183 | textStorage.endEditing() 1184 | 1185 | textStorage.beginEditing() 1186 | textStorage.replaceCharacters(in: NSRange(location: 0, length: string.paragraphs[0].length), with: "") 1187 | textStorage.endEditing() 1188 | 1189 | XCTAssertTrue(delegate.insertions.count == 2 && delegate.editions.count == 1 && delegate.removals.count == 1, 1190 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1191 | 1192 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2) 1193 | XCTAssertTrue(delegate.editions[0] == 0) 1194 | XCTAssertTrue(delegate.removals[0] == 0) 1195 | 1196 | let endString = "Second paragraph\nThird paragraph" 1197 | 1198 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 1199 | "ParagraphTextStorage should now have 2 paragraphs") 1200 | 1201 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1202 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1203 | 1204 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1205 | textStorage.paragraphRanges[1] == secondRange, 1206 | "ParagraphTextStorage paragraph ranges should be correct") 1207 | 1208 | let storageSubstring1 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string 1209 | let testSubstring1 = String(endString[Range(firstRange, in: endString)!]) 1210 | let storageSubstring2 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string 1211 | let testSubstring2 = String(endString[Range(secondRange, in: endString)!]) 1212 | XCTAssertTrue( storageSubstring1 == testSubstring1 && 1213 | storageSubstring2 == testSubstring2, 1214 | "ParagraphTextStorage strings should match the test trings") 1215 | 1216 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1217 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1218 | } 1219 | 1220 | func testParagraphTextStorage_DeleteWholeParagraphInMiddle() { 1221 | let string = "First paragraph\nSecond paragraph\nThird paragraph\nFourth paragraph" 1222 | 1223 | textStorage.beginEditing() 1224 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1225 | textStorage.endEditing() 1226 | 1227 | textStorage.beginEditing() 1228 | textStorage.replaceCharacters(in: NSRange(location: 16, length: string.paragraphs[1].length), with: "") 1229 | textStorage.endEditing() 1230 | 1231 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 1 && delegate.removals.count == 1, 1232 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1233 | 1234 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3) 1235 | XCTAssertTrue(delegate.editions[0] == 0) 1236 | XCTAssertTrue(delegate.removals[0] == 1) 1237 | 1238 | let endString = "First paragraph\nThird paragraph\nFourth paragraph" 1239 | 1240 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 1241 | "ParagraphTextStorage should now have 3 paragraphs") 1242 | 1243 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1244 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1245 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 1246 | 1247 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1248 | textStorage.paragraphRanges[1] == secondRange && 1249 | textStorage.paragraphRanges[2] == thirdRange, 1250 | "ParagraphTextStorage paragraph ranges should be correct") 1251 | 1252 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1253 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1254 | } 1255 | 1256 | func testParagraphTextStorage_DeleteWholeParagraphAtEnd() { 1257 | let string = "First paragraph\nSecond paragraph\nThird paragraph" 1258 | 1259 | textStorage.beginEditing() 1260 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1261 | textStorage.endEditing() 1262 | 1263 | textStorage.beginEditing() 1264 | textStorage.replaceCharacters(in: NSRange(location: 32, length: string.paragraphs[2].length + 1), with: "") 1265 | textStorage.endEditing() 1266 | 1267 | XCTAssertTrue(delegate.insertions.count == 2 && delegate.editions.count == 2 && delegate.removals.count == 1, 1268 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1269 | 1270 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2) 1271 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 1) 1272 | XCTAssertTrue(delegate.removals[0] == 2) 1273 | 1274 | let endString = "First paragraph\nSecond paragraph" 1275 | 1276 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 1277 | "ParagraphTextStorage should now have 2 paragraphs") 1278 | 1279 | print(endString.paragraphs[1]) 1280 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1281 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1282 | 1283 | let storageSubstring1 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string 1284 | let testSubstring1 = String(endString[Range(firstRange, in: endString)!]) 1285 | let storageSubstring2 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string 1286 | let testSubstring2 = String(endString[Range(secondRange, in: endString)!]) 1287 | XCTAssertTrue( storageSubstring1 == testSubstring1 && 1288 | storageSubstring2 == testSubstring2, 1289 | "ParagraphTextStorage strings should match the test trings") 1290 | 1291 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1292 | textStorage.paragraphRanges[1] == secondRange, 1293 | "ParagraphTextStorage paragraph ranges should be correct") 1294 | 1295 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1296 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1297 | } 1298 | 1299 | func testParagraphTextStorage_DeleteAllParagraphs() { 1300 | let string = "First paragraph\nSecond paragraph\nThird paragraph" 1301 | 1302 | textStorage.beginEditing() 1303 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1304 | textStorage.endEditing() 1305 | 1306 | textStorage.beginEditing() 1307 | textStorage.replaceCharacters(in: NSRange(location: 0, length: string.length), with: "") 1308 | textStorage.endEditing() 1309 | 1310 | XCTAssertTrue(delegate.insertions.count == 2 && delegate.editions.count == 2 && delegate.removals.count == 2, 1311 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1312 | 1313 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2) 1314 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0) 1315 | XCTAssertTrue(delegate.removals[0] == 2) 1316 | XCTAssertTrue(delegate.removals[1] == 1) 1317 | 1318 | XCTAssertTrue(textStorage.paragraphRanges.count == 1, 1319 | "ParagraphTextStorage should now have 1 paragraphs") 1320 | XCTAssertTrue(delegate.paragraphs[0].isEmpty) 1321 | } 1322 | 1323 | 1324 | // MARK: - Mixed Tests 1325 | 1326 | func testParagraphTextStorage_DeleteWholeParagraphAtBeginningAndEditNextOneWithSameFinalRanges() { 1327 | let string = "First paragraph\nSecond💋 paragraph\nThird paragraph\nFourth paragraph" 1328 | 1329 | textStorage.beginEditing() 1330 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1331 | textStorage.endEditing() 1332 | 1333 | textStorage.beginEditing() 1334 | textStorage.replaceCharacters(in: NSRange(location: 0, length: string.paragraphs[0].length + 3), with: "") 1335 | textStorage.endEditing() 1336 | 1337 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 2 && delegate.removals.count == 1, 1338 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1339 | 1340 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3) 1341 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0) 1342 | XCTAssertTrue(delegate.removals[0] == 0) 1343 | 1344 | let endString = "ond💋 paragraph\nThird paragraph\nFourth paragraph" 1345 | 1346 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 1347 | "ParagraphTextStorage should now have 3 paragraphs") 1348 | 1349 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1350 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1351 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 1352 | 1353 | let storageSubstring1 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string 1354 | let testSubstring1 = String(endString[Range(firstRange, in: endString)!]) 1355 | let storageSubstring2 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string 1356 | let testSubstring2 = String(endString[Range(secondRange, in: endString)!]) 1357 | let storageSubstring3 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[2]).string 1358 | let testSubstring3 = String(endString[Range(thirdRange, in: endString)!]) 1359 | XCTAssertTrue( storageSubstring1 == testSubstring1 && 1360 | storageSubstring2 == testSubstring2 && 1361 | storageSubstring3 == testSubstring3, 1362 | "ParagraphTextStorage strings should match the test trings") 1363 | 1364 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1365 | textStorage.paragraphRanges[1] == secondRange && 1366 | textStorage.paragraphRanges[2] == thirdRange, 1367 | "ParagraphTextStorage paragraph ranges should be correct") 1368 | 1369 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1370 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1371 | } 1372 | 1373 | func testParagraphTextStorage_DeleteTwoParagraphAtBeginningAndEditNextOneWithSameFinalRanges() { 1374 | let string = "First paragraph\nSecond💋 paragraph\nSecond💋 paragraph\nFourth paragraph" 1375 | 1376 | textStorage.beginEditing() 1377 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1378 | textStorage.endEditing() 1379 | 1380 | textStorage.beginEditing() 1381 | textStorage.replaceCharacters(in: NSRange(location: 0, length: string.paragraphs[0].length + string.paragraphs[1].length + 3), with: "") 1382 | textStorage.endEditing() 1383 | 1384 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 2 && delegate.removals.count == 2, 1385 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1386 | 1387 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3) 1388 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0) 1389 | XCTAssertTrue(delegate.removals[0] == 1 && delegate.removals[1] == 0) 1390 | 1391 | let endString = "ond💋 paragraph\nFourth paragraph" 1392 | 1393 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 1394 | "ParagraphTextStorage should now have 2 paragraphs") 1395 | 1396 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1397 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1398 | 1399 | let storageSubstring1 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string 1400 | let testSubstring1 = String(endString[Range(firstRange, in: endString)!]) 1401 | let storageSubstring2 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string 1402 | let testSubstring2 = String(endString[Range(secondRange, in: endString)!]) 1403 | XCTAssertTrue( storageSubstring1 == testSubstring1 && 1404 | storageSubstring2 == testSubstring2, 1405 | "ParagraphTextStorage strings should match the test trings") 1406 | 1407 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1408 | textStorage.paragraphRanges[1] == secondRange, 1409 | "ParagraphTextStorage paragraph ranges should be correct") 1410 | 1411 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1412 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1413 | } 1414 | 1415 | func testParagraphTextStorage_DeleteWholeParagraphAtBeginningAndEditNextOne() { 1416 | let string = "First paragraph\nSecond💋 paragraph!\nThird paragraph\nFourth paragraph" 1417 | 1418 | textStorage.beginEditing() 1419 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1420 | textStorage.endEditing() 1421 | 1422 | textStorage.beginEditing() 1423 | textStorage.replaceCharacters(in: NSRange(location: 0, length: string.paragraphs[0].length + 3), with: "") 1424 | textStorage.endEditing() 1425 | 1426 | XCTAssertTrue(delegate.insertions.count == 3 && delegate.editions.count == 2 && delegate.removals.count == 1, 1427 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1428 | 1429 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3) 1430 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 1) 1431 | XCTAssertTrue(delegate.removals[0] == 0) 1432 | 1433 | let endString = "ond💋 paragraph!\nThird paragraph\nFourth paragraph" 1434 | 1435 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 1436 | "ParagraphTextStorage should now have 3 paragraphs") 1437 | 1438 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1439 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1440 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 1441 | 1442 | let storageSubstring1 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string 1443 | let testSubstring1 = String(endString[Range(firstRange, in: endString)!]) 1444 | let storageSubstring2 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string 1445 | let testSubstring2 = String(endString[Range(secondRange, in: endString)!]) 1446 | let storageSubstring3 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[2]).string 1447 | let testSubstring3 = String(endString[Range(thirdRange, in: endString)!]) 1448 | XCTAssertTrue( storageSubstring1 == testSubstring1 && 1449 | storageSubstring2 == testSubstring2 && 1450 | storageSubstring3 == testSubstring3, 1451 | "ParagraphTextStorage strings should match the test trings") 1452 | 1453 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1454 | textStorage.paragraphRanges[1] == secondRange && 1455 | textStorage.paragraphRanges[2] == thirdRange, 1456 | "ParagraphTextStorage paragraph ranges should be correct") 1457 | 1458 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1459 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1460 | } 1461 | 1462 | func testParagraphTextStorage_DeleteWholeTwoParagraphsAtBeginningAndEditNextOne() { 1463 | let string = "First paragraph\nSeco💋nd paragraph\nThird paragraph" 1464 | 1465 | textStorage.beginEditing() 1466 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1467 | textStorage.endEditing() 1468 | 1469 | textStorage.beginEditing() 1470 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 35 + 3), with: "") 1471 | textStorage.endEditing() 1472 | 1473 | XCTAssertTrue(delegate.insertions.count == 2 && delegate.editions.count == 2 && delegate.removals.count == 2, 1474 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1475 | 1476 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2) 1477 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 2) 1478 | XCTAssertTrue(delegate.removals[0] == 1 && delegate.removals[1] == 0) 1479 | 1480 | let endString = "rd paragraph" 1481 | 1482 | XCTAssertTrue(textStorage.paragraphRanges.count == 1, 1483 | "ParagraphTextStorage should now have 1 paragraph") 1484 | 1485 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1486 | 1487 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange, 1488 | "ParagraphTextStorage paragraph ranges should be correct") 1489 | 1490 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1491 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1492 | } 1493 | 1494 | func testParagraphTextStorage_DeleteWholeTwoParagraphsAtBeginningEditingTheNextOneAndInsertNewParagraph() { 1495 | let string = "First paragraph\nSecond pa💋ragraph\nThird paragraph\nFourth paragraph" 1496 | 1497 | textStorage.beginEditing() 1498 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1499 | textStorage.endEditing() 1500 | 1501 | textStorage.beginEditing() 1502 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 35 + 3), with: "new paragraph\n") 1503 | textStorage.endEditing() 1504 | 1505 | XCTAssertTrue(delegate.insertions.count == 4 && delegate.editions.count == 2 && delegate.removals.count == 2, 1506 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1507 | 1508 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3 && delegate.insertions[3] == 1) 1509 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 2) 1510 | XCTAssertTrue(delegate.removals[0] == 1 && delegate.removals[1] == 0) 1511 | 1512 | let endString = "new paragraph\nrd paragraph\nFourth paragraph" 1513 | 1514 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 1515 | "ParagraphTextStorage should now have 3 paragraphs") 1516 | 1517 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1518 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1519 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 1520 | 1521 | let storageSubstring1 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string 1522 | let testSubstring1 = String(endString[Range(firstRange, in: endString)!]) 1523 | let storageSubstring2 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string 1524 | let testSubstring2 = String(endString[Range(secondRange, in: endString)!]) 1525 | let storageSubstring3 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[2]).string 1526 | let testSubstring3 = String(endString[Range(thirdRange, in: endString)!]) 1527 | 1528 | XCTAssertTrue( storageSubstring1 == testSubstring1 && 1529 | storageSubstring2 == testSubstring2 && 1530 | storageSubstring3 == testSubstring3, 1531 | "ParagraphTextStorage strings should match the test trings") 1532 | 1533 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1534 | textStorage.paragraphRanges[1] == secondRange && 1535 | textStorage.paragraphRanges[2] == thirdRange, 1536 | "ParagraphTextStorage paragraph ranges should be correct") 1537 | 1538 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1539 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1540 | } 1541 | 1542 | func testParagraphTextStorage_DeleteTwoParagraphsInMiddleEditingTheNextOneAndInsertNewParagraph() { 1543 | let string = "First paragraph\nSecond parag💋raph\nThird paragraph\nFourth paragraph" 1544 | 1545 | textStorage.beginEditing() 1546 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1547 | textStorage.endEditing() 1548 | 1549 | textStorage.beginEditing() 1550 | textStorage.replaceCharacters(in: NSRange(location: 15, length: 18 + 3), with: "new paragraph\n") 1551 | textStorage.endEditing() 1552 | 1553 | XCTAssertTrue(delegate.insertions.count == 4 && delegate.editions.count == 2 && delegate.removals.count == 2, 1554 | "ParagraphTextStorage paragraph delegate should be notified of exact changes") 1555 | 1556 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3 && delegate.insertions[3] == 1) 1557 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 0) 1558 | XCTAssertTrue(delegate.removals[0] == 2 && delegate.removals[1] == 1) 1559 | 1560 | let endString = "First paragraphnew paragraph\nhird paragraph\nFourth paragraph" 1561 | 1562 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 1563 | "ParagraphTextStorage should now have 3 paragraphs") 1564 | 1565 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1566 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1567 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 1568 | 1569 | let storageSubstring1 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string 1570 | let testSubstring1 = String(endString[Range(firstRange, in: endString)!]) 1571 | let storageSubstring2 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string 1572 | let testSubstring2 = String(endString[Range(secondRange, in: endString)!]) 1573 | let storageSubstring3 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[2]).string 1574 | let testSubstring3 = String(endString[Range(thirdRange, in: endString)!]) 1575 | XCTAssertTrue( storageSubstring1 == testSubstring1 && 1576 | storageSubstring2 == testSubstring2 && 1577 | storageSubstring3 == testSubstring3, 1578 | "ParagraphTextStorage strings should match the test trings") 1579 | 1580 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1581 | textStorage.paragraphRanges[1] == secondRange && 1582 | textStorage.paragraphRanges[2] == thirdRange, 1583 | "ParagraphTextStorage paragraph ranges should be correct") 1584 | 1585 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1586 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1587 | } 1588 | 1589 | func testParagraphTextStorage_DeleteOneParagraphInMiddleEditingTheNextOneAndInsertNewParagraph() { 1590 | let string = "First paragraph\nSecond parag💋raph\nThird paragraph\nFourth paragraph" 1591 | let location = (string.paragraphs[0].length + string.paragraphs[1].length) - 1 1592 | 1593 | textStorage.beginEditing() 1594 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1595 | textStorage.endEditing() 1596 | 1597 | textStorage.beginEditing() 1598 | textStorage.replaceCharacters(in: NSRange(location: location, length: 3), with: "new paragraph\n") 1599 | textStorage.endEditing() 1600 | 1601 | XCTAssertTrue(delegate.insertions[0] == 1 && delegate.insertions[1] == 2 && delegate.insertions[2] == 3 && delegate.insertions[3] == 2) 1602 | XCTAssertTrue(delegate.editions[0] == 0 && delegate.editions[1] == 1) 1603 | XCTAssertTrue(delegate.removals[0] == 2) 1604 | 1605 | let endString = "First paragraph\nSecond parag💋raphnew paragraph\nird paragraph\nFourth paragraph" 1606 | 1607 | XCTAssertTrue(textStorage.paragraphRanges.count == 4, 1608 | "ParagraphTextStorage should now have 4 paragraphs") 1609 | 1610 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1611 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1612 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 1613 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 1614 | 1615 | let storageSubstring1 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[0]).string 1616 | let testSubstring1 = String(endString[Range(firstRange, in: endString)!]) 1617 | let storageSubstring2 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[1]).string 1618 | let testSubstring2 = String(endString[Range(secondRange, in: endString)!]) 1619 | let storageSubstring3 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[2]).string 1620 | let testSubstring3 = String(endString[Range(thirdRange, in: endString)!]) 1621 | let storageSubstring4 = textStorage.attributedSubstring(from: textStorage.paragraphRanges[3]).string 1622 | let testSubstring4 = String(endString[Range(fourthRange, in: endString)!]) 1623 | XCTAssertTrue( storageSubstring1 == testSubstring1 && 1624 | storageSubstring2 == testSubstring2 && 1625 | storageSubstring3 == testSubstring3 && 1626 | storageSubstring4 == testSubstring4, 1627 | "ParagraphTextStorage strings should match the test trings") 1628 | 1629 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1630 | textStorage.paragraphRanges[1] == secondRange && 1631 | textStorage.paragraphRanges[2] == thirdRange && 1632 | textStorage.paragraphRanges[3] == fourthRange, 1633 | "ParagraphTextStorage paragraph ranges should be correct") 1634 | 1635 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1636 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1637 | } 1638 | 1639 | func testParagraphTextStorage_IncrementallyAddAndEditParagraphAtEndAndPeriodicallyInsertNewParagraphInMiddle() { 1640 | var lastRange = NSRange(location: 0, length: textStorage.length) 1641 | var ranges = [lastRange] 1642 | var endString = "" 1643 | var additionallyAdded = 0 1644 | 1645 | for i in 0 ..< 50 { 1646 | // every 10 iterations, remove and add the range 1647 | if i % 10 == 0, i > 0 { 1648 | let index = i - 5 1649 | let range = textStorage.paragraphRanges[index] 1650 | let addRange = NSRange(location: range.location, length: 0) 1651 | textStorage.beginEditing() 1652 | textStorage.replaceCharacters(in: addRange, with: "\n") 1653 | textStorage.endEditing() 1654 | endString.insert("\n", at: Range(addRange, in: endString)!.upperBound) 1655 | 1656 | let string = String(describing: i) 1657 | let editRange = NSRange(location: addRange.max, length: 0) 1658 | textStorage.beginEditing() 1659 | textStorage.replaceCharacters(in: editRange, with: string) 1660 | textStorage.endEditing() 1661 | endString.insert(contentsOf: string, at: Range(editRange, in: endString)!.upperBound) 1662 | let changedRange = NSRange(location: addRange.location, length: 1 + string.count) 1663 | ranges.insert(changedRange, at: index) 1664 | 1665 | for idx in index + 1 ..< ranges.count { 1666 | ranges[idx].location += 1 + string.count 1667 | } 1668 | lastRange = NSRange(location: textStorage.length, length: 0) 1669 | additionallyAdded += 1 1670 | } 1671 | 1672 | let string = String(describing: i) 1673 | textStorage.beginEditing() 1674 | textStorage.replaceCharacters(in: lastRange, with: string) 1675 | textStorage.endEditing() 1676 | endString += string 1677 | lastRange.length += string.count 1678 | 1679 | textStorage.beginEditing() 1680 | textStorage.replaceCharacters(in: NSRange(location: lastRange.max, length: 0), with: "\n") 1681 | textStorage.endEditing() 1682 | lastRange.length += 1 1683 | 1684 | endString += "\n" 1685 | ranges[i + additionallyAdded] = lastRange 1686 | 1687 | lastRange = NSRange(location: textStorage.length, length: 0) 1688 | ranges.append(lastRange) 1689 | } 1690 | 1691 | XCTAssertTrue(textStorage.paragraphRanges.count == ranges.count, 1692 | "ParagraphTextStorage should now have \(ranges.count) paragraph descriptors") 1693 | 1694 | let storageRanges = textStorage.paragraphRanges 1695 | 1696 | XCTAssertTrue(storageRanges == ranges, 1697 | "ParagraphTextStorage paragraph ranges should be correct") 1698 | 1699 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1700 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1701 | } 1702 | 1703 | func testParagraphTextStorage_IncrementallyAddAndEditParagraphAtEndAndThenDeleteBunchOfThem() { 1704 | var lastRange = NSRange(location: 0, length: textStorage.length) 1705 | 1706 | for i in 0 ..< 50 { 1707 | let string = String(describing: i) 1708 | textStorage.beginEditing() 1709 | textStorage.replaceCharacters(in: lastRange, with: string) 1710 | textStorage.endEditing() 1711 | lastRange.length += string.count 1712 | 1713 | textStorage.beginEditing() 1714 | textStorage.replaceCharacters(in: NSRange(location: lastRange.max, length: 0), with: "\n") 1715 | textStorage.endEditing() 1716 | lastRange.length += 1 1717 | 1718 | lastRange = NSRange(location: textStorage.length, length: 0) 1719 | } 1720 | 1721 | let range = NSRange(location: 5, length: textStorage.length - 10) 1722 | textStorage.beginEditing() 1723 | textStorage.replaceCharacters(in: range, with: "") 1724 | textStorage.endEditing() 1725 | 1726 | let endStrig = "0\n1\n28\n49\n" 1727 | let endParagraphs = endStrig.paragraphs 1728 | var paragraphs: [NSRange] = [] 1729 | var theRange = NSRange.zero 1730 | for paragraph in endParagraphs { 1731 | theRange = NSRange(location: theRange.max, length: paragraph.count) 1732 | paragraphs.append(theRange) 1733 | } 1734 | 1735 | XCTAssertTrue(textStorage.paragraphRanges.count == paragraphs.count, 1736 | "ParagraphTextStorage should now have \(paragraphs.count) paragraph descriptors") 1737 | 1738 | let storageRanges = textStorage.paragraphRanges 1739 | 1740 | XCTAssertTrue(storageRanges == paragraphs, 1741 | "ParagraphTextStorage paragraph ranges should be correct") 1742 | 1743 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1744 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1745 | } 1746 | 1747 | func testParagraphTextStorage_ReplaceAllParagraphsWithTwoNewParagraphs() { 1748 | let string = "First paragraph\nSecond 💋paragraph\nThird paragraph" 1749 | 1750 | textStorage.beginEditing() 1751 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1752 | textStorage.endEditing() 1753 | 1754 | textStorage.beginEditing() 1755 | textStorage.replaceCharacters(in: NSRange(location: 0, length: string.length), with: "new paragraph\nanotherParagraph") 1756 | textStorage.endEditing() 1757 | 1758 | let endString = "new paragraph\nanotherParagraph" 1759 | 1760 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 1761 | "ParagraphTextStorage should now have 2 paragraphs") 1762 | 1763 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1764 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1765 | 1766 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1767 | textStorage.paragraphRanges[1] == secondRange, 1768 | "ParagraphTextStorage paragraph ranges should be correct") 1769 | 1770 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1771 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1772 | } 1773 | 1774 | func testParagraphTextStorage_InsertBlankParagraphInMiddleAndInsertAnotherOne() { 1775 | let string = "First paragraph\nSecond paragraph\nThird paragraph\nFourthOne" 1776 | 1777 | textStorage.beginEditing() 1778 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1779 | textStorage.endEditing() 1780 | 1781 | textStorage.beginEditing() 1782 | textStorage.replaceCharacters(in: NSRange(location: 16, length: 0), with: "\n") 1783 | textStorage.endEditing() 1784 | 1785 | textStorage.beginEditing() 1786 | textStorage.replaceCharacters(in: NSRange(location: 34, length: 0), with: "\n") 1787 | textStorage.endEditing() 1788 | 1789 | let endString = "First paragraph\n\nSecond paragraph\n\nThird paragraph\nFourthOne" 1790 | let endParagraphs = endString.paragraphs 1791 | 1792 | XCTAssertTrue(textStorage.paragraphRanges.count == 6, 1793 | "ParagraphTextStorage should now have 6 paragraph descriptors") 1794 | 1795 | var paragraphs: [NSRange] = [] 1796 | var lastRange = NSRange.zero 1797 | for paragraph in endParagraphs { 1798 | lastRange = NSRange(location: lastRange.max, length: paragraph.count) 1799 | paragraphs.append(lastRange) 1800 | } 1801 | 1802 | let storageRanges = textStorage.paragraphRanges 1803 | XCTAssertTrue(storageRanges == paragraphs, 1804 | "ParagraphTextStorage paragraph ranges should be correct") 1805 | 1806 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1807 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1808 | } 1809 | 1810 | func testParagraphTextStorage_DeleteParagraphInMiddleAndEditingTheNextOne() { 1811 | let string = "First paragraph\nSecond paragraph💋\nThirdParagraph" 1812 | let editString = "\naddition" 1813 | 1814 | textStorage.beginEditing() 1815 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1816 | textStorage.endEditing() 1817 | 1818 | textStorage.beginEditing() 1819 | textStorage.replaceCharacters(in: NSRange(location: 15, length: 1), with: editString) 1820 | textStorage.endEditing() 1821 | 1822 | let endString = "First paragraph\nadditionSecond paragraph💋\nThirdParagraph" 1823 | 1824 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 1825 | "ParagraphTextStorage should now have 3 paragraphs") 1826 | 1827 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1828 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1829 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 1830 | 1831 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1832 | textStorage.paragraphRanges[1] == secondRange && 1833 | textStorage.paragraphRanges[2] == thirdRange, 1834 | "ParagraphTextStorage paragraph ranges should be correct") 1835 | 1836 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1837 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1838 | } 1839 | 1840 | func testParagraphTextStorage_InsertBetweenParagraphsBlankParagraphEditingTheNextOne() { 1841 | let string = "First paragraph\nSe🤙cond paragraph\nThirdParagraph" 1842 | let editString = "\naddition" 1843 | 1844 | textStorage.beginEditing() 1845 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1846 | textStorage.endEditing() 1847 | 1848 | textStorage.beginEditing() 1849 | textStorage.replaceCharacters(in: NSRange(location: 16, length: 1), with: editString) 1850 | textStorage.endEditing() 1851 | 1852 | let endString = "First paragraph\n\nadditione🤙cond paragraph\nThirdParagraph" 1853 | 1854 | XCTAssertTrue(textStorage.paragraphRanges.count == 4, 1855 | "ParagraphTextStorage should now have 4 paragraphs") 1856 | 1857 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1858 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1859 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 1860 | let fourthRange = NSRange(location: NSMaxRange(thirdRange), length: endString.paragraphs[3].length) 1861 | 1862 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1863 | textStorage.paragraphRanges[1] == secondRange && 1864 | textStorage.paragraphRanges[2] == thirdRange && 1865 | textStorage.paragraphRanges[3] == fourthRange, 1866 | "ParagraphTextStorage paragraph ranges should be correct") 1867 | 1868 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1869 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1870 | } 1871 | 1872 | func testParagraphTextStorage_IncrementalEditingAndInsertingParagraph() { 1873 | let string = "1" 1874 | 1875 | textStorage.beginEditing() 1876 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1877 | textStorage.endEditing() 1878 | textStorage.beginEditing() 1879 | textStorage.replaceCharacters(in: NSRange(location: 1, length: 0), with: "\n") 1880 | textStorage.endEditing() 1881 | textStorage.beginEditing() 1882 | textStorage.replaceCharacters(in: NSRange(location: 2, length: 0), with: "2") 1883 | textStorage.endEditing() 1884 | let endString = "1\n2" 1885 | 1886 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 1887 | "ParagraphTextStorage should now have 2 paragraphs") 1888 | 1889 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1890 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1891 | 1892 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1893 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1894 | 1895 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1896 | textStorage.paragraphRanges[1] == secondRange, 1897 | "ParagraphTextStorage paragraph ranges should be correct") 1898 | } 1899 | 1900 | func testParagraphTextStorage_IncrementalEditingAndMakeFirstParagraphEmpty() { 1901 | let string = "1\n2" 1902 | 1903 | textStorage.beginEditing() 1904 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1905 | textStorage.endEditing() 1906 | textStorage.beginEditing() 1907 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 1), with: "") 1908 | textStorage.endEditing() 1909 | let endString = "\n2" 1910 | 1911 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 1912 | "ParagraphTextStorage should now have 2 paragraphs") 1913 | 1914 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1915 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1916 | 1917 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1918 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1919 | 1920 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1921 | textStorage.paragraphRanges[1] == secondRange, 1922 | "ParagraphTextStorage paragraph ranges should be correct") 1923 | } 1924 | 1925 | func testParagraphTextStorage_IncrementalEditingAndMakeMiddleParagraphEmpty() { 1926 | let string = "1\n2\n3" 1927 | 1928 | textStorage.beginEditing() 1929 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1930 | textStorage.endEditing() 1931 | textStorage.beginEditing() 1932 | textStorage.replaceCharacters(in: NSRange(location: 2, length: 1), with: "") 1933 | textStorage.endEditing() 1934 | let endString = "1\n\n3" 1935 | 1936 | XCTAssertTrue(textStorage.paragraphRanges.count == 3, 1937 | "ParagraphTextStorage should now have 3 paragraphs") 1938 | 1939 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1940 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1941 | let thirdRange = NSRange(location: NSMaxRange(secondRange), length: endString.paragraphs[2].length) 1942 | 1943 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1944 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1945 | 1946 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1947 | textStorage.paragraphRanges[1] == secondRange && 1948 | textStorage.paragraphRanges[2] == thirdRange, 1949 | "ParagraphTextStorage paragraph ranges should be correct") 1950 | } 1951 | 1952 | func testParagraphTextStorage_IncrementalEditingAndMakeLastParagraphEmpty() { 1953 | let string = "1\n2" 1954 | 1955 | textStorage.beginEditing() 1956 | textStorage.replaceCharacters(in: NSRange(location: 0, length: 0), with: string) 1957 | textStorage.endEditing() 1958 | textStorage.beginEditing() 1959 | textStorage.replaceCharacters(in: NSRange(location: 2, length: 1), with: "") 1960 | textStorage.endEditing() 1961 | let endString = "1\n" 1962 | 1963 | XCTAssertTrue(textStorage.paragraphRanges.count == 2, 1964 | "ParagraphTextStorage should now have 2 paragraphs") 1965 | 1966 | let firstRange = NSRange(location: 0, length: endString.paragraphs[0].length) 1967 | let secondRange = NSRange(location: NSMaxRange(firstRange), length: endString.paragraphs[1].length) 1968 | 1969 | XCTAssertEqual(textStorage.paragraphRanges, delegate.ranges, 1970 | "ParagraphTextStorage paragraph ranges should match the delegate ranges") 1971 | 1972 | XCTAssertTrue(textStorage.paragraphRanges[0] == firstRange && 1973 | textStorage.paragraphRanges[1] == secondRange, 1974 | "ParagraphTextStorage paragraph ranges should be correct") 1975 | } 1976 | 1977 | 1978 | static var allTests = [ 1979 | ("test for initialization", testParagraphTextStorage_Initialization), 1980 | 1981 | // insertion tests 1982 | ("test for inserting a new line character into the middle of text heap", testParagraphTextStorage_InsertNewLineInMiddleHeap), 1983 | ("test for inserting three new line characters into the middle of text heap", testParagraphTextStorage_InsertThreeNewLinesInMiddleHeap), 1984 | ("test for inserting following paragraphs", testParagraphTextStorage_InsertFirstParagraphs), 1985 | ("test for inserting an empty paragraph at the beginning", testParagraphTextStorage_InsertEmptyAtBeginning), 1986 | ("test for inserting an non-empty paragraph at the beginning", testParagraphTextStorage_InsertNonemptyAtBeginning), 1987 | ("test for inserting an empty paragraph in the middle", testParagraphTextStorage_InsertEmptyInMiddle), 1988 | ("test for inserting an non-empty paragraph in the middle", testParagraphTextStorage_InsertNonemptyInMiddle), 1989 | ("test for inserting an empty paragraph between two paragraphs", testParagraphTextStorage_InsertEmptyBetweenParagraphs), 1990 | ("test for inserting an non-empty paragraph between two paragraphs", testParagraphTextStorage_InsertNonemptyBetweenParagraphs), 1991 | ("test for inserting an empty paragraph between two other paragraphs", testParagraphTextStorage_InsertEmptyBetweenParagraphs2), 1992 | ("test for inserting an empty paragraph at the end", testParagraphTextStorage_InsertEmptyAtEnd), 1993 | ("test for inserting an non-empty paragraph at the end", testParagraphTextStorage_InsertNonemptyAtEnd), 1994 | 1995 | // editing tests 1996 | ("test for editing the first paragraph when there's no other paragraphs", testParagraphTextStorage_EditFirstParagraph), 1997 | ("test for editing the first paragraph among other paragraphs", testParagraphTextStorage_EditParagraphAtBeginning), 1998 | ("test for editing the second paragraph among other paragraphs", testParagraphTextStorage_EditParagraphInMiddle), 1999 | ("test for editing the last paragraph among other paragraphs", testParagraphTextStorage_EditParagraphAtEnd), 2000 | ("test for editing the last empty paragraph", testParagraphTextStorage_EditEmptyParagraphAtEnd), 2001 | 2002 | // deletion tests 2003 | ("test for deleting the paragraph between other paragraphs", testParagraphTextStorage_DeleteParagraphInMiddle), 2004 | ("test for deleting the last paragraph", testParagraphTextStorage_DeleteParagraphAtEnd), 2005 | ("test for deleting the last empty paragraph", testParagraphTextStorage_DeleteEmptyParagraphAtEnd), 2006 | ("test for deleting the whole paragraph at the beginning", testParagraphTextStorage_DeleteWholeParagraphAtBeginning), 2007 | ("test for deleting the whole paragraph in the middle", testParagraphTextStorage_DeleteWholeParagraphInMiddle), 2008 | ("test for deleting the whole paragraph at the end", testParagraphTextStorage_DeleteWholeParagraphAtEnd), 2009 | 2010 | // mixed operations tests 2011 | ("test for deleting the paragraph at the beginning and editing the following one", testParagraphTextStorage_DeleteWholeParagraphAtBeginningAndEditNextOne), 2012 | ("test for deleting the paragraph at the beginning, editing the following one (when the resulting ranges are equal", 2013 | testParagraphTextStorage_DeleteWholeParagraphAtBeginningAndEditNextOneWithSameFinalRanges), 2014 | ("test for deleting the two paragraphs at the beginning, editing the following one (when the resulting ranges are equal", 2015 | testParagraphTextStorage_DeleteTwoParagraphAtBeginningAndEditNextOneWithSameFinalRanges), 2016 | ("test for deleting the two paragraphs at the beginning, editing the following one", testParagraphTextStorage_DeleteWholeTwoParagraphsAtBeginningAndEditNextOne), 2017 | ("test for deleting the two paragraphs at the beginning and editing the following paragraph and inserting a new one", testParagraphTextStorage_DeleteWholeTwoParagraphsAtBeginningEditingTheNextOneAndInsertNewParagraph), 2018 | ("test for deleting two paragraphs in the middle, editing the following paragraph and inserting a new one", testParagraphTextStorage_DeleteTwoParagraphsInMiddleEditingTheNextOneAndInsertNewParagraph), 2019 | ("test for deleting one paragraph in the middle, editing the following paragraph and inserting a new one", testParagraphTextStorage_DeleteOneParagraphInMiddleEditingTheNextOneAndInsertNewParagraph), 2020 | ("test for incrementally adding and editing paragraphs at the end and periodically insert a new paragraph in the middle", testParagraphTextStorage_IncrementallyAddAndEditParagraphAtEndAndPeriodicallyInsertNewParagraphInMiddle), 2021 | ("test for incrementally adding and editing paragraphs at the end and then deleting some of them", testParagraphTextStorage_IncrementallyAddAndEditParagraphAtEndAndThenDeleteBunchOfThem), 2022 | ("test for replacing all of the existing paragraphs with the two new paragraphs", testParagraphTextStorage_ReplaceAllParagraphsWithTwoNewParagraphs), 2023 | ("test for inserting an empty paragraph in the middle and then inserting another one", testParagraphTextStorage_InsertBlankParagraphInMiddleAndInsertAnotherOne), 2024 | ("test for deleting a paragraph in the middle and editing the next one", testParagraphTextStorage_DeleteParagraphInMiddleAndEditingTheNextOne), 2025 | ("test for inserting an empty paragraph in the middle and edinging the following one", testParagraphTextStorage_InsertBetweenParagraphsBlankParagraphEditingTheNextOne), 2026 | ("test for incrementally editing and inserting paragraphs", testParagraphTextStorage_IncrementalEditingAndInsertingParagraph), 2027 | ("test for incrementally editing paragraphs and make the first one empty", testParagraphTextStorage_IncrementalEditingAndMakeFirstParagraphEmpty), 2028 | ("test for incrementally editing paragraphs and make the middle one empty", testParagraphTextStorage_IncrementalEditingAndMakeMiddleParagraphEmpty), 2029 | ("test for incrementally editing paragraphs and make the last one empty", testParagraphTextStorage_IncrementalEditingAndMakeLastParagraphEmpty) 2030 | ] 2031 | } 2032 | --------------------------------------------------------------------------------