├── 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 |
--------------------------------------------------------------------------------