@apollohg/react-native-prose-editor 0.5.15 → 0.5.17
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/src/main/java/com/apollohg/editor/EditorEditText.kt +1454 -127
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +403 -59
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1666 -79
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +209 -87
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +27 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +58 -9
- package/dist/NativeEditorBridge.d.ts +34 -1
- package/dist/NativeEditorBridge.js +243 -83
- package/dist/NativeRichTextEditor.js +998 -137
- package/dist/addons.d.ts +7 -0
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- 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/NativeEditorExpoView.swift +830 -17
- package/ios/NativeEditorModule.swift +304 -108
- package/ios/PositionBridge.swift +24 -1
- package/ios/RichTextEditorView.swift +787 -51
- package/package.json +2 -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
|
@@ -819,6 +819,26 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
819
819
|
let entries: [TopLevelChildMetadata]
|
|
820
820
|
}
|
|
821
821
|
|
|
822
|
+
private struct NativeTextMutation {
|
|
823
|
+
let from: UInt32
|
|
824
|
+
let to: UInt32
|
|
825
|
+
let replacementText: String
|
|
826
|
+
let resultingText: String
|
|
827
|
+
let authorizedText: String
|
|
828
|
+
let selectionAnchor: UInt32?
|
|
829
|
+
let selectionHead: UInt32?
|
|
830
|
+
let capturedWhileFirstResponder: Bool
|
|
831
|
+
let capturedWhileEditable: Bool
|
|
832
|
+
let capturedAfterBlur: Bool
|
|
833
|
+
let inputGeneration: UInt64
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
private enum NativeTextMutationCommitResult {
|
|
837
|
+
case committed
|
|
838
|
+
case deferred
|
|
839
|
+
case rejected
|
|
840
|
+
}
|
|
841
|
+
|
|
822
842
|
private enum PositionCacheUpdate {
|
|
823
843
|
case scan
|
|
824
844
|
case invalidate
|
|
@@ -915,6 +935,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
915
935
|
/// The plain text from the last Rust render, used by the reconciliation
|
|
916
936
|
/// fallback to detect unauthorized text storage mutations.
|
|
917
937
|
private var lastAuthorizedTextStorage = NSMutableString()
|
|
938
|
+
private var lastAuthorizedAttributedTextStorage = NSMutableAttributedString()
|
|
918
939
|
private var lastAuthorizedText: String {
|
|
919
940
|
lastAuthorizedTextStorage as String
|
|
920
941
|
}
|
|
@@ -964,6 +985,17 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
964
985
|
/// trailing UIKit text-storage callbacks that arrive on the next run loop.
|
|
965
986
|
private var interceptedInputDepth = 0
|
|
966
987
|
private var reconciliationWorkScheduled = false
|
|
988
|
+
private var nativeTextMutationCommitScheduled = false
|
|
989
|
+
private var pendingNativeTextMutation: NativeTextMutation?
|
|
990
|
+
private var nativeTextMutationGeneration: UInt64 = 0
|
|
991
|
+
private var nativeTextMutationAfterBlurDeadline: TimeInterval?
|
|
992
|
+
private var nativeTextMutationAfterBlurGeneration: UInt64?
|
|
993
|
+
private let nativeTextMutationAfterBlurGraceInterval: TimeInterval = 1.0
|
|
994
|
+
private var desiredInputTraitState = InputTraitState()
|
|
995
|
+
private var appliedInputTraitState = InputTraitState()
|
|
996
|
+
private var pendingInputTraitChange = PendingInputTraitChange()
|
|
997
|
+
private var pendingInputTraitRetryScheduled = false
|
|
998
|
+
private var pendingInputTraitRetryGeneration: UInt64 = 0
|
|
967
999
|
|
|
968
1000
|
/// Coalesces selection sync until UIKit has finished resolving the
|
|
969
1001
|
/// current tap/drag gesture's final caret position.
|
|
@@ -977,9 +1009,29 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
977
1009
|
private var markedTextReplacementScalarRange: (from: UInt32, to: UInt32)?
|
|
978
1010
|
private var markedTextReplacementUtf16Range: NSRange?
|
|
979
1011
|
private var markedTextCompositionText: String?
|
|
1012
|
+
private var markedTextCompositionIsExplicitlyEmpty = false
|
|
980
1013
|
|
|
981
1014
|
private let editorLayoutManager: EditorLayoutManager
|
|
982
1015
|
|
|
1016
|
+
private struct InputTraitState {
|
|
1017
|
+
var autoCapitalize: String?
|
|
1018
|
+
var autoCorrect: Bool?
|
|
1019
|
+
var keyboardType: String?
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
private struct PendingInputTraitChange {
|
|
1023
|
+
var hasAutoCapitalize = false
|
|
1024
|
+
var autoCapitalize: String?
|
|
1025
|
+
var hasAutoCorrect = false
|
|
1026
|
+
var autoCorrect: Bool?
|
|
1027
|
+
var hasKeyboardType = false
|
|
1028
|
+
var keyboardType: String?
|
|
1029
|
+
|
|
1030
|
+
var isEmpty: Bool {
|
|
1031
|
+
!hasAutoCapitalize && !hasAutoCorrect && !hasKeyboardType
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
983
1035
|
// MARK: - Placeholder
|
|
984
1036
|
|
|
985
1037
|
private lazy var placeholderLabel: UILabel = {
|
|
@@ -1064,6 +1116,19 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1064
1116
|
}
|
|
1065
1117
|
|
|
1066
1118
|
func setAutoCapitalize(_ autoCapitalize: String?) {
|
|
1119
|
+
desiredInputTraitState.autoCapitalize = autoCapitalize
|
|
1120
|
+
guard prepareForInputTraitChange() else {
|
|
1121
|
+
pendingInputTraitChange.hasAutoCapitalize = true
|
|
1122
|
+
pendingInputTraitChange.autoCapitalize = autoCapitalize
|
|
1123
|
+
scheduleInputTraitChangeRetry()
|
|
1124
|
+
return
|
|
1125
|
+
}
|
|
1126
|
+
applyAutoCapitalize(autoCapitalize)
|
|
1127
|
+
appliedInputTraitState.autoCapitalize = autoCapitalize
|
|
1128
|
+
clearPendingAutoCapitalize()
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
private func applyAutoCapitalize(_ autoCapitalize: String?) {
|
|
1067
1132
|
switch autoCapitalize {
|
|
1068
1133
|
case "none":
|
|
1069
1134
|
autocapitalizationType = .none
|
|
@@ -1074,21 +1139,127 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1074
1139
|
default:
|
|
1075
1140
|
autocapitalizationType = .sentences
|
|
1076
1141
|
}
|
|
1142
|
+
if isFirstResponder {
|
|
1143
|
+
reloadInputViews()
|
|
1144
|
+
}
|
|
1077
1145
|
}
|
|
1078
1146
|
|
|
1079
1147
|
func setAutoCorrect(_ autoCorrect: Bool?) {
|
|
1148
|
+
desiredInputTraitState.autoCorrect = autoCorrect
|
|
1149
|
+
guard prepareForInputTraitChange() else {
|
|
1150
|
+
pendingInputTraitChange.hasAutoCorrect = true
|
|
1151
|
+
pendingInputTraitChange.autoCorrect = autoCorrect
|
|
1152
|
+
scheduleInputTraitChangeRetry()
|
|
1153
|
+
return
|
|
1154
|
+
}
|
|
1155
|
+
applyAutoCorrect(autoCorrect)
|
|
1156
|
+
appliedInputTraitState.autoCorrect = autoCorrect
|
|
1157
|
+
clearPendingAutoCorrect()
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
private func applyAutoCorrect(_ autoCorrect: Bool?) {
|
|
1080
1161
|
let isEnabled = autoCorrect ?? false
|
|
1081
1162
|
autocorrectionType = isEnabled ? .yes : .no
|
|
1082
1163
|
spellCheckingType = isEnabled ? .default : .no
|
|
1164
|
+
if isFirstResponder {
|
|
1165
|
+
reloadInputViews()
|
|
1166
|
+
}
|
|
1083
1167
|
}
|
|
1084
1168
|
|
|
1085
1169
|
func setKeyboardType(_ keyboardType: String?) {
|
|
1170
|
+
desiredInputTraitState.keyboardType = keyboardType
|
|
1171
|
+
guard prepareForInputTraitChange() else {
|
|
1172
|
+
pendingInputTraitChange.hasKeyboardType = true
|
|
1173
|
+
pendingInputTraitChange.keyboardType = keyboardType
|
|
1174
|
+
scheduleInputTraitChangeRetry()
|
|
1175
|
+
return
|
|
1176
|
+
}
|
|
1177
|
+
applyKeyboardType(keyboardType)
|
|
1178
|
+
appliedInputTraitState.keyboardType = keyboardType
|
|
1179
|
+
clearPendingKeyboardType()
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
private func applyKeyboardType(_ keyboardType: String?) {
|
|
1086
1183
|
self.keyboardType = Self.resolvedKeyboardType(from: keyboardType)
|
|
1087
1184
|
if isFirstResponder {
|
|
1088
1185
|
reloadInputViews()
|
|
1089
1186
|
}
|
|
1090
1187
|
}
|
|
1091
1188
|
|
|
1189
|
+
private func prepareForInputTraitChange() -> Bool {
|
|
1190
|
+
guard isFirstResponder, editorId != 0 else { return true }
|
|
1191
|
+
return prepareForExternalEditorUpdate()
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
private func scheduleInputTraitChangeRetry() {
|
|
1195
|
+
guard !pendingInputTraitRetryScheduled else { return }
|
|
1196
|
+
pendingInputTraitRetryScheduled = true
|
|
1197
|
+
pendingInputTraitRetryGeneration &+= 1
|
|
1198
|
+
let retryGeneration = pendingInputTraitRetryGeneration
|
|
1199
|
+
DispatchQueue.main.async { [weak self] in
|
|
1200
|
+
guard let self else { return }
|
|
1201
|
+
guard retryGeneration == self.pendingInputTraitRetryGeneration else { return }
|
|
1202
|
+
self.pendingInputTraitRetryScheduled = false
|
|
1203
|
+
let pending = self.pendingInputTraitChange
|
|
1204
|
+
self.pendingInputTraitChange = PendingInputTraitChange()
|
|
1205
|
+
if pending.hasAutoCapitalize,
|
|
1206
|
+
pending.autoCapitalize == self.desiredInputTraitState.autoCapitalize {
|
|
1207
|
+
self.setAutoCapitalize(pending.autoCapitalize)
|
|
1208
|
+
}
|
|
1209
|
+
if pending.hasAutoCorrect,
|
|
1210
|
+
pending.autoCorrect == self.desiredInputTraitState.autoCorrect {
|
|
1211
|
+
self.setAutoCorrect(pending.autoCorrect)
|
|
1212
|
+
}
|
|
1213
|
+
if pending.hasKeyboardType,
|
|
1214
|
+
pending.keyboardType == self.desiredInputTraitState.keyboardType {
|
|
1215
|
+
self.setKeyboardType(pending.keyboardType)
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
private func clearPendingAutoCapitalize() {
|
|
1221
|
+
pendingInputTraitChange.hasAutoCapitalize = false
|
|
1222
|
+
pendingInputTraitChange.autoCapitalize = nil
|
|
1223
|
+
cancelPendingInputTraitRetryIfEmpty()
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
private func clearPendingAutoCorrect() {
|
|
1227
|
+
pendingInputTraitChange.hasAutoCorrect = false
|
|
1228
|
+
pendingInputTraitChange.autoCorrect = nil
|
|
1229
|
+
cancelPendingInputTraitRetryIfEmpty()
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
private func clearPendingKeyboardType() {
|
|
1233
|
+
pendingInputTraitChange.hasKeyboardType = false
|
|
1234
|
+
pendingInputTraitChange.keyboardType = nil
|
|
1235
|
+
cancelPendingInputTraitRetryIfEmpty()
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
private func clearPendingInputTraitRetry() {
|
|
1239
|
+
pendingInputTraitChange = PendingInputTraitChange()
|
|
1240
|
+
guard pendingInputTraitRetryScheduled else { return }
|
|
1241
|
+
pendingInputTraitRetryScheduled = false
|
|
1242
|
+
pendingInputTraitRetryGeneration &+= 1
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
private func cancelPendingInputTraitRetryIfEmpty() {
|
|
1246
|
+
guard pendingInputTraitRetryScheduled, pendingInputTraitChange.isEmpty else { return }
|
|
1247
|
+
pendingInputTraitRetryScheduled = false
|
|
1248
|
+
pendingInputTraitRetryGeneration &+= 1
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
private func replayDesiredInputTraitsIfNeeded() {
|
|
1252
|
+
if desiredInputTraitState.autoCapitalize != appliedInputTraitState.autoCapitalize {
|
|
1253
|
+
setAutoCapitalize(desiredInputTraitState.autoCapitalize)
|
|
1254
|
+
}
|
|
1255
|
+
if desiredInputTraitState.autoCorrect != appliedInputTraitState.autoCorrect {
|
|
1256
|
+
setAutoCorrect(desiredInputTraitState.autoCorrect)
|
|
1257
|
+
}
|
|
1258
|
+
if desiredInputTraitState.keyboardType != appliedInputTraitState.keyboardType {
|
|
1259
|
+
setKeyboardType(desiredInputTraitState.keyboardType)
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1092
1263
|
private static func resolvedKeyboardType(from keyboardType: String?) -> UIKeyboardType {
|
|
1093
1264
|
switch keyboardType {
|
|
1094
1265
|
case "ascii-capable":
|
|
@@ -1223,6 +1394,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1223
1394
|
let didBecomeFirstResponder = super.becomeFirstResponder()
|
|
1224
1395
|
if didBecomeFirstResponder {
|
|
1225
1396
|
ensureInternalTextViewDelegate()
|
|
1397
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1226
1398
|
DispatchQueue.main.async { [weak self] in
|
|
1227
1399
|
self?.ensureInternalTextViewDelegate()
|
|
1228
1400
|
}
|
|
@@ -1232,6 +1404,31 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1232
1404
|
return didBecomeFirstResponder
|
|
1233
1405
|
}
|
|
1234
1406
|
|
|
1407
|
+
override func resignFirstResponder() -> Bool {
|
|
1408
|
+
ensureInternalTextViewDelegate()
|
|
1409
|
+
_ = drainPendingNativeTextMutation(allowAfterBlur: false, allowWhileIntercepting: true)
|
|
1410
|
+
|
|
1411
|
+
let wasFirstResponder = isFirstResponder
|
|
1412
|
+
if wasFirstResponder {
|
|
1413
|
+
nativeTextMutationAfterBlurGeneration = nativeTextMutationGeneration
|
|
1414
|
+
nativeTextMutationAfterBlurDeadline = ProcessInfo.processInfo.systemUptime
|
|
1415
|
+
+ nativeTextMutationAfterBlurGraceInterval
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
let didResignFirstResponder = super.resignFirstResponder()
|
|
1419
|
+
if wasFirstResponder || didResignFirstResponder {
|
|
1420
|
+
_ = drainPendingNativeTextMutation(allowAfterBlur: true, allowWhileIntercepting: true)
|
|
1421
|
+
DispatchQueue.main.async { [weak self] in
|
|
1422
|
+
guard let self else { return }
|
|
1423
|
+
_ = self.drainPendingNativeTextMutation(
|
|
1424
|
+
allowAfterBlur: true,
|
|
1425
|
+
allowWhileIntercepting: true
|
|
1426
|
+
)
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return didResignFirstResponder
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1235
1432
|
private func isRenderedContentEmpty() -> Bool {
|
|
1236
1433
|
let renderedText = textStorage.string
|
|
1237
1434
|
guard !renderedText.isEmpty else { return true }
|
|
@@ -1401,6 +1598,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1401
1598
|
lastRenderAppliedPatchForTesting
|
|
1402
1599
|
}
|
|
1403
1600
|
|
|
1601
|
+
func authorizedTextForTesting() -> String {
|
|
1602
|
+
lastAuthorizedText
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1404
1605
|
func lastApplyUpdateTrace() -> ApplyUpdateTrace? {
|
|
1405
1606
|
lastApplyUpdateTraceForTesting
|
|
1406
1607
|
}
|
|
@@ -1603,6 +1804,12 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1603
1804
|
/// - initialHTML: Optional HTML to set as initial content.
|
|
1604
1805
|
func bindEditor(id: UInt64, initialHTML: String? = nil) {
|
|
1605
1806
|
ensureInternalTextViewDelegate()
|
|
1807
|
+
if editorId == id, initialHTML == nil {
|
|
1808
|
+
return
|
|
1809
|
+
}
|
|
1810
|
+
if editorId != id {
|
|
1811
|
+
discardTransientNativeInputForEditorRebind()
|
|
1812
|
+
}
|
|
1606
1813
|
editorId = id
|
|
1607
1814
|
|
|
1608
1815
|
if let html = initialHTML, !html.isEmpty {
|
|
@@ -1614,10 +1821,12 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1614
1821
|
let stateJSON = editorGetCurrentState(id: editorId)
|
|
1615
1822
|
applyUpdateJSON(stateJSON, notifyDelegate: false)
|
|
1616
1823
|
}
|
|
1824
|
+
replayDesiredInputTraitsIfNeeded()
|
|
1617
1825
|
}
|
|
1618
1826
|
|
|
1619
1827
|
/// Unbind from the current editor instance.
|
|
1620
1828
|
func unbindEditor() {
|
|
1829
|
+
discardTransientNativeInputForEditorRebind()
|
|
1621
1830
|
editorId = 0
|
|
1622
1831
|
}
|
|
1623
1832
|
|
|
@@ -1640,6 +1849,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1640
1849
|
super.insertText(text)
|
|
1641
1850
|
return
|
|
1642
1851
|
}
|
|
1852
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1643
1853
|
|
|
1644
1854
|
if markedTextReplacementScalarRange != nil || markedTextRange != nil {
|
|
1645
1855
|
let replacementRange = trackedMarkedTextReplacementRange()
|
|
@@ -1731,6 +1941,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1731
1941
|
super.deleteBackward()
|
|
1732
1942
|
return
|
|
1733
1943
|
}
|
|
1944
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1734
1945
|
|
|
1735
1946
|
if markedTextReplacementScalarRange != nil || markedTextRange != nil {
|
|
1736
1947
|
performTransientTextMutation {
|
|
@@ -1883,6 +2094,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1883
2094
|
guard !isApplyingRustState else { return }
|
|
1884
2095
|
guard editorId != 0 else { return }
|
|
1885
2096
|
guard isEditable else { return }
|
|
2097
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1886
2098
|
guard isCaretInsideList() else { return }
|
|
1887
2099
|
guard let selection = currentScalarSelection() else { return }
|
|
1888
2100
|
|
|
@@ -1934,6 +2146,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1934
2146
|
super.replace(range, withText: text)
|
|
1935
2147
|
return
|
|
1936
2148
|
}
|
|
2149
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1937
2150
|
|
|
1938
2151
|
if markedTextReplacementScalarRange != nil || markedTextRange != nil {
|
|
1939
2152
|
let replacementRange = trackedMarkedTextReplacementRange()
|
|
@@ -1969,21 +2182,36 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1969
2182
|
override func setMarkedText(_ markedText: String?, selectedRange: NSRange) {
|
|
1970
2183
|
ensureInternalTextViewDelegate()
|
|
1971
2184
|
if markedText != nil {
|
|
2185
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1972
2186
|
captureMarkedTextReplacementRangeIfNeeded()
|
|
2187
|
+
} else if markedTextReplacementScalarRange == nil, markedTextRange == nil {
|
|
2188
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1973
2189
|
}
|
|
1974
2190
|
isComposing = markedText != nil || markedTextReplacementScalarRange != nil
|
|
1975
2191
|
Self.inputLog.debug(
|
|
1976
2192
|
"[setMarkedText] marked=\(self.preview(markedText ?? ""), privacy: .public) nsRange=\(selectedRange.location),\(selectedRange.length) selection=\(self.selectionSummary(), privacy: .public)"
|
|
1977
2193
|
)
|
|
1978
|
-
performTransientTextMutation {
|
|
1979
|
-
super.setMarkedText(markedText, selectedRange: selectedRange)
|
|
1980
|
-
}
|
|
1981
2194
|
if markedText == nil {
|
|
2195
|
+
// Some keyboard paths finalize composition by clearing marked text
|
|
2196
|
+
// instead of calling unmarkText().
|
|
2197
|
+
let composedText = validatedTrackedMarkedTextForCommit()
|
|
2198
|
+
let replacementRange = trackedMarkedTextReplacementRange()
|
|
2199
|
+
performTransientTextMutation {
|
|
2200
|
+
super.setMarkedText(nil, selectedRange: selectedRange)
|
|
2201
|
+
}
|
|
1982
2202
|
clearMarkedTextTracking()
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2203
|
+
if shouldCommitMarkedText(composedText, replacementRange: replacementRange) {
|
|
2204
|
+
commitMarkedText(composedText ?? "", replacementRange: replacementRange)
|
|
2205
|
+
} else {
|
|
2206
|
+
restoreAuthorizedTextAfterCancelledCompositionIfNeeded()
|
|
2207
|
+
}
|
|
2208
|
+
return
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
performTransientTextMutation {
|
|
2212
|
+
super.setMarkedText(markedText, selectedRange: selectedRange)
|
|
1986
2213
|
}
|
|
2214
|
+
refreshMarkedTextCompositionText(fallback: markedText)
|
|
1987
2215
|
}
|
|
1988
2216
|
|
|
1989
2217
|
/// Called when composition is finalized (user selects a candidate or
|
|
@@ -2004,6 +2232,8 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2004
2232
|
"[unmarkText] composed=\(self.preview(composed), privacy: .public) replacement=\(self.previewMarkedTextReplacementRange(replacementRange), privacy: .public) selection=\(self.selectionSummary(), privacy: .public)"
|
|
2005
2233
|
)
|
|
2006
2234
|
commitMarkedText(composed, replacementRange: replacementRange)
|
|
2235
|
+
} else if shouldCommitMarkedText(composedText, replacementRange: replacementRange) {
|
|
2236
|
+
commitMarkedText(composedText ?? "", replacementRange: replacementRange)
|
|
2007
2237
|
} else {
|
|
2008
2238
|
restoreAuthorizedTextAfterCancelledCompositionIfNeeded()
|
|
2009
2239
|
}
|
|
@@ -2014,9 +2244,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2014
2244
|
|
|
2015
2245
|
guard let selectedRange = selectedTextRange else {
|
|
2016
2246
|
let scalarPos = PositionBridge.cursorScalarOffset(in: self)
|
|
2247
|
+
let utf16Pos = PositionBridge.scalarToUtf16Offset(scalarPos, in: lastAuthorizedText)
|
|
2017
2248
|
markedTextReplacementScalarRange = (from: scalarPos, to: scalarPos)
|
|
2018
2249
|
markedTextReplacementUtf16Range = NSRange(
|
|
2019
|
-
location:
|
|
2250
|
+
location: utf16Pos,
|
|
2020
2251
|
length: 0
|
|
2021
2252
|
)
|
|
2022
2253
|
return
|
|
@@ -2046,6 +2277,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2046
2277
|
markedTextReplacementScalarRange = nil
|
|
2047
2278
|
markedTextReplacementUtf16Range = nil
|
|
2048
2279
|
markedTextCompositionText = nil
|
|
2280
|
+
markedTextCompositionIsExplicitlyEmpty = false
|
|
2049
2281
|
isComposing = false
|
|
2050
2282
|
}
|
|
2051
2283
|
|
|
@@ -2064,14 +2296,30 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2064
2296
|
}
|
|
2065
2297
|
|
|
2066
2298
|
private func currentMarkedTextForCommit() -> String? {
|
|
2067
|
-
|
|
2299
|
+
if markedTextCompositionIsExplicitlyEmpty { return "" }
|
|
2300
|
+
return transientMarkedTextFromAuthorizedDiff()
|
|
2301
|
+
?? markedTextRange.flatMap { text(in: $0) }
|
|
2068
2302
|
?? markedTextCompositionText
|
|
2069
|
-
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
private func validatedTrackedMarkedTextForCommit() -> String? {
|
|
2306
|
+
guard markedTextReplacementScalarRange != nil || markedTextReplacementUtf16Range != nil else {
|
|
2307
|
+
return nil
|
|
2308
|
+
}
|
|
2309
|
+
if markedTextCompositionIsExplicitlyEmpty { return "" }
|
|
2310
|
+
return transientMarkedTextFromAuthorizedDiff()
|
|
2311
|
+
?? markedTextRange.flatMap { text(in: $0) }
|
|
2070
2312
|
}
|
|
2071
2313
|
|
|
2072
2314
|
private func refreshMarkedTextCompositionText(fallback: String? = nil) {
|
|
2073
|
-
|
|
2074
|
-
|
|
2315
|
+
if fallback?.isEmpty == true {
|
|
2316
|
+
markedTextCompositionText = ""
|
|
2317
|
+
markedTextCompositionIsExplicitlyEmpty = true
|
|
2318
|
+
return
|
|
2319
|
+
}
|
|
2320
|
+
markedTextCompositionIsExplicitlyEmpty = false
|
|
2321
|
+
markedTextCompositionText = transientMarkedTextFromAuthorizedDiff()
|
|
2322
|
+
?? markedTextRange.flatMap { text(in: $0) }
|
|
2075
2323
|
?? fallback
|
|
2076
2324
|
}
|
|
2077
2325
|
|
|
@@ -2094,6 +2342,27 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2094
2342
|
return nil
|
|
2095
2343
|
}
|
|
2096
2344
|
|
|
2345
|
+
if replacementRange.location > 0 {
|
|
2346
|
+
let prefixRange = NSRange(location: 0, length: replacementRange.location)
|
|
2347
|
+
guard currentText.substring(with: prefixRange) == authorizedText.substring(with: prefixRange) else {
|
|
2348
|
+
return nil
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
let insertedEnd = replacementRange.location + insertedLength
|
|
2353
|
+
let authorizedSuffixLength = authorizedText.length - replacementEnd
|
|
2354
|
+
let currentSuffixLength = currentText.length - insertedEnd
|
|
2355
|
+
guard authorizedSuffixLength == currentSuffixLength else { return nil }
|
|
2356
|
+
if authorizedSuffixLength > 0 {
|
|
2357
|
+
let authorizedSuffixRange = NSRange(location: replacementEnd, length: authorizedSuffixLength)
|
|
2358
|
+
let currentSuffixRange = NSRange(location: insertedEnd, length: currentSuffixLength)
|
|
2359
|
+
guard currentText.substring(with: currentSuffixRange)
|
|
2360
|
+
== authorizedText.substring(with: authorizedSuffixRange)
|
|
2361
|
+
else {
|
|
2362
|
+
return nil
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2097
2366
|
return currentText.substring(
|
|
2098
2367
|
with: NSRange(location: replacementRange.location, length: insertedLength)
|
|
2099
2368
|
)
|
|
@@ -2105,13 +2374,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2105
2374
|
) {
|
|
2106
2375
|
guard editorId != 0 else { return }
|
|
2107
2376
|
guard let replacementRange else {
|
|
2108
|
-
performInterceptedInput {
|
|
2377
|
+
performInterceptedInput(flushPendingNativeTextMutation: false) {
|
|
2109
2378
|
insertTextInRust(text, at: PositionBridge.cursorScalarOffset(in: self))
|
|
2110
2379
|
}
|
|
2111
2380
|
return
|
|
2112
2381
|
}
|
|
2113
2382
|
|
|
2114
|
-
performInterceptedInput {
|
|
2383
|
+
performInterceptedInput(flushPendingNativeTextMutation: false) {
|
|
2115
2384
|
if replacementRange.from == replacementRange.to {
|
|
2116
2385
|
insertTextInRust(text, at: replacementRange.from)
|
|
2117
2386
|
} else {
|
|
@@ -2124,6 +2393,16 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2124
2393
|
}
|
|
2125
2394
|
}
|
|
2126
2395
|
|
|
2396
|
+
private func shouldCommitMarkedText(
|
|
2397
|
+
_ text: String?,
|
|
2398
|
+
replacementRange: (from: UInt32, to: UInt32)?
|
|
2399
|
+
) -> Bool {
|
|
2400
|
+
guard let text else { return false }
|
|
2401
|
+
if !text.isEmpty { return true }
|
|
2402
|
+
guard let replacementRange else { return false }
|
|
2403
|
+
return replacementRange.from != replacementRange.to
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2127
2406
|
private func restoreAuthorizedTextAfterCancelledCompositionIfNeeded() {
|
|
2128
2407
|
guard editorId != 0 else { return }
|
|
2129
2408
|
guard textStorage.string != lastAuthorizedText else { return }
|
|
@@ -2154,6 +2433,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2154
2433
|
super.paste(sender)
|
|
2155
2434
|
return
|
|
2156
2435
|
}
|
|
2436
|
+
guard prepareForExternalEditorUpdate() else { return }
|
|
2157
2437
|
|
|
2158
2438
|
Self.inputLog.debug(
|
|
2159
2439
|
"[paste] selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
@@ -2183,7 +2463,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2183
2463
|
documentAttributes: [.documentType: NSAttributedString.DocumentType.html]
|
|
2184
2464
|
), let html = String(data: htmlData, encoding: .utf8) {
|
|
2185
2465
|
performInterceptedInput {
|
|
2186
|
-
pasteHTML(html)
|
|
2466
|
+
if !pasteHTML(html, detectContentChange: true),
|
|
2467
|
+
!attrStr.string.isEmpty {
|
|
2468
|
+
pastePlainText(attrStr.string)
|
|
2469
|
+
}
|
|
2187
2470
|
}
|
|
2188
2471
|
return
|
|
2189
2472
|
}
|
|
@@ -2209,7 +2492,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2209
2492
|
func textViewDidChangeSelection(_ textView: UITextView) {
|
|
2210
2493
|
guard textView === self else { return }
|
|
2211
2494
|
ensureInternalTextViewDelegate()
|
|
2212
|
-
guard !isApplyingRustState,
|
|
2495
|
+
guard !isApplyingRustState,
|
|
2496
|
+
!isComposing,
|
|
2497
|
+
!nativeTextMutationCommitScheduled,
|
|
2498
|
+
pendingNativeTextMutation == nil
|
|
2499
|
+
else {
|
|
2500
|
+
return
|
|
2501
|
+
}
|
|
2213
2502
|
if normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded() {
|
|
2214
2503
|
return
|
|
2215
2504
|
}
|
|
@@ -2269,7 +2558,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2269
2558
|
delegate = self
|
|
2270
2559
|
}
|
|
2271
2560
|
|
|
2272
|
-
private func performInterceptedInput(
|
|
2561
|
+
private func performInterceptedInput(
|
|
2562
|
+
flushPendingNativeTextMutation: Bool = true,
|
|
2563
|
+
_ action: () -> Void
|
|
2564
|
+
) {
|
|
2565
|
+
if flushPendingNativeTextMutation, interceptedInputDepth == 0 {
|
|
2566
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
2567
|
+
}
|
|
2273
2568
|
interceptedInputDepth += 1
|
|
2274
2569
|
Self.inputLog.debug(
|
|
2275
2570
|
"[intercept.begin] depth=\(self.interceptedInputDepth) selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
@@ -2281,6 +2576,12 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2281
2576
|
Self.inputLog.debug(
|
|
2282
2577
|
"[intercept.end] depth=\(self.interceptedInputDepth) selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
2283
2578
|
)
|
|
2579
|
+
if self.interceptedInputDepth == 0 {
|
|
2580
|
+
_ = self.drainPendingNativeTextMutation(
|
|
2581
|
+
allowAfterBlur: false,
|
|
2582
|
+
allowWhileIntercepting: false
|
|
2583
|
+
)
|
|
2584
|
+
}
|
|
2284
2585
|
}
|
|
2285
2586
|
}
|
|
2286
2587
|
|
|
@@ -2390,7 +2691,14 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2390
2691
|
}
|
|
2391
2692
|
|
|
2392
2693
|
private func syncSelectionToRustAndNotifyDelegate() {
|
|
2393
|
-
guard !isApplyingRustState,
|
|
2694
|
+
guard !isApplyingRustState,
|
|
2695
|
+
!isComposing,
|
|
2696
|
+
!nativeTextMutationCommitScheduled,
|
|
2697
|
+
pendingNativeTextMutation == nil,
|
|
2698
|
+
editorId != 0
|
|
2699
|
+
else {
|
|
2700
|
+
return
|
|
2701
|
+
}
|
|
2394
2702
|
guard let range = selectedTextRange else { return }
|
|
2395
2703
|
|
|
2396
2704
|
let anchor = PositionBridge.textViewToScalar(range.start, in: self)
|
|
@@ -2406,9 +2714,11 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2406
2714
|
editorDelegate?.editorTextView(self, selectionDidChange: docAnchor, head: docHead)
|
|
2407
2715
|
}
|
|
2408
2716
|
|
|
2409
|
-
|
|
2410
|
-
|
|
2717
|
+
@discardableResult
|
|
2718
|
+
func applyTheme(_ theme: EditorTheme?) -> Bool {
|
|
2411
2719
|
if editorId != 0 {
|
|
2720
|
+
guard prepareForExternalEditorUpdate() else { return false }
|
|
2721
|
+
self.theme = theme
|
|
2412
2722
|
let previousOffset = contentOffset
|
|
2413
2723
|
let stateJSON = editorGetCurrentState(id: editorId)
|
|
2414
2724
|
applyUpdateJSON(stateJSON, notifyDelegate: false)
|
|
@@ -2416,11 +2726,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2416
2726
|
preserveScrollOffset(previousOffset)
|
|
2417
2727
|
}
|
|
2418
2728
|
} else {
|
|
2729
|
+
self.theme = theme
|
|
2419
2730
|
refreshTypingAttributesForSelection()
|
|
2420
2731
|
}
|
|
2421
2732
|
if heightBehavior == .autoGrow {
|
|
2422
2733
|
notifyHeightChangeIfNeeded(force: true)
|
|
2423
2734
|
}
|
|
2735
|
+
return true
|
|
2424
2736
|
}
|
|
2425
2737
|
|
|
2426
2738
|
private func preserveScrollOffset(_ previousOffset: CGPoint) {
|
|
@@ -2856,8 +3168,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2856
3168
|
}
|
|
2857
3169
|
|
|
2858
3170
|
func performToolbarToggleMark(_ markName: String) {
|
|
2859
|
-
guard
|
|
2860
|
-
guard isEditable else { return }
|
|
3171
|
+
guard prepareForToolbarCommand() else { return }
|
|
2861
3172
|
guard let selection = currentScalarSelection() else { return }
|
|
2862
3173
|
performInterceptedInput {
|
|
2863
3174
|
let updateJSON = editorToggleMarkAtSelectionScalar(
|
|
@@ -2871,8 +3182,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2871
3182
|
}
|
|
2872
3183
|
|
|
2873
3184
|
func performToolbarToggleList(_ listType: String, isActive: Bool) {
|
|
2874
|
-
guard
|
|
2875
|
-
guard isEditable else { return }
|
|
3185
|
+
guard prepareForToolbarCommand() else { return }
|
|
2876
3186
|
guard let selection = currentScalarSelection() else { return }
|
|
2877
3187
|
performInterceptedInput {
|
|
2878
3188
|
let updateJSON = isActive
|
|
@@ -2892,8 +3202,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2892
3202
|
}
|
|
2893
3203
|
|
|
2894
3204
|
func performToolbarToggleBlockquote() {
|
|
2895
|
-
guard
|
|
2896
|
-
guard isEditable else { return }
|
|
3205
|
+
guard prepareForToolbarCommand() else { return }
|
|
2897
3206
|
guard let selection = currentScalarSelection() else { return }
|
|
2898
3207
|
performInterceptedInput {
|
|
2899
3208
|
let updateJSON = editorToggleBlockquoteAtSelectionScalar(
|
|
@@ -2906,8 +3215,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2906
3215
|
}
|
|
2907
3216
|
|
|
2908
3217
|
func performToolbarToggleHeading(_ level: Int) {
|
|
2909
|
-
guard
|
|
2910
|
-
guard isEditable else { return }
|
|
3218
|
+
guard prepareForToolbarCommand() else { return }
|
|
2911
3219
|
guard let selection = currentScalarSelection() else { return }
|
|
2912
3220
|
guard let level = UInt8(exactly: level), (1...6).contains(level) else { return }
|
|
2913
3221
|
performInterceptedInput {
|
|
@@ -2922,8 +3230,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2922
3230
|
}
|
|
2923
3231
|
|
|
2924
3232
|
func performToolbarIndentListItem() {
|
|
2925
|
-
guard
|
|
2926
|
-
guard isEditable else { return }
|
|
3233
|
+
guard prepareForToolbarCommand() else { return }
|
|
2927
3234
|
guard let selection = currentScalarSelection() else { return }
|
|
2928
3235
|
performInterceptedInput {
|
|
2929
3236
|
let updateJSON = editorIndentListItemAtSelectionScalar(
|
|
@@ -2936,8 +3243,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2936
3243
|
}
|
|
2937
3244
|
|
|
2938
3245
|
func performToolbarOutdentListItem() {
|
|
2939
|
-
guard
|
|
2940
|
-
guard isEditable else { return }
|
|
3246
|
+
guard prepareForToolbarCommand() else { return }
|
|
2941
3247
|
guard let selection = currentScalarSelection() else { return }
|
|
2942
3248
|
performInterceptedInput {
|
|
2943
3249
|
let updateJSON = editorOutdentListItemAtSelectionScalar(
|
|
@@ -2950,16 +3256,14 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2950
3256
|
}
|
|
2951
3257
|
|
|
2952
3258
|
func performToolbarInsertNode(_ nodeType: String) {
|
|
2953
|
-
guard
|
|
2954
|
-
guard isEditable else { return }
|
|
3259
|
+
guard prepareForToolbarCommand() else { return }
|
|
2955
3260
|
performInterceptedInput {
|
|
2956
3261
|
insertNodeInRust(nodeType)
|
|
2957
3262
|
}
|
|
2958
3263
|
}
|
|
2959
3264
|
|
|
2960
3265
|
func performToolbarUndo() {
|
|
2961
|
-
guard
|
|
2962
|
-
guard isEditable else { return }
|
|
3266
|
+
guard prepareForToolbarCommand() else { return }
|
|
2963
3267
|
performInterceptedInput {
|
|
2964
3268
|
let updateJSON = editorUndo(id: editorId)
|
|
2965
3269
|
applyUpdateJSON(updateJSON)
|
|
@@ -2967,14 +3271,19 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2967
3271
|
}
|
|
2968
3272
|
|
|
2969
3273
|
func performToolbarRedo() {
|
|
2970
|
-
guard
|
|
2971
|
-
guard isEditable else { return }
|
|
3274
|
+
guard prepareForToolbarCommand() else { return }
|
|
2972
3275
|
performInterceptedInput {
|
|
2973
3276
|
let updateJSON = editorRedo(id: editorId)
|
|
2974
3277
|
applyUpdateJSON(updateJSON)
|
|
2975
3278
|
}
|
|
2976
3279
|
}
|
|
2977
3280
|
|
|
3281
|
+
private func prepareForToolbarCommand() -> Bool {
|
|
3282
|
+
guard editorId != 0 else { return false }
|
|
3283
|
+
guard isEditable else { return false }
|
|
3284
|
+
return prepareForExternalEditorUpdate()
|
|
3285
|
+
}
|
|
3286
|
+
|
|
2978
3287
|
/// Insert text at a scalar position via the Rust editor.
|
|
2979
3288
|
private func insertTextInRust(_ text: String, at scalarPos: UInt32) {
|
|
2980
3289
|
Self.inputLog.debug(
|
|
@@ -2997,6 +3306,366 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2997
3306
|
applyUpdateJSON(updateJSON)
|
|
2998
3307
|
}
|
|
2999
3308
|
|
|
3309
|
+
private func nativeTextMutationFromAuthorizedDiff(
|
|
3310
|
+
currentText: String
|
|
3311
|
+
) -> NativeTextMutation? {
|
|
3312
|
+
let authorizedText = lastAuthorizedText
|
|
3313
|
+
guard currentText != authorizedText else { return nil }
|
|
3314
|
+
|
|
3315
|
+
let authorized = authorizedText as NSString
|
|
3316
|
+
let current = currentText as NSString
|
|
3317
|
+
let sharedLength = min(authorized.length, current.length)
|
|
3318
|
+
var prefix = 0
|
|
3319
|
+
while prefix < sharedLength,
|
|
3320
|
+
authorized.character(at: prefix) == current.character(at: prefix) {
|
|
3321
|
+
prefix += 1
|
|
3322
|
+
}
|
|
3323
|
+
prefix = sharedUtf16ScalarBoundary(atOrBefore: prefix, in: authorizedText, and: currentText)
|
|
3324
|
+
|
|
3325
|
+
var authorizedEnd = authorized.length
|
|
3326
|
+
var currentEnd = current.length
|
|
3327
|
+
while authorizedEnd > prefix,
|
|
3328
|
+
currentEnd > prefix,
|
|
3329
|
+
authorized.character(at: authorizedEnd - 1) == current.character(at: currentEnd - 1) {
|
|
3330
|
+
authorizedEnd -= 1
|
|
3331
|
+
currentEnd -= 1
|
|
3332
|
+
}
|
|
3333
|
+
authorizedEnd = utf16ScalarBoundary(atOrAfter: authorizedEnd, in: authorizedText)
|
|
3334
|
+
currentEnd = utf16ScalarBoundary(atOrAfter: currentEnd, in: currentText)
|
|
3335
|
+
|
|
3336
|
+
let replacementLength = currentEnd - prefix
|
|
3337
|
+
guard replacementLength >= 0 else { return nil }
|
|
3338
|
+
let replacementText = current.substring(
|
|
3339
|
+
with: NSRange(location: prefix, length: replacementLength)
|
|
3340
|
+
)
|
|
3341
|
+
|
|
3342
|
+
let selectedScalarRange = selectedTextRange.map {
|
|
3343
|
+
PositionBridge.textRangeToScalarRange($0, in: self)
|
|
3344
|
+
}
|
|
3345
|
+
let capturedAfterBlur = canAdoptNativeTextMutationAfterBlur()
|
|
3346
|
+
|
|
3347
|
+
return NativeTextMutation(
|
|
3348
|
+
from: PositionBridge.utf16OffsetToScalar(prefix, in: lastAuthorizedAttributedTextStorage),
|
|
3349
|
+
to: PositionBridge.utf16OffsetToScalar(authorizedEnd, in: lastAuthorizedAttributedTextStorage),
|
|
3350
|
+
replacementText: replacementText,
|
|
3351
|
+
resultingText: currentText,
|
|
3352
|
+
authorizedText: authorizedText,
|
|
3353
|
+
selectionAnchor: selectedScalarRange?.from,
|
|
3354
|
+
selectionHead: selectedScalarRange?.to,
|
|
3355
|
+
capturedWhileFirstResponder: isFirstResponder || capturedAfterBlur,
|
|
3356
|
+
capturedWhileEditable: isEditable,
|
|
3357
|
+
capturedAfterBlur: capturedAfterBlur,
|
|
3358
|
+
inputGeneration: nativeTextMutationGeneration
|
|
3359
|
+
)
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
private func nativeTextMutationWithCurrentSelection(
|
|
3363
|
+
_ mutation: NativeTextMutation
|
|
3364
|
+
) -> NativeTextMutation {
|
|
3365
|
+
let selectedScalarRange = selectedTextRange.map {
|
|
3366
|
+
PositionBridge.textRangeToScalarRange($0, in: self)
|
|
3367
|
+
}
|
|
3368
|
+
return NativeTextMutation(
|
|
3369
|
+
from: mutation.from,
|
|
3370
|
+
to: mutation.to,
|
|
3371
|
+
replacementText: mutation.replacementText,
|
|
3372
|
+
resultingText: mutation.resultingText,
|
|
3373
|
+
authorizedText: mutation.authorizedText,
|
|
3374
|
+
selectionAnchor: selectedScalarRange?.from ?? mutation.selectionAnchor,
|
|
3375
|
+
selectionHead: selectedScalarRange?.to ?? mutation.selectionHead,
|
|
3376
|
+
capturedWhileFirstResponder: mutation.capturedWhileFirstResponder,
|
|
3377
|
+
capturedWhileEditable: mutation.capturedWhileEditable,
|
|
3378
|
+
capturedAfterBlur: mutation.capturedAfterBlur,
|
|
3379
|
+
inputGeneration: mutation.inputGeneration
|
|
3380
|
+
)
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
private func isUtf16ScalarBoundary(_ offset: Int, in text: String) -> Bool {
|
|
3384
|
+
guard offset >= 0, offset <= text.utf16.count else { return false }
|
|
3385
|
+
let utf16Index = text.utf16.index(text.utf16.startIndex, offsetBy: offset)
|
|
3386
|
+
return String.Index(utf16Index, within: text) != nil
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
private func utf16ScalarBoundary(atOrBefore offset: Int, in text: String) -> Int {
|
|
3390
|
+
var candidate = min(max(offset, 0), text.utf16.count)
|
|
3391
|
+
while candidate > 0, !isUtf16ScalarBoundary(candidate, in: text) {
|
|
3392
|
+
candidate -= 1
|
|
3393
|
+
}
|
|
3394
|
+
return candidate
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
private func utf16ScalarBoundary(atOrAfter offset: Int, in text: String) -> Int {
|
|
3398
|
+
var candidate = min(max(offset, 0), text.utf16.count)
|
|
3399
|
+
while candidate < text.utf16.count, !isUtf16ScalarBoundary(candidate, in: text) {
|
|
3400
|
+
candidate += 1
|
|
3401
|
+
}
|
|
3402
|
+
return candidate
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
private func sharedUtf16ScalarBoundary(atOrBefore offset: Int, in lhs: String, and rhs: String) -> Int {
|
|
3406
|
+
var candidate = min(max(offset, 0), lhs.utf16.count, rhs.utf16.count)
|
|
3407
|
+
while candidate > 0,
|
|
3408
|
+
(!isUtf16ScalarBoundary(candidate, in: lhs) || !isUtf16ScalarBoundary(candidate, in: rhs)) {
|
|
3409
|
+
candidate -= 1
|
|
3410
|
+
}
|
|
3411
|
+
return candidate
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
private func shouldAdoptNativeTextStorageMutation(
|
|
3415
|
+
_ mutation: NativeTextMutation,
|
|
3416
|
+
allowAfterBlur: Bool = false
|
|
3417
|
+
) -> Bool {
|
|
3418
|
+
if isFirstResponder && isEditable {
|
|
3419
|
+
return true
|
|
3420
|
+
}
|
|
3421
|
+
return allowAfterBlur
|
|
3422
|
+
&& mutation.capturedAfterBlur
|
|
3423
|
+
&& mutation.inputGeneration == nativeTextMutationGeneration
|
|
3424
|
+
&& canAdoptNativeTextMutationAfterBlur()
|
|
3425
|
+
&& mutation.capturedWhileFirstResponder
|
|
3426
|
+
&& mutation.capturedWhileEditable
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3429
|
+
private func canAdoptNativeTextMutationAfterBlur() -> Bool {
|
|
3430
|
+
guard let deadline = nativeTextMutationAfterBlurDeadline else {
|
|
3431
|
+
return false
|
|
3432
|
+
}
|
|
3433
|
+
guard nativeTextMutationAfterBlurGeneration == nativeTextMutationGeneration else {
|
|
3434
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3435
|
+
return false
|
|
3436
|
+
}
|
|
3437
|
+
guard ProcessInfo.processInfo.systemUptime <= deadline else {
|
|
3438
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3439
|
+
return false
|
|
3440
|
+
}
|
|
3441
|
+
return true
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
private func clearNativeTextMutationAfterBlurWindow() {
|
|
3445
|
+
nativeTextMutationAfterBlurDeadline = nil
|
|
3446
|
+
nativeTextMutationAfterBlurGeneration = nil
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
private func advanceNativeTextMutationGeneration() {
|
|
3450
|
+
nativeTextMutationGeneration &+= 1
|
|
3451
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
func expireNativeTextMutationAfterBlurDeadlineForTesting() {
|
|
3455
|
+
nativeTextMutationAfterBlurDeadline = ProcessInfo.processInfo.systemUptime - 0.001
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
func discardTransientNativeInputForEditorRebind() {
|
|
3459
|
+
pendingNativeTextMutation = nil
|
|
3460
|
+
nativeTextMutationCommitScheduled = false
|
|
3461
|
+
clearPendingInputTraitRetry()
|
|
3462
|
+
markedTextReplacementScalarRange = nil
|
|
3463
|
+
markedTextReplacementUtf16Range = nil
|
|
3464
|
+
markedTextCompositionText = nil
|
|
3465
|
+
markedTextCompositionIsExplicitlyEmpty = false
|
|
3466
|
+
isComposing = false
|
|
3467
|
+
advanceNativeTextMutationGeneration()
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
@discardableResult
|
|
3471
|
+
private func flushPendingNativeTextMutationCommitIfNeeded() -> Bool {
|
|
3472
|
+
drainPendingNativeTextMutation(
|
|
3473
|
+
allowAfterBlur: false,
|
|
3474
|
+
allowWhileIntercepting: true
|
|
3475
|
+
)
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
@discardableResult
|
|
3479
|
+
func prepareForExternalEditorUpdate() -> Bool {
|
|
3480
|
+
guard prepareActiveCompositionForExternalMutation() else { return false }
|
|
3481
|
+
return drainPendingNativeTextMutation(
|
|
3482
|
+
allowAfterBlur: true,
|
|
3483
|
+
allowWhileIntercepting: true
|
|
3484
|
+
)
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
@discardableResult
|
|
3488
|
+
func prepareForExternalEditorCommand() -> (ready: Bool, updateJSON: String?, blockedReason: String?) {
|
|
3489
|
+
let previousEditorId = editorId
|
|
3490
|
+
let previousAuthorizedText = lastAuthorizedText
|
|
3491
|
+
let previousStateJSON = previousEditorId != 0 ? editorGetCurrentState(id: previousEditorId) : nil
|
|
3492
|
+
guard prepareForExternalEditorUpdate() else {
|
|
3493
|
+
return (false, nil, "composition")
|
|
3494
|
+
}
|
|
3495
|
+
guard editorId != 0 else {
|
|
3496
|
+
return (true, nil, nil)
|
|
3497
|
+
}
|
|
3498
|
+
let currentStateJSON = editorGetCurrentState(id: editorId)
|
|
3499
|
+
guard lastAuthorizedText != previousAuthorizedText
|
|
3500
|
+
|| previousEditorId != editorId
|
|
3501
|
+
|| previousStateJSON != currentStateJSON
|
|
3502
|
+
else {
|
|
3503
|
+
return (true, nil, nil)
|
|
3504
|
+
}
|
|
3505
|
+
return (true, currentStateJSON, nil)
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
private func prepareActiveCompositionForExternalMutation() -> Bool {
|
|
3509
|
+
guard isComposing else { return true }
|
|
3510
|
+
|
|
3511
|
+
let composedText = validatedTrackedMarkedTextForCommit()
|
|
3512
|
+
let replacementRange = trackedMarkedTextReplacementRange()
|
|
3513
|
+
finishTransientMarkedTextMutation()
|
|
3514
|
+
|
|
3515
|
+
guard shouldCommitMarkedText(composedText, replacementRange: replacementRange) else {
|
|
3516
|
+
restoreAuthorizedTextAfterCancelledCompositionIfNeeded()
|
|
3517
|
+
return false
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
commitMarkedText(composedText ?? "", replacementRange: replacementRange)
|
|
3521
|
+
return true
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
@discardableResult
|
|
3525
|
+
private func drainPendingNativeTextMutation(
|
|
3526
|
+
allowAfterBlur: Bool,
|
|
3527
|
+
allowWhileIntercepting: Bool
|
|
3528
|
+
) -> Bool {
|
|
3529
|
+
guard nativeTextMutationCommitScheduled
|
|
3530
|
+
|| pendingNativeTextMutation != nil
|
|
3531
|
+
|| (!isComposing && textStorage.string != lastAuthorizedText)
|
|
3532
|
+
else {
|
|
3533
|
+
return true
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
nativeTextMutationCommitScheduled = false
|
|
3537
|
+
let currentText = textStorage.string
|
|
3538
|
+
let mutation: NativeTextMutation?
|
|
3539
|
+
if let pendingNativeTextMutation,
|
|
3540
|
+
pendingNativeTextMutation.resultingText == currentText,
|
|
3541
|
+
pendingNativeTextMutation.authorizedText == lastAuthorizedText
|
|
3542
|
+
{
|
|
3543
|
+
mutation = nativeTextMutationWithCurrentSelection(pendingNativeTextMutation)
|
|
3544
|
+
} else {
|
|
3545
|
+
mutation = nativeTextMutationFromAuthorizedDiff(currentText: currentText)
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
guard let mutation else {
|
|
3549
|
+
pendingNativeTextMutation = nil
|
|
3550
|
+
return true
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
switch commitNativeTextMutationIfPossible(
|
|
3554
|
+
mutation,
|
|
3555
|
+
allowAfterBlur: allowAfterBlur,
|
|
3556
|
+
allowWhileIntercepting: allowWhileIntercepting
|
|
3557
|
+
) {
|
|
3558
|
+
case .committed, .rejected:
|
|
3559
|
+
pendingNativeTextMutation = nil
|
|
3560
|
+
return true
|
|
3561
|
+
case .deferred:
|
|
3562
|
+
pendingNativeTextMutation = mutation
|
|
3563
|
+
return false
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
private func scheduleNativeTextMutationCommit(_ mutation: NativeTextMutation) {
|
|
3568
|
+
pendingNativeTextMutation = mutation
|
|
3569
|
+
guard !nativeTextMutationCommitScheduled else { return }
|
|
3570
|
+
|
|
3571
|
+
nativeTextMutationCommitScheduled = true
|
|
3572
|
+
DispatchQueue.main.async { [weak self] in
|
|
3573
|
+
guard let self else { return }
|
|
3574
|
+
_ = self.drainPendingNativeTextMutation(
|
|
3575
|
+
allowAfterBlur: true,
|
|
3576
|
+
allowWhileIntercepting: true
|
|
3577
|
+
)
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
@discardableResult
|
|
3582
|
+
private func commitNativeTextMutationIfPossible(
|
|
3583
|
+
_ mutation: NativeTextMutation,
|
|
3584
|
+
allowAfterBlur: Bool,
|
|
3585
|
+
allowWhileIntercepting: Bool
|
|
3586
|
+
) -> NativeTextMutationCommitResult {
|
|
3587
|
+
guard editorId != 0 else {
|
|
3588
|
+
return .rejected
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
guard !isApplyingRustState,
|
|
3592
|
+
(!isInterceptingInput || allowWhileIntercepting),
|
|
3593
|
+
!isComposing
|
|
3594
|
+
else {
|
|
3595
|
+
return .deferred
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
guard shouldAdoptNativeTextStorageMutation(mutation, allowAfterBlur: allowAfterBlur) else {
|
|
3599
|
+
if textStorage.string != lastAuthorizedText {
|
|
3600
|
+
scheduleReconciliationFromRust()
|
|
3601
|
+
}
|
|
3602
|
+
return .rejected
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
guard textStorage.string == mutation.resultingText else {
|
|
3606
|
+
if let refreshedMutation = nativeTextMutationFromAuthorizedDiff(currentText: textStorage.string) {
|
|
3607
|
+
return commitNativeTextMutationIfPossible(
|
|
3608
|
+
refreshedMutation,
|
|
3609
|
+
allowAfterBlur: allowAfterBlur,
|
|
3610
|
+
allowWhileIntercepting: allowWhileIntercepting
|
|
3611
|
+
)
|
|
3612
|
+
}
|
|
3613
|
+
return .rejected
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
guard mutation.authorizedText == lastAuthorizedText else {
|
|
3617
|
+
if let refreshedMutation = nativeTextMutationFromAuthorizedDiff(currentText: textStorage.string) {
|
|
3618
|
+
return commitNativeTextMutationIfPossible(
|
|
3619
|
+
refreshedMutation,
|
|
3620
|
+
allowAfterBlur: allowAfterBlur,
|
|
3621
|
+
allowWhileIntercepting: allowWhileIntercepting
|
|
3622
|
+
)
|
|
3623
|
+
}
|
|
3624
|
+
return .rejected
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
performInterceptedInput(flushPendingNativeTextMutation: false) {
|
|
3628
|
+
if mutation.from == mutation.to {
|
|
3629
|
+
guard !mutation.replacementText.isEmpty else { return }
|
|
3630
|
+
insertTextInRust(mutation.replacementText, at: mutation.from)
|
|
3631
|
+
} else if mutation.replacementText.isEmpty {
|
|
3632
|
+
deleteScalarRangeInRust(from: mutation.from, to: mutation.to)
|
|
3633
|
+
} else {
|
|
3634
|
+
replaceTextRangeInRust(
|
|
3635
|
+
from: mutation.from,
|
|
3636
|
+
to: mutation.to,
|
|
3637
|
+
with: mutation.replacementText
|
|
3638
|
+
)
|
|
3639
|
+
}
|
|
3640
|
+
restoreNativeTextMutationSelectionIfNeeded(mutation)
|
|
3641
|
+
}
|
|
3642
|
+
if mutation.capturedAfterBlur {
|
|
3643
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3644
|
+
}
|
|
3645
|
+
return .committed
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
private func restoreNativeTextMutationSelectionIfNeeded(_ mutation: NativeTextMutation) {
|
|
3649
|
+
guard let anchor = mutation.selectionAnchor,
|
|
3650
|
+
let head = mutation.selectionHead,
|
|
3651
|
+
editorId != 0
|
|
3652
|
+
else {
|
|
3653
|
+
return
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
let startUtf16 = PositionBridge.scalarToUtf16Offset(min(anchor, head), in: self)
|
|
3657
|
+
let endUtf16 = PositionBridge.scalarToUtf16Offset(max(anchor, head), in: self)
|
|
3658
|
+
let targetRange = NSRange(location: startUtf16, length: max(0, endUtf16 - startUtf16))
|
|
3659
|
+
if selectedRange != targetRange {
|
|
3660
|
+
selectedRange = targetRange
|
|
3661
|
+
}
|
|
3662
|
+
editorSetSelectionScalar(id: editorId, scalarAnchor: anchor, scalarHead: head)
|
|
3663
|
+
refreshTypingAttributesForSelection()
|
|
3664
|
+
let docAnchor = editorScalarToDoc(id: editorId, scalar: anchor)
|
|
3665
|
+
let docHead = editorScalarToDoc(id: editorId, scalar: head)
|
|
3666
|
+
editorDelegate?.editorTextView(self, selectionDidChange: docAnchor, head: docHead)
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3000
3669
|
private func insertNodeInRust(_ nodeType: String) {
|
|
3001
3670
|
guard let selection = currentScalarSelection() else { return }
|
|
3002
3671
|
Self.inputLog.debug(
|
|
@@ -3178,12 +3847,24 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3178
3847
|
}
|
|
3179
3848
|
|
|
3180
3849
|
/// Paste HTML content through Rust.
|
|
3181
|
-
|
|
3850
|
+
@discardableResult
|
|
3851
|
+
private func pasteHTML(_ html: String, detectContentChange: Bool = false) -> Bool {
|
|
3852
|
+
let previousHTML = detectContentChange ? editorGetHtml(id: editorId) : nil
|
|
3853
|
+
syncCurrentUIKitSelectionToRust()
|
|
3182
3854
|
Self.inputLog.debug(
|
|
3183
3855
|
"[rust.pasteHTML] html=\(self.preview(html), privacy: .public) selection=\(self.selectionSummary(), privacy: .public)"
|
|
3184
3856
|
)
|
|
3185
3857
|
let updateJSON = editorInsertContentHtml(id: editorId, html: html)
|
|
3186
3858
|
applyUpdateJSON(updateJSON)
|
|
3859
|
+
guard let previousHTML else { return true }
|
|
3860
|
+
return editorGetHtml(id: editorId) != previousHTML
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
private func syncCurrentUIKitSelectionToRust() {
|
|
3864
|
+
guard editorId != 0, let range = selectedTextRange else { return }
|
|
3865
|
+
let anchor = PositionBridge.textViewToScalar(range.start, in: self)
|
|
3866
|
+
let head = PositionBridge.textViewToScalar(range.end, in: self)
|
|
3867
|
+
editorSetSelectionScalar(id: editorId, scalarAnchor: anchor, scalarHead: head)
|
|
3187
3868
|
}
|
|
3188
3869
|
|
|
3189
3870
|
/// Paste plain text through Rust.
|
|
@@ -3474,7 +4155,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3474
4155
|
_ attrStr: NSAttributedString,
|
|
3475
4156
|
replaceRange: NSRange? = nil,
|
|
3476
4157
|
usedPatch: Bool,
|
|
3477
|
-
positionCacheUpdate: PositionCacheUpdate = .scan
|
|
4158
|
+
positionCacheUpdate: PositionCacheUpdate = .scan,
|
|
4159
|
+
authorizedReplaceRange: NSRange? = nil,
|
|
4160
|
+
authorizedReplacementText: String? = nil,
|
|
4161
|
+
authorizedReplacementAttributedText: NSAttributedString? = nil
|
|
3478
4162
|
) -> ApplyRenderTrace {
|
|
3479
4163
|
let totalStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3480
4164
|
let replaceUtf16Length = replaceRange?.length ?? textStorage.length
|
|
@@ -3523,13 +4207,27 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3523
4207
|
let endEditingNanos = DispatchTime.now().uptimeNanoseconds - endEditingStartedAt
|
|
3524
4208
|
let textMutationNanos = DispatchTime.now().uptimeNanoseconds - textMutationStartedAt
|
|
3525
4209
|
let authorizedTextStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
4210
|
+
let snapshotReplaceRange = authorizedReplaceRange ?? replaceRange
|
|
4211
|
+
let snapshotReplacementText = authorizedReplacementText ?? attrStr.string
|
|
4212
|
+
let snapshotReplacementAttributedText = authorizedReplacementAttributedText ?? attrStr
|
|
4213
|
+
if let snapshotReplaceRange,
|
|
4214
|
+
snapshotReplaceRange.location >= 0,
|
|
4215
|
+
snapshotReplaceRange.location + snapshotReplaceRange.length <= lastAuthorizedTextStorage.length
|
|
3529
4216
|
{
|
|
3530
|
-
lastAuthorizedTextStorage.replaceCharacters(
|
|
4217
|
+
lastAuthorizedTextStorage.replaceCharacters(
|
|
4218
|
+
in: snapshotReplaceRange,
|
|
4219
|
+
with: snapshotReplacementText
|
|
4220
|
+
)
|
|
4221
|
+
lastAuthorizedAttributedTextStorage.replaceCharacters(
|
|
4222
|
+
in: snapshotReplaceRange,
|
|
4223
|
+
with: snapshotReplacementAttributedText
|
|
4224
|
+
)
|
|
3531
4225
|
} else {
|
|
3532
|
-
lastAuthorizedTextStorage.setString(
|
|
4226
|
+
lastAuthorizedTextStorage.setString(replaceRange == nil ? snapshotReplacementText : textStorage.string)
|
|
4227
|
+
let fallbackAttributedSnapshot = replaceRange == nil
|
|
4228
|
+
? snapshotReplacementAttributedText
|
|
4229
|
+
: NSAttributedString(attributedString: textStorage)
|
|
4230
|
+
lastAuthorizedAttributedTextStorage.setAttributedString(fallbackAttributedSnapshot)
|
|
3533
4231
|
}
|
|
3534
4232
|
let authorizedTextNanos = DispatchTime.now().uptimeNanoseconds - authorizedTextStartedAt
|
|
3535
4233
|
let cacheInvalidationStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
@@ -3829,8 +4527,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3829
4527
|
}
|
|
3830
4528
|
|
|
3831
4529
|
let existing = textStorage.attributedSubstring(from: fullReplaceRange)
|
|
3832
|
-
let
|
|
3833
|
-
let
|
|
4530
|
+
let existingRawString = existing.string
|
|
4531
|
+
let replacementRawString = replacement.string
|
|
4532
|
+
let existingString = existingRawString as NSString
|
|
4533
|
+
let replacementString = replacementRawString as NSString
|
|
3834
4534
|
let sharedLength = min(existing.length, replacement.length)
|
|
3835
4535
|
|
|
3836
4536
|
var prefix = 0
|
|
@@ -3893,6 +4593,17 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3893
4593
|
break
|
|
3894
4594
|
}
|
|
3895
4595
|
}
|
|
4596
|
+
prefix = sharedUtf16ScalarBoundary(atOrBefore: prefix, in: existingRawString, and: replacementRawString)
|
|
4597
|
+
while suffix > 0 {
|
|
4598
|
+
let existingSuffixStart = existing.length - suffix
|
|
4599
|
+
let replacementSuffixStart = replacement.length - suffix
|
|
4600
|
+
if suffix <= sharedLength - prefix,
|
|
4601
|
+
isUtf16ScalarBoundary(existingSuffixStart, in: existingRawString),
|
|
4602
|
+
isUtf16ScalarBoundary(replacementSuffixStart, in: replacementRawString) {
|
|
4603
|
+
break
|
|
4604
|
+
}
|
|
4605
|
+
suffix -= 1
|
|
4606
|
+
}
|
|
3896
4607
|
|
|
3897
4608
|
guard prefix > 0 || suffix > 0 else {
|
|
3898
4609
|
return (fullReplaceRange, replacement)
|
|
@@ -4025,7 +4736,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4025
4736
|
patchToApply.replacement,
|
|
4026
4737
|
replaceRange: patchToApply.replaceRange,
|
|
4027
4738
|
usedPatch: true,
|
|
4028
|
-
positionCacheUpdate: positionCacheUpdate
|
|
4739
|
+
positionCacheUpdate: positionCacheUpdate,
|
|
4740
|
+
authorizedReplaceRange: fullReplaceRange,
|
|
4741
|
+
authorizedReplacementText: attrStr.string,
|
|
4742
|
+
authorizedReplacementAttributedText: attrStr
|
|
4029
4743
|
)
|
|
4030
4744
|
let metadataStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4031
4745
|
applyTopLevelChildMetadataPatch(
|
|
@@ -4069,6 +4783,9 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4069
4783
|
let update = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
4070
4784
|
else { return }
|
|
4071
4785
|
let parseNanos = DispatchTime.now().uptimeNanoseconds - parseStartedAt
|
|
4786
|
+
pendingNativeTextMutation = nil
|
|
4787
|
+
nativeTextMutationCommitScheduled = false
|
|
4788
|
+
advanceNativeTextMutationGeneration()
|
|
4072
4789
|
|
|
4073
4790
|
let renderElements = update["renderElements"] as? [[String: Any]]
|
|
4074
4791
|
let selectionFromUpdate = (update["selection"] as? [String: Any])
|
|
@@ -4433,16 +5150,31 @@ extension EditorTextView: NSTextStorageDelegate {
|
|
|
4433
5150
|
// Only care about actual character edits, not attribute-only changes.
|
|
4434
5151
|
guard editedMask.contains(.editedCharacters) else { return }
|
|
4435
5152
|
|
|
4436
|
-
// Skip if this change came from our own Rust apply path.
|
|
4437
|
-
guard !isApplyingRustState, !
|
|
5153
|
+
// Skip if this change came from our own Rust apply path or transient IME composition.
|
|
5154
|
+
guard !isApplyingRustState, !isComposing else { return }
|
|
4438
5155
|
|
|
4439
5156
|
// Skip if no editor is bound yet (nothing to reconcile against).
|
|
4440
5157
|
guard editorId != 0 else { return }
|
|
4441
5158
|
|
|
5159
|
+
PositionBridge.invalidateCache(for: self)
|
|
5160
|
+
|
|
4442
5161
|
// Compare current text storage content against last authorized snapshot.
|
|
4443
5162
|
let currentText = textStorage.string
|
|
4444
5163
|
guard currentText != lastAuthorizedText else { return }
|
|
4445
5164
|
currentTopLevelChildMetadata = nil
|
|
5165
|
+
|
|
5166
|
+
let allowAfterBlur = canAdoptNativeTextMutationAfterBlur()
|
|
5167
|
+
if let mutation = nativeTextMutationFromAuthorizedDiff(currentText: currentText),
|
|
5168
|
+
isInterceptingInput
|
|
5169
|
+
|| shouldAdoptNativeTextStorageMutation(
|
|
5170
|
+
mutation,
|
|
5171
|
+
allowAfterBlur: allowAfterBlur
|
|
5172
|
+
)
|
|
5173
|
+
{
|
|
5174
|
+
scheduleNativeTextMutationCommit(mutation)
|
|
5175
|
+
return
|
|
5176
|
+
}
|
|
5177
|
+
|
|
4446
5178
|
let authorizedPreview = preview(lastAuthorizedText)
|
|
4447
5179
|
let storagePreview = preview(currentText)
|
|
4448
5180
|
|
|
@@ -4585,6 +5317,8 @@ final class RichTextEditorView: UIView {
|
|
|
4585
5317
|
/// The Rust editor instance ID. Setting this binds/unbinds the editor.
|
|
4586
5318
|
var editorId: UInt64 = 0 {
|
|
4587
5319
|
didSet {
|
|
5320
|
+
guard oldValue != editorId else { return }
|
|
5321
|
+
textView.discardTransientNativeInputForEditorRebind()
|
|
4588
5322
|
if editorId != 0 {
|
|
4589
5323
|
textView.bindEditor(id: editorId)
|
|
4590
5324
|
} else {
|
|
@@ -4705,12 +5439,14 @@ final class RichTextEditorView: UIView {
|
|
|
4705
5439
|
textView.backgroundColor = backgroundColor
|
|
4706
5440
|
}
|
|
4707
5441
|
|
|
4708
|
-
|
|
4709
|
-
|
|
5442
|
+
@discardableResult
|
|
5443
|
+
func applyTheme(_ theme: EditorTheme?) -> Bool {
|
|
5444
|
+
guard textView.applyTheme(theme) else { return false }
|
|
4710
5445
|
let cornerRadius = theme?.borderRadius ?? 0
|
|
4711
5446
|
layer.cornerRadius = cornerRadius
|
|
4712
5447
|
clipsToBounds = cornerRadius > 0
|
|
4713
5448
|
refreshOverlays()
|
|
5449
|
+
return true
|
|
4714
5450
|
}
|
|
4715
5451
|
|
|
4716
5452
|
func setRemoteSelections(_ selections: [RemoteSelectionDecoration]) {
|