@openeditor/react-native-prose-editor 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +35 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
- package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
- package/dist/EditorToolbar.d.ts +1 -1
- package/dist/EditorToolbar.js +19 -0
- package/dist/NativeEditorBridge.d.ts +62 -35
- package/dist/NativeEditorBridge.js +122 -79
- package/dist/NativeProseViewer.d.ts +13 -34
- package/dist/NativeProseViewer.js +40 -465
- package/dist/NativeRichTextEditor.d.ts +10 -1
- package/dist/NativeRichTextEditor.js +72 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/schemas.d.ts +11 -0
- package/dist/schemas.js +149 -0
- package/dist/useNativeEditor.d.ts +3 -3
- package/dist/useNativeEditor.js +3 -3
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/Generated_editor_core.swift +97 -2
- package/ios/NativeBlockEditorSurface.swift +1393 -0
- package/ios/NativeEditorExpoView.swift +3 -3
- package/ios/NativeEditorModule.swift +30 -45
- package/ios/NativeInputSupport.swift +396 -0
- package/ios/PositionBridge.swift +4 -4
- package/ios/RichTextEditorView.swift +333 -786
- package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
- package/package.json +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2087 -1399
- package/android/src/androidTest/AndroidManifest.xml +0 -8
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
- package/ios/EditorLayoutManager.swift +0 -774
- package/ios/NativeProseViewerExpoView.swift +0 -276
- package/ios/RenderBridge.swift +0 -1708
- package/ios/Tests/NativePerformanceTests.swift +0 -687
- package/ios/Tests/PositionBridgeTests.swift +0 -706
- package/ios/Tests/RenderBridgeTests.swift +0 -2236
- package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
|
@@ -1,687 +0,0 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
|
|
3
|
-
private struct ApplyUpdateTraceStats {
|
|
4
|
-
let name: String
|
|
5
|
-
let traces: [EditorTextView.ApplyUpdateTrace]
|
|
6
|
-
|
|
7
|
-
private func average(_ selector: (EditorTextView.ApplyUpdateTrace) -> UInt64) -> Double {
|
|
8
|
-
guard !traces.isEmpty else { return 0 }
|
|
9
|
-
return Double(traces.map(selector).reduce(0, +)) / Double(traces.count) / 1_000_000.0
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
func summaryString(tag: String = "NativePerformanceTests") -> String {
|
|
13
|
-
let averageReplaceUtf16 = traces.isEmpty
|
|
14
|
-
? 0
|
|
15
|
-
: traces.map(\.applyRenderReplaceUtf16Length).reduce(0, +) / traces.count
|
|
16
|
-
let averageReplacementUtf16 = traces.isEmpty
|
|
17
|
-
? 0
|
|
18
|
-
: traces.map(\.applyRenderReplacementUtf16Length).reduce(0, +) / traces.count
|
|
19
|
-
return "[\(tag)] \(name) avgMs={parse=\(String(format: "%.3f", average { $0.parseNanos })), resolveBlocks=\(String(format: "%.3f", average { $0.resolveRenderBlocksNanos })), patchEligibility=\(String(format: "%.3f", average { $0.patchEligibilityNanos })), patchTrim=\(String(format: "%.3f", average { $0.patchTrimNanos })), patchMetadata=\(String(format: "%.3f", average { $0.patchMetadataNanos })), buildRender=\(String(format: "%.3f", average { $0.buildRenderNanos })), applyRender=\(String(format: "%.3f", average { $0.applyRenderNanos })), applyRenderTextMutation=\(String(format: "%.3f", average { $0.applyRenderTextMutationNanos })), applyRenderBeginEditing=\(String(format: "%.3f", average { $0.applyRenderBeginEditingNanos })), applyRenderEndEditing=\(String(format: "%.3f", average { $0.applyRenderEndEditingNanos })), applyRenderStringMutation=\(String(format: "%.3f", average { $0.applyRenderStringMutationNanos })), applyRenderAttributeMutation=\(String(format: "%.3f", average { $0.applyRenderAttributeMutationNanos })), applyRenderAuthorizedText=\(String(format: "%.3f", average { $0.applyRenderAuthorizedTextNanos })), applyRenderCacheInvalidation=\(String(format: "%.3f", average { $0.applyRenderCacheInvalidationNanos })), selection=\(String(format: "%.3f", average { $0.selectionNanos })), selectionResolve=\(String(format: "%.3f", average { $0.selectionResolveNanos })), selectionAssignment=\(String(format: "%.3f", average { $0.selectionAssignmentNanos })), selectionChrome=\(String(format: "%.3f", average { $0.selectionChromeNanos })), postApply=\(String(format: "%.3f", average { $0.postApplyNanos })), postApplyTypingAttributes=\(String(format: "%.3f", average { $0.postApplyTypingAttributesNanos })), postApplyHeightNotify=\(String(format: "%.3f", average { $0.postApplyHeightNotifyNanos })), postApplyHeightNotifyMeasure=\(String(format: "%.3f", average { $0.postApplyHeightNotifyMeasureNanos })), postApplyHeightNotifyCallback=\(String(format: "%.3f", average { $0.postApplyHeightNotifyCallbackNanos })), postApplyHeightNotifyEnsureLayout=\(String(format: "%.3f", average { $0.postApplyHeightNotifyEnsureLayoutNanos })), postApplyHeightNotifyUsedRect=\(String(format: "%.3f", average { $0.postApplyHeightNotifyUsedRectNanos })), postApplyHeightNotifyContentSize=\(String(format: "%.3f", average { $0.postApplyHeightNotifyContentSizeNanos })), postApplyHeightNotifySizeThatFits=\(String(format: "%.3f", average { $0.postApplyHeightNotifySizeThatFitsNanos })), postApplySelectionOrContent=\(String(format: "%.3f", average { $0.postApplySelectionOrContentCallbackNanos })), total=\(String(format: "%.3f", average { $0.totalNanos }))} patchUsage=\(traces.filter { $0.usedPatch }.count)/\(traces.count) smallPatchMutationUsage=\(traces.filter { $0.usedSmallPatchTextMutation }.count)/\(traces.count) avgPatchUtf16={replace=\(averageReplaceUtf16), replacement=\(averageReplacementUtf16)}"
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
private struct HostedLayoutTraceStats {
|
|
24
|
-
let name: String
|
|
25
|
-
let traces: [RichTextEditorView.HostedLayoutTrace]
|
|
26
|
-
|
|
27
|
-
private func average(
|
|
28
|
-
_ selector: (RichTextEditorView.HostedLayoutTrace) -> UInt64
|
|
29
|
-
) -> Double {
|
|
30
|
-
guard !traces.isEmpty else { return 0 }
|
|
31
|
-
return Double(traces.map(selector).reduce(0, +)) / Double(traces.count) / 1_000_000.0
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
private func averageCount(
|
|
35
|
-
_ selector: (RichTextEditorView.HostedLayoutTrace) -> Int
|
|
36
|
-
) -> Double {
|
|
37
|
-
guard !traces.isEmpty else { return 0 }
|
|
38
|
-
return Double(traces.map(selector).reduce(0, +)) / Double(traces.count)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
func summaryString(tag: String = "NativePerformanceTests") -> String {
|
|
42
|
-
"[\(tag)] \(name) avgMs={intrinsicContentSize=\(String(format: "%.3f", average { $0.intrinsicContentSizeNanos })), measuredEditorHeight=\(String(format: "%.3f", average { $0.measuredEditorHeightNanos })), layoutSubviews=\(String(format: "%.3f", average { $0.layoutSubviewsNanos })), refreshOverlays=\(String(format: "%.3f", average { $0.refreshOverlaysNanos })), onHeightMayChange=\(String(format: "%.3f", average { $0.onHeightMayChangeNanos }))} avgCount={intrinsicContentSize=\(String(format: "%.2f", averageCount { $0.intrinsicContentSizeCount })), measuredEditorHeight=\(String(format: "%.2f", averageCount { $0.measuredEditorHeightCount })), layoutSubviews=\(String(format: "%.2f", averageCount { $0.layoutSubviewsCount })), refreshOverlays=\(String(format: "%.2f", averageCount { $0.refreshOverlaysCount })), overlayScheduleRequest=\(String(format: "%.2f", averageCount { $0.overlayScheduleRequestCount })), overlayScheduleExecute=\(String(format: "%.2f", averageCount { $0.overlayScheduleExecuteCount })), overlayScheduleSkip=\(String(format: "%.2f", averageCount { $0.overlayScheduleSkipCount })), onHeightMayChange=\(String(format: "%.2f", averageCount { $0.onHeightMayChangeCount }))}"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
@MainActor
|
|
47
|
-
final class NativePerformanceTests: XCTestCase {
|
|
48
|
-
private let baseFont = UIFont.systemFont(ofSize: 16)
|
|
49
|
-
private let textColor = UIColor.black
|
|
50
|
-
|
|
51
|
-
func testPerformance_renderBridgeLargeDocument() {
|
|
52
|
-
let renderJSON = NativePerformanceFixtureFactory.largeRenderJSON()
|
|
53
|
-
let options = measureOptions()
|
|
54
|
-
|
|
55
|
-
measure(metrics: [XCTClockMetric()], options: options) {
|
|
56
|
-
autoreleasepool {
|
|
57
|
-
let attributed = RenderBridge.renderElements(
|
|
58
|
-
fromJSON: renderJSON,
|
|
59
|
-
baseFont: baseFont,
|
|
60
|
-
textColor: textColor
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
XCTAssertGreaterThan(attributed.length, 0)
|
|
64
|
-
_ = attributed.string.utf16.count
|
|
65
|
-
if attributed.length > 0 {
|
|
66
|
-
_ = attributed.attributes(at: min(1, attributed.length - 1), effectiveRange: nil)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
func testPerformance_applyUpdateJSONLargeDocument() {
|
|
73
|
-
let editorId = editorCreate(configJson: "{}")
|
|
74
|
-
defer { editorDestroy(id: editorId) }
|
|
75
|
-
|
|
76
|
-
let updateJSON = NativePerformanceFixtureFactory.loadLargeDocument(into: editorId)
|
|
77
|
-
let textView = EditorTextView(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
|
|
78
|
-
textView.captureApplyUpdateTraceForTesting = true
|
|
79
|
-
textView.bindEditor(id: editorId)
|
|
80
|
-
var traceSamples: [EditorTextView.ApplyUpdateTrace] = []
|
|
81
|
-
|
|
82
|
-
// Warm the text system before measuring steady-state apply cost.
|
|
83
|
-
textView.applyUpdateJSON(updateJSON, notifyDelegate: false)
|
|
84
|
-
textView.layoutIfNeeded()
|
|
85
|
-
|
|
86
|
-
let options = measureOptions()
|
|
87
|
-
measure(metrics: [XCTClockMetric()], options: options) {
|
|
88
|
-
autoreleasepool {
|
|
89
|
-
textView.applyUpdateJSON(updateJSON, notifyDelegate: false)
|
|
90
|
-
textView.layoutIfNeeded()
|
|
91
|
-
if let trace = textView.lastApplyUpdateTrace() {
|
|
92
|
-
traceSamples.append(trace)
|
|
93
|
-
}
|
|
94
|
-
XCTAssertFalse(textView.text.isEmpty)
|
|
95
|
-
_ = textView.attributedText.length
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
print(
|
|
99
|
-
ApplyUpdateTraceStats(
|
|
100
|
-
name: "applyUpdateJSONLargeDocument.breakdown",
|
|
101
|
-
traces: traceSamples
|
|
102
|
-
).summaryString()
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
func testPerformance_typingRoundTripLargeDocument() {
|
|
107
|
-
let editorId = editorCreate(configJson: "{}")
|
|
108
|
-
defer { editorDestroy(id: editorId) }
|
|
109
|
-
|
|
110
|
-
_ = NativePerformanceFixtureFactory.loadLargeDocument(into: editorId)
|
|
111
|
-
let textView = EditorTextView(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
|
|
112
|
-
textView.bindEditor(id: editorId)
|
|
113
|
-
textView.layoutIfNeeded()
|
|
114
|
-
|
|
115
|
-
let typingOffset = NativePerformanceFixtureFactory.typingCursorOffset(in: textView)
|
|
116
|
-
setSelection(in: textView, utf16Range: NSRange(location: typingOffset, length: 0))
|
|
117
|
-
|
|
118
|
-
let options = measureOptions()
|
|
119
|
-
measure(metrics: [XCTClockMetric()], options: options) {
|
|
120
|
-
autoreleasepool {
|
|
121
|
-
setSelection(in: textView, utf16Range: NSRange(location: typingOffset, length: 0))
|
|
122
|
-
textView.insertText("!")
|
|
123
|
-
textView.deleteBackward()
|
|
124
|
-
XCTAssertFalse(textView.text.isEmpty)
|
|
125
|
-
XCTAssertNotNil(textView.selectedTextRange)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
func testPerformance_paragraphSplitRoundTripLargeDocument() {
|
|
131
|
-
let options = measureOptions()
|
|
132
|
-
let sessions = NativePerformanceFixtureFactory.paragraphSplitSessions(
|
|
133
|
-
count: max(options.iterationCount + 4, 12)
|
|
134
|
-
)
|
|
135
|
-
defer {
|
|
136
|
-
for session in sessions {
|
|
137
|
-
editorDestroy(id: session.editorId)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
var remainingSessionIndices = Array(sessions.indices)
|
|
142
|
-
var traceSamples: [EditorTextView.ApplyUpdateTrace] = []
|
|
143
|
-
|
|
144
|
-
measure(metrics: [XCTClockMetric()], options: options) {
|
|
145
|
-
autoreleasepool {
|
|
146
|
-
guard let sessionIndex = remainingSessionIndices.first else {
|
|
147
|
-
XCTFail("expected prebuilt paragraph split sessions")
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
remainingSessionIndices.removeFirst()
|
|
151
|
-
|
|
152
|
-
let session = sessions[sessionIndex]
|
|
153
|
-
setSelection(in: session.textView, utf16Range: NSRange(location: session.splitOffset, length: 0))
|
|
154
|
-
session.textView.insertText("\n")
|
|
155
|
-
session.textView.layoutIfNeeded()
|
|
156
|
-
|
|
157
|
-
if let trace = session.textView.lastApplyUpdateTrace() {
|
|
158
|
-
traceSamples.append(trace)
|
|
159
|
-
}
|
|
160
|
-
XCTAssertGreaterThan(session.textView.attributedText.length, session.initialTextLength)
|
|
161
|
-
XCTAssertTrue(session.textView.lastRenderAppliedPatch())
|
|
162
|
-
XCTAssertNotNil(session.textView.selectedTextRange)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
print(
|
|
166
|
-
ApplyUpdateTraceStats(
|
|
167
|
-
name: "paragraphSplitRoundTripLargeDocument.breakdown",
|
|
168
|
-
traces: traceSamples
|
|
169
|
-
).summaryString()
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
func testPerformance_paragraphSplitRoundTripLargeDocument_autoGrow() {
|
|
174
|
-
let options = measureOptions()
|
|
175
|
-
let sessions = NativePerformanceFixtureFactory.paragraphSplitSessions(
|
|
176
|
-
count: max(options.iterationCount + 4, 12),
|
|
177
|
-
autoGrow: true
|
|
178
|
-
)
|
|
179
|
-
defer {
|
|
180
|
-
for session in sessions {
|
|
181
|
-
editorDestroy(id: session.editorId)
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
var remainingSessionIndices = Array(sessions.indices)
|
|
186
|
-
var traceSamples: [EditorTextView.ApplyUpdateTrace] = []
|
|
187
|
-
|
|
188
|
-
measure(metrics: [XCTClockMetric()], options: options) {
|
|
189
|
-
autoreleasepool {
|
|
190
|
-
guard let sessionIndex = remainingSessionIndices.first else {
|
|
191
|
-
XCTFail("expected prebuilt paragraph split sessions")
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
remainingSessionIndices.removeFirst()
|
|
195
|
-
|
|
196
|
-
let session = sessions[sessionIndex]
|
|
197
|
-
setSelection(in: session.textView, utf16Range: NSRange(location: session.splitOffset, length: 0))
|
|
198
|
-
session.textView.insertText("\n")
|
|
199
|
-
session.textView.layoutIfNeeded()
|
|
200
|
-
|
|
201
|
-
if let trace = session.textView.lastApplyUpdateTrace() {
|
|
202
|
-
traceSamples.append(trace)
|
|
203
|
-
}
|
|
204
|
-
XCTAssertGreaterThan(session.textView.attributedText.length, session.initialTextLength)
|
|
205
|
-
XCTAssertTrue(session.textView.lastRenderAppliedPatch())
|
|
206
|
-
XCTAssertNotNil(session.textView.selectedTextRange)
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
print(
|
|
210
|
-
ApplyUpdateTraceStats(
|
|
211
|
-
name: "paragraphSplitRoundTripLargeDocument.autoGrow.breakdown",
|
|
212
|
-
traces: traceSamples
|
|
213
|
-
).summaryString()
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
func testPerformance_paragraphSplitRoundTripLargeDocument_autoGrowHostedView() {
|
|
218
|
-
let options = measureOptions()
|
|
219
|
-
let sessions = NativePerformanceFixtureFactory.hostedParagraphSplitSessions(
|
|
220
|
-
count: max(options.iterationCount + 4, 12)
|
|
221
|
-
)
|
|
222
|
-
defer {
|
|
223
|
-
for session in sessions {
|
|
224
|
-
session.view.removeFromSuperview()
|
|
225
|
-
session.window.isHidden = true
|
|
226
|
-
editorDestroy(id: session.editorId)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
var remainingSessionIndices = Array(sessions.indices)
|
|
231
|
-
var traceSamples: [EditorTextView.ApplyUpdateTrace] = []
|
|
232
|
-
var hostedLayoutTraceSamples: [RichTextEditorView.HostedLayoutTrace] = []
|
|
233
|
-
|
|
234
|
-
measure(metrics: [XCTClockMetric()], options: options) {
|
|
235
|
-
autoreleasepool {
|
|
236
|
-
guard let sessionIndex = remainingSessionIndices.first else {
|
|
237
|
-
XCTFail("expected prebuilt hosted paragraph split sessions")
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
remainingSessionIndices.removeFirst()
|
|
241
|
-
|
|
242
|
-
let session = sessions[sessionIndex]
|
|
243
|
-
session.view.captureHostedLayoutTraceForTesting = true
|
|
244
|
-
session.view.resetHostedLayoutTraceForTesting()
|
|
245
|
-
setSelection(in: session.view.textView, utf16Range: NSRange(location: session.splitOffset, length: 0))
|
|
246
|
-
session.view.textView.insertText("\n")
|
|
247
|
-
flushMainQueue()
|
|
248
|
-
|
|
249
|
-
let measuredHeight = ceil(session.view.intrinsicContentSize.height)
|
|
250
|
-
session.view.frame.size.height = measuredHeight
|
|
251
|
-
session.view.layoutIfNeeded()
|
|
252
|
-
|
|
253
|
-
if let trace = session.view.textView.lastApplyUpdateTrace() {
|
|
254
|
-
traceSamples.append(trace)
|
|
255
|
-
}
|
|
256
|
-
hostedLayoutTraceSamples.append(session.view.lastHostedLayoutTraceForTesting())
|
|
257
|
-
XCTAssertGreaterThan(session.view.textView.attributedText.length, session.initialTextLength)
|
|
258
|
-
XCTAssertTrue(session.view.textView.lastRenderAppliedPatch())
|
|
259
|
-
XCTAssertGreaterThan(measuredHeight, 0)
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
print(
|
|
263
|
-
ApplyUpdateTraceStats(
|
|
264
|
-
name: "paragraphSplitRoundTripLargeDocument.autoGrowHostedView.breakdown",
|
|
265
|
-
traces: traceSamples
|
|
266
|
-
).summaryString()
|
|
267
|
-
)
|
|
268
|
-
print(
|
|
269
|
-
HostedLayoutTraceStats(
|
|
270
|
-
name: "paragraphSplitRoundTripLargeDocument.autoGrowHostedView.hostedLayout",
|
|
271
|
-
traces: hostedLayoutTraceSamples
|
|
272
|
-
).summaryString()
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
func testPerformance_selectionScrubLargeDocument() {
|
|
277
|
-
let editorId = editorCreate(configJson: "{}")
|
|
278
|
-
defer { editorDestroy(id: editorId) }
|
|
279
|
-
|
|
280
|
-
_ = NativePerformanceFixtureFactory.loadLargeDocument(into: editorId)
|
|
281
|
-
let textView = EditorTextView(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
|
|
282
|
-
textView.bindEditor(id: editorId)
|
|
283
|
-
textView.layoutIfNeeded()
|
|
284
|
-
|
|
285
|
-
let scrubOffsets = NativePerformanceFixtureFactory.selectionScrubOffsets(in: textView, points: 48)
|
|
286
|
-
let options = measureOptions()
|
|
287
|
-
|
|
288
|
-
measure(metrics: [XCTClockMetric()], options: options) {
|
|
289
|
-
autoreleasepool {
|
|
290
|
-
for offset in scrubOffsets {
|
|
291
|
-
setSelection(in: textView, utf16Range: NSRange(location: offset, length: 0))
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
let finalOffset = textView.offset(
|
|
295
|
-
from: textView.beginningOfDocument,
|
|
296
|
-
to: textView.selectedTextRange?.start ?? textView.endOfDocument
|
|
297
|
-
)
|
|
298
|
-
XCTAssertEqual(finalOffset, scrubOffsets.last ?? 0)
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
func testPerformance_remoteSelectionOverlayRefreshMultiPeerLargeDocument() {
|
|
304
|
-
let editorId = editorCreate(configJson: "{}")
|
|
305
|
-
defer { editorDestroy(id: editorId) }
|
|
306
|
-
|
|
307
|
-
let updateJSON = NativePerformanceFixtureFactory.loadLargeDocument(into: editorId)
|
|
308
|
-
let view = RichTextEditorView(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
|
|
309
|
-
view.editorId = editorId
|
|
310
|
-
view.textView.applyUpdateJSON(updateJSON, notifyDelegate: false)
|
|
311
|
-
view.layoutIfNeeded()
|
|
312
|
-
|
|
313
|
-
let selections = NativePerformanceFixtureFactory.remoteSelections(
|
|
314
|
-
editorId: editorId,
|
|
315
|
-
peerCount: 24,
|
|
316
|
-
selectionWidth: 24
|
|
317
|
-
)
|
|
318
|
-
view.setRemoteSelections(selections)
|
|
319
|
-
view.layoutIfNeeded()
|
|
320
|
-
|
|
321
|
-
let options = measureOptions()
|
|
322
|
-
measure(metrics: [XCTClockMetric()], options: options) {
|
|
323
|
-
autoreleasepool {
|
|
324
|
-
view.setRemoteSelections(selections)
|
|
325
|
-
view.layoutIfNeeded()
|
|
326
|
-
XCTAssertFalse(view.remoteSelectionOverlaySubviewsForTesting().isEmpty)
|
|
327
|
-
XCTAssertGreaterThanOrEqual(view.remoteSelectionOverlaySubviewsForTesting().count, selections.count)
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
private func measureOptions() -> XCTMeasureOptions {
|
|
333
|
-
let options = XCTMeasureOptions()
|
|
334
|
-
options.iterationCount = 5
|
|
335
|
-
return options
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
private enum NativePerformanceFixtureFactory {
|
|
340
|
-
private static let blockCount = 96
|
|
341
|
-
private static let paragraphCharacterCount = 180
|
|
342
|
-
|
|
343
|
-
struct ParagraphSplitSession {
|
|
344
|
-
let editorId: UInt64
|
|
345
|
-
let textView: EditorTextView
|
|
346
|
-
let splitOffset: Int
|
|
347
|
-
let initialTextLength: Int
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
struct HostedParagraphSplitSession {
|
|
351
|
-
let editorId: UInt64
|
|
352
|
-
let window: UIWindow
|
|
353
|
-
let view: RichTextEditorView
|
|
354
|
-
let splitOffset: Int
|
|
355
|
-
let initialTextLength: Int
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
static func largeRenderJSON() -> String {
|
|
359
|
-
let editorId = editorCreate(configJson: "{}")
|
|
360
|
-
defer { editorDestroy(id: editorId) }
|
|
361
|
-
return editorSetJson(id: editorId, json: largeDocumentJSONString())
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
static func loadLargeDocument(into editorId: UInt64) -> String {
|
|
365
|
-
_ = editorSetJson(id: editorId, json: largeDocumentJSONString())
|
|
366
|
-
return editorGetCurrentState(id: editorId)
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
static func remoteSelections(
|
|
370
|
-
editorId: UInt64,
|
|
371
|
-
peerCount: Int = 6,
|
|
372
|
-
selectionWidth: Int = 0
|
|
373
|
-
) -> [RemoteSelectionDecoration] {
|
|
374
|
-
let totalScalar = editorDocToScalar(id: editorId, docPos: editorDocumentContentSize(id: editorId))
|
|
375
|
-
let upperBound = max(1, Int(totalScalar > 0 ? totalScalar - 1 : 0))
|
|
376
|
-
let samplePoints = evenlySpacedValues(from: 1, through: upperBound, count: peerCount)
|
|
377
|
-
|
|
378
|
-
return samplePoints.enumerated().map { index, scalar in
|
|
379
|
-
let headScalar = (selectionWidth > 0 && !index.isMultiple(of: 2))
|
|
380
|
-
? min(upperBound, scalar + selectionWidth)
|
|
381
|
-
: scalar
|
|
382
|
-
let anchorDoc = editorScalarToDoc(id: editorId, scalar: UInt32(scalar))
|
|
383
|
-
let headDoc = editorScalarToDoc(id: editorId, scalar: UInt32(headScalar))
|
|
384
|
-
return RemoteSelectionDecoration(
|
|
385
|
-
clientId: index + 1,
|
|
386
|
-
anchor: anchorDoc,
|
|
387
|
-
head: headDoc,
|
|
388
|
-
color: indexedColor(index),
|
|
389
|
-
name: "Peer \(index + 1)",
|
|
390
|
-
isFocused: true
|
|
391
|
-
)
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
static func typingCursorOffset(in textView: UITextView) -> Int {
|
|
396
|
-
selectionScrubOffsets(in: textView, points: 1).first ?? 0
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
static func paragraphSplitSessions(count: Int, autoGrow: Bool = false) -> [ParagraphSplitSession] {
|
|
400
|
-
(0..<count).map { _ in
|
|
401
|
-
let editorId = editorCreate(configJson: "{}")
|
|
402
|
-
_ = loadLargeDocument(into: editorId)
|
|
403
|
-
|
|
404
|
-
let textView = EditorTextView(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
|
|
405
|
-
textView.heightBehavior = autoGrow ? .autoGrow : .fixed
|
|
406
|
-
textView.captureApplyUpdateTraceForTesting = true
|
|
407
|
-
textView.bindEditor(id: editorId)
|
|
408
|
-
textView.layoutIfNeeded()
|
|
409
|
-
|
|
410
|
-
return ParagraphSplitSession(
|
|
411
|
-
editorId: editorId,
|
|
412
|
-
textView: textView,
|
|
413
|
-
splitOffset: paragraphSplitCursorOffset(in: textView),
|
|
414
|
-
initialTextLength: textView.attributedText.length
|
|
415
|
-
)
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
static func hostedParagraphSplitSessions(count: Int) -> [HostedParagraphSplitSession] {
|
|
420
|
-
(0..<count).map { _ in
|
|
421
|
-
let editorId = editorCreate(configJson: "{}")
|
|
422
|
-
|
|
423
|
-
let view = RichTextEditorView(frame: CGRect(x: 0, y: 0, width: 390, height: 0))
|
|
424
|
-
let window = hostEditorView(view, size: CGSize(width: 390, height: 844))
|
|
425
|
-
view.heightBehavior = .autoGrow
|
|
426
|
-
view.textView.captureApplyUpdateTraceForTesting = true
|
|
427
|
-
view.editorId = editorId
|
|
428
|
-
view.setContent(json: largeDocumentJSONString())
|
|
429
|
-
flushMainQueue()
|
|
430
|
-
|
|
431
|
-
let measuredHeight = ceil(view.intrinsicContentSize.height)
|
|
432
|
-
view.frame.size.height = measuredHeight
|
|
433
|
-
view.layoutIfNeeded()
|
|
434
|
-
|
|
435
|
-
return HostedParagraphSplitSession(
|
|
436
|
-
editorId: editorId,
|
|
437
|
-
window: window,
|
|
438
|
-
view: view,
|
|
439
|
-
splitOffset: paragraphSplitCursorOffset(in: view.textView),
|
|
440
|
-
initialTextLength: view.textView.attributedText.length
|
|
441
|
-
)
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
static func selectionScrubOffsets(in textView: UITextView, points: Int) -> [Int] {
|
|
446
|
-
let candidates = visibleCharacterOffsets(in: textView.textStorage.string as NSString)
|
|
447
|
-
guard !candidates.isEmpty else { return [0] }
|
|
448
|
-
return evenlySpacedValues(from: 0, through: candidates.count - 1, count: points).map { candidates[$0] }
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
static func paragraphSplitCursorOffset(in textView: UITextView) -> Int {
|
|
452
|
-
let text = textView.textStorage.string as NSString
|
|
453
|
-
let firstBlockBreak = (0..<text.length).first { index in
|
|
454
|
-
let character = text.character(at: index)
|
|
455
|
-
return character == 0x000A || character == 0x000D
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
guard let firstBlockBreak else {
|
|
459
|
-
return typingCursorOffset(in: textView)
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
let paragraphOffsets = visibleCharacterOffsets(in: text).filter { $0 > firstBlockBreak }
|
|
463
|
-
guard !paragraphOffsets.isEmpty else {
|
|
464
|
-
return typingCursorOffset(in: textView)
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return paragraphOffsets[min(32, paragraphOffsets.count - 1)]
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
private static func largeDocumentJSONString() -> String {
|
|
471
|
-
let jsonObject: [String: Any] = [
|
|
472
|
-
"type": "doc",
|
|
473
|
-
"content": largeDocumentContent(),
|
|
474
|
-
]
|
|
475
|
-
let data = try! JSONSerialization.data(withJSONObject: jsonObject, options: [])
|
|
476
|
-
return String(data: data, encoding: .utf8)!
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
private static func largeDocumentContent() -> [[String: Any]] {
|
|
480
|
-
var content: [[String: Any]] = [
|
|
481
|
-
[
|
|
482
|
-
"type": "h1",
|
|
483
|
-
"content": [textNode(textFragment(seed: 10_000, minCharacterCount: 40))],
|
|
484
|
-
],
|
|
485
|
-
]
|
|
486
|
-
|
|
487
|
-
for index in 0..<blockCount {
|
|
488
|
-
if index > 0 && index % 18 == 0 {
|
|
489
|
-
content.append(["type": "horizontalRule"])
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if index % 12 == 5 {
|
|
493
|
-
content.append([
|
|
494
|
-
"type": "blockquote",
|
|
495
|
-
"content": [[
|
|
496
|
-
"type": "paragraph",
|
|
497
|
-
"content": richInlineContent(seed: index, totalCharacters: paragraphCharacterCount),
|
|
498
|
-
]],
|
|
499
|
-
])
|
|
500
|
-
continue
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if index % 9 == 3 {
|
|
504
|
-
content.append([
|
|
505
|
-
"type": "h2",
|
|
506
|
-
"content": [textNode(textFragment(seed: index + 2_000, minCharacterCount: 72))],
|
|
507
|
-
])
|
|
508
|
-
continue
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
content.append([
|
|
512
|
-
"type": "paragraph",
|
|
513
|
-
"content": richInlineContent(seed: index, totalCharacters: paragraphCharacterCount),
|
|
514
|
-
])
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
return content
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
private static func richInlineContent(seed: Int, totalCharacters: Int) -> [[String: Any]] {
|
|
521
|
-
let text = textFragment(seed: seed, minCharacterCount: totalCharacters)
|
|
522
|
-
let characters = Array(text)
|
|
523
|
-
let count = characters.count
|
|
524
|
-
let cutA = count / 4
|
|
525
|
-
let cutB = count / 2
|
|
526
|
-
let cutC = (count * 3) / 4
|
|
527
|
-
|
|
528
|
-
let segments: [(String, [[String: Any]]?)] = [
|
|
529
|
-
(String(characters[0..<cutA]), nil),
|
|
530
|
-
(String(characters[cutA..<cutB]), [["type": "bold"]]),
|
|
531
|
-
(String(characters[cutB..<cutC]), [["type": "italic"]]),
|
|
532
|
-
(
|
|
533
|
-
String(characters[cutC..<count]),
|
|
534
|
-
[[
|
|
535
|
-
"type": "link",
|
|
536
|
-
"attrs": [
|
|
537
|
-
"href": "https://example.com/item/\(seed)",
|
|
538
|
-
"target": "_blank",
|
|
539
|
-
"rel": "noopener noreferrer nofollow",
|
|
540
|
-
"class": NSNull(),
|
|
541
|
-
"title": NSNull(),
|
|
542
|
-
],
|
|
543
|
-
]]
|
|
544
|
-
),
|
|
545
|
-
]
|
|
546
|
-
|
|
547
|
-
return segments.compactMap { text, marks in
|
|
548
|
-
guard !text.isEmpty else { return nil }
|
|
549
|
-
return textNode(text, marks: marks)
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
private static func textNode(_ text: String, marks: [[String: Any]]? = nil) -> [String: Any] {
|
|
554
|
-
var node: [String: Any] = [
|
|
555
|
-
"type": "text",
|
|
556
|
-
"text": text,
|
|
557
|
-
]
|
|
558
|
-
if let marks, !marks.isEmpty {
|
|
559
|
-
node["marks"] = marks
|
|
560
|
-
}
|
|
561
|
-
return node
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
private static func textFragment(seed: Int, minCharacterCount: Int) -> String {
|
|
565
|
-
let words = [
|
|
566
|
-
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india",
|
|
567
|
-
"juliet", "kilo", "lima", "mike", "november", "oscar", "papa", "quebec", "romeo",
|
|
568
|
-
"sierra", "tango", "uniform", "victor", "whiskey", "xray", "yankee", "zulu",
|
|
569
|
-
]
|
|
570
|
-
|
|
571
|
-
var result = ""
|
|
572
|
-
var cursor = 0
|
|
573
|
-
while result.count < minCharacterCount {
|
|
574
|
-
if !result.isEmpty {
|
|
575
|
-
result.append(" ")
|
|
576
|
-
}
|
|
577
|
-
result.append(words[(seed + cursor) % words.count])
|
|
578
|
-
cursor += 1
|
|
579
|
-
}
|
|
580
|
-
return String(result.prefix(minCharacterCount))
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
private static func indexedColor(_ index: Int) -> UIColor {
|
|
584
|
-
let colors: [UIColor] = [
|
|
585
|
-
.systemBlue,
|
|
586
|
-
.systemGreen,
|
|
587
|
-
.systemOrange,
|
|
588
|
-
.systemPink,
|
|
589
|
-
.systemPurple,
|
|
590
|
-
.systemTeal,
|
|
591
|
-
]
|
|
592
|
-
return colors[index % colors.count]
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private static func visibleCharacterOffsets(in text: NSString) -> [Int] {
|
|
596
|
-
(0..<text.length).compactMap { index in
|
|
597
|
-
switch text.character(at: index) {
|
|
598
|
-
case 0xFFFC, 0x200B, 0x000A, 0x000D:
|
|
599
|
-
return nil
|
|
600
|
-
default:
|
|
601
|
-
return index
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
private static func evenlySpacedValues(from start: Int, through end: Int, count: Int) -> [Int] {
|
|
607
|
-
guard count > 1, end > start else {
|
|
608
|
-
return [min(start, end)]
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
return (0..<count).map { index in
|
|
612
|
-
start + Int((Double(end - start) * Double(index) / Double(count - 1)).rounded(.toNearestOrAwayFromZero))
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
private static func editorDocumentContentSize(id: UInt64) -> UInt32 {
|
|
617
|
-
guard let data = editorGetJson(id: id).data(using: .utf8),
|
|
618
|
-
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
619
|
-
else {
|
|
620
|
-
return 0
|
|
621
|
-
}
|
|
622
|
-
let children = json["content"] as? [[String: Any]] ?? []
|
|
623
|
-
return children.reduce(UInt32(0)) { partial, child in
|
|
624
|
-
partial + nodeSize(child)
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
private static func nodeSize(_ node: [String: Any]) -> UInt32 {
|
|
629
|
-
let type = node["type"] as? String ?? ""
|
|
630
|
-
if type == "text" {
|
|
631
|
-
let text = node["text"] as? String ?? ""
|
|
632
|
-
return UInt32(text.count)
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
if isVoidNode(type) {
|
|
636
|
-
return 1
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
let children = node["content"] as? [[String: Any]] ?? []
|
|
640
|
-
let childrenSize = children.reduce(UInt32(0)) { partial, child in
|
|
641
|
-
partial + nodeSize(child)
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
return 1 + childrenSize + 1
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
private static func isVoidNode(_ type: String) -> Bool {
|
|
648
|
-
switch type {
|
|
649
|
-
case "horizontalRule", "hardBreak", "image", "mention":
|
|
650
|
-
return true
|
|
651
|
-
default:
|
|
652
|
-
return false
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
private func setSelection(in textView: UITextView, utf16Range: NSRange) {
|
|
658
|
-
guard
|
|
659
|
-
let start = textView.position(from: textView.beginningOfDocument, offset: utf16Range.location),
|
|
660
|
-
let end = textView.position(from: start, offset: utf16Range.length),
|
|
661
|
-
let range = textView.textRange(from: start, to: end)
|
|
662
|
-
else {
|
|
663
|
-
XCTFail("expected selection range \(utf16Range)")
|
|
664
|
-
return
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
textView.selectedTextRange = range
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
private func hostEditorView(_ view: RichTextEditorView, size: CGSize) -> UIWindow {
|
|
671
|
-
let window = UIWindow(frame: CGRect(origin: .zero, size: size))
|
|
672
|
-
let viewController = UIViewController()
|
|
673
|
-
window.rootViewController = viewController
|
|
674
|
-
window.makeKeyAndVisible()
|
|
675
|
-
view.frame = CGRect(origin: .zero, size: size)
|
|
676
|
-
viewController.view.addSubview(view)
|
|
677
|
-
view.layoutIfNeeded()
|
|
678
|
-
return window
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
private func flushMainQueue() {
|
|
682
|
-
let expectation = XCTestExpectation(description: "flush main queue")
|
|
683
|
-
DispatchQueue.main.async {
|
|
684
|
-
expectation.fulfill()
|
|
685
|
-
}
|
|
686
|
-
XCTWaiter().wait(for: [expectation], timeout: 1.0)
|
|
687
|
-
}
|