@apollohg/react-native-prose-editor 0.5.16 → 0.5.18
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 +2440 -275
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +783 -64
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1767 -81
- 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 +912 -89
- 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
|
@@ -824,6 +824,22 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
824
824
|
let to: UInt32
|
|
825
825
|
let replacementText: String
|
|
826
826
|
let resultingText: String
|
|
827
|
+
let authorizedText: String
|
|
828
|
+
let selectionAnchor: UInt32?
|
|
829
|
+
let selectionHead: UInt32?
|
|
830
|
+
let authorizedSelectionUtf16Range: NSRange?
|
|
831
|
+
let rawSelectionUtf16Range: NSRange?
|
|
832
|
+
let selectionRevision: UInt64
|
|
833
|
+
let capturedWhileFirstResponder: Bool
|
|
834
|
+
let capturedWhileEditable: Bool
|
|
835
|
+
let capturedAfterBlur: Bool
|
|
836
|
+
let inputGeneration: UInt64
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private enum NativeTextMutationCommitResult {
|
|
840
|
+
case committed
|
|
841
|
+
case deferred
|
|
842
|
+
case rejected
|
|
827
843
|
}
|
|
828
844
|
|
|
829
845
|
private enum PositionCacheUpdate {
|
|
@@ -922,6 +938,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
922
938
|
/// The plain text from the last Rust render, used by the reconciliation
|
|
923
939
|
/// fallback to detect unauthorized text storage mutations.
|
|
924
940
|
private var lastAuthorizedTextStorage = NSMutableString()
|
|
941
|
+
private var lastAuthorizedAttributedTextStorage = NSMutableAttributedString()
|
|
925
942
|
private var lastAuthorizedText: String {
|
|
926
943
|
lastAuthorizedTextStorage as String
|
|
927
944
|
}
|
|
@@ -973,6 +990,18 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
973
990
|
private var reconciliationWorkScheduled = false
|
|
974
991
|
private var nativeTextMutationCommitScheduled = false
|
|
975
992
|
private var pendingNativeTextMutation: NativeTextMutation?
|
|
993
|
+
private var nativeTextMutationGeneration: UInt64 = 0
|
|
994
|
+
private var nativeTextMutationAfterBlurDeadline: TimeInterval?
|
|
995
|
+
private var nativeTextMutationAfterBlurGeneration: UInt64?
|
|
996
|
+
private let nativeTextMutationAfterBlurGraceInterval: TimeInterval = 1.0
|
|
997
|
+
/// Last selection known to match `lastAuthorizedText`, stored in that text's UTF-16 coordinates.
|
|
998
|
+
private var lastAuthorizedSelectedUtf16Range: NSRange?
|
|
999
|
+
private var selectionRevision: UInt64 = 0
|
|
1000
|
+
private var desiredInputTraitState = InputTraitState()
|
|
1001
|
+
private var appliedInputTraitState = InputTraitState()
|
|
1002
|
+
private var pendingInputTraitChange = PendingInputTraitChange()
|
|
1003
|
+
private var pendingInputTraitRetryScheduled = false
|
|
1004
|
+
private var pendingInputTraitRetryGeneration: UInt64 = 0
|
|
976
1005
|
|
|
977
1006
|
/// Coalesces selection sync until UIKit has finished resolving the
|
|
978
1007
|
/// current tap/drag gesture's final caret position.
|
|
@@ -986,9 +1015,29 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
986
1015
|
private var markedTextReplacementScalarRange: (from: UInt32, to: UInt32)?
|
|
987
1016
|
private var markedTextReplacementUtf16Range: NSRange?
|
|
988
1017
|
private var markedTextCompositionText: String?
|
|
1018
|
+
private var markedTextCompositionIsExplicitlyEmpty = false
|
|
989
1019
|
|
|
990
1020
|
private let editorLayoutManager: EditorLayoutManager
|
|
991
1021
|
|
|
1022
|
+
private struct InputTraitState {
|
|
1023
|
+
var autoCapitalize: String?
|
|
1024
|
+
var autoCorrect: Bool?
|
|
1025
|
+
var keyboardType: String?
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private struct PendingInputTraitChange {
|
|
1029
|
+
var hasAutoCapitalize = false
|
|
1030
|
+
var autoCapitalize: String?
|
|
1031
|
+
var hasAutoCorrect = false
|
|
1032
|
+
var autoCorrect: Bool?
|
|
1033
|
+
var hasKeyboardType = false
|
|
1034
|
+
var keyboardType: String?
|
|
1035
|
+
|
|
1036
|
+
var isEmpty: Bool {
|
|
1037
|
+
!hasAutoCapitalize && !hasAutoCorrect && !hasKeyboardType
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
992
1041
|
// MARK: - Placeholder
|
|
993
1042
|
|
|
994
1043
|
private lazy var placeholderLabel: UILabel = {
|
|
@@ -1073,6 +1122,19 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1073
1122
|
}
|
|
1074
1123
|
|
|
1075
1124
|
func setAutoCapitalize(_ autoCapitalize: String?) {
|
|
1125
|
+
desiredInputTraitState.autoCapitalize = autoCapitalize
|
|
1126
|
+
guard prepareForInputTraitChange() else {
|
|
1127
|
+
pendingInputTraitChange.hasAutoCapitalize = true
|
|
1128
|
+
pendingInputTraitChange.autoCapitalize = autoCapitalize
|
|
1129
|
+
scheduleInputTraitChangeRetry()
|
|
1130
|
+
return
|
|
1131
|
+
}
|
|
1132
|
+
applyAutoCapitalize(autoCapitalize)
|
|
1133
|
+
appliedInputTraitState.autoCapitalize = autoCapitalize
|
|
1134
|
+
clearPendingAutoCapitalize()
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
private func applyAutoCapitalize(_ autoCapitalize: String?) {
|
|
1076
1138
|
switch autoCapitalize {
|
|
1077
1139
|
case "none":
|
|
1078
1140
|
autocapitalizationType = .none
|
|
@@ -1083,21 +1145,127 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1083
1145
|
default:
|
|
1084
1146
|
autocapitalizationType = .sentences
|
|
1085
1147
|
}
|
|
1148
|
+
if isFirstResponder {
|
|
1149
|
+
reloadInputViews()
|
|
1150
|
+
}
|
|
1086
1151
|
}
|
|
1087
1152
|
|
|
1088
1153
|
func setAutoCorrect(_ autoCorrect: Bool?) {
|
|
1154
|
+
desiredInputTraitState.autoCorrect = autoCorrect
|
|
1155
|
+
guard prepareForInputTraitChange() else {
|
|
1156
|
+
pendingInputTraitChange.hasAutoCorrect = true
|
|
1157
|
+
pendingInputTraitChange.autoCorrect = autoCorrect
|
|
1158
|
+
scheduleInputTraitChangeRetry()
|
|
1159
|
+
return
|
|
1160
|
+
}
|
|
1161
|
+
applyAutoCorrect(autoCorrect)
|
|
1162
|
+
appliedInputTraitState.autoCorrect = autoCorrect
|
|
1163
|
+
clearPendingAutoCorrect()
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
private func applyAutoCorrect(_ autoCorrect: Bool?) {
|
|
1089
1167
|
let isEnabled = autoCorrect ?? false
|
|
1090
1168
|
autocorrectionType = isEnabled ? .yes : .no
|
|
1091
1169
|
spellCheckingType = isEnabled ? .default : .no
|
|
1170
|
+
if isFirstResponder {
|
|
1171
|
+
reloadInputViews()
|
|
1172
|
+
}
|
|
1092
1173
|
}
|
|
1093
1174
|
|
|
1094
1175
|
func setKeyboardType(_ keyboardType: String?) {
|
|
1176
|
+
desiredInputTraitState.keyboardType = keyboardType
|
|
1177
|
+
guard prepareForInputTraitChange() else {
|
|
1178
|
+
pendingInputTraitChange.hasKeyboardType = true
|
|
1179
|
+
pendingInputTraitChange.keyboardType = keyboardType
|
|
1180
|
+
scheduleInputTraitChangeRetry()
|
|
1181
|
+
return
|
|
1182
|
+
}
|
|
1183
|
+
applyKeyboardType(keyboardType)
|
|
1184
|
+
appliedInputTraitState.keyboardType = keyboardType
|
|
1185
|
+
clearPendingKeyboardType()
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
private func applyKeyboardType(_ keyboardType: String?) {
|
|
1095
1189
|
self.keyboardType = Self.resolvedKeyboardType(from: keyboardType)
|
|
1096
1190
|
if isFirstResponder {
|
|
1097
1191
|
reloadInputViews()
|
|
1098
1192
|
}
|
|
1099
1193
|
}
|
|
1100
1194
|
|
|
1195
|
+
private func prepareForInputTraitChange() -> Bool {
|
|
1196
|
+
guard isFirstResponder, editorId != 0 else { return true }
|
|
1197
|
+
return prepareForExternalEditorUpdate()
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
private func scheduleInputTraitChangeRetry() {
|
|
1201
|
+
guard !pendingInputTraitRetryScheduled else { return }
|
|
1202
|
+
pendingInputTraitRetryScheduled = true
|
|
1203
|
+
pendingInputTraitRetryGeneration &+= 1
|
|
1204
|
+
let retryGeneration = pendingInputTraitRetryGeneration
|
|
1205
|
+
DispatchQueue.main.async { [weak self] in
|
|
1206
|
+
guard let self else { return }
|
|
1207
|
+
guard retryGeneration == self.pendingInputTraitRetryGeneration else { return }
|
|
1208
|
+
self.pendingInputTraitRetryScheduled = false
|
|
1209
|
+
let pending = self.pendingInputTraitChange
|
|
1210
|
+
self.pendingInputTraitChange = PendingInputTraitChange()
|
|
1211
|
+
if pending.hasAutoCapitalize,
|
|
1212
|
+
pending.autoCapitalize == self.desiredInputTraitState.autoCapitalize {
|
|
1213
|
+
self.setAutoCapitalize(pending.autoCapitalize)
|
|
1214
|
+
}
|
|
1215
|
+
if pending.hasAutoCorrect,
|
|
1216
|
+
pending.autoCorrect == self.desiredInputTraitState.autoCorrect {
|
|
1217
|
+
self.setAutoCorrect(pending.autoCorrect)
|
|
1218
|
+
}
|
|
1219
|
+
if pending.hasKeyboardType,
|
|
1220
|
+
pending.keyboardType == self.desiredInputTraitState.keyboardType {
|
|
1221
|
+
self.setKeyboardType(pending.keyboardType)
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
private func clearPendingAutoCapitalize() {
|
|
1227
|
+
pendingInputTraitChange.hasAutoCapitalize = false
|
|
1228
|
+
pendingInputTraitChange.autoCapitalize = nil
|
|
1229
|
+
cancelPendingInputTraitRetryIfEmpty()
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
private func clearPendingAutoCorrect() {
|
|
1233
|
+
pendingInputTraitChange.hasAutoCorrect = false
|
|
1234
|
+
pendingInputTraitChange.autoCorrect = nil
|
|
1235
|
+
cancelPendingInputTraitRetryIfEmpty()
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
private func clearPendingKeyboardType() {
|
|
1239
|
+
pendingInputTraitChange.hasKeyboardType = false
|
|
1240
|
+
pendingInputTraitChange.keyboardType = nil
|
|
1241
|
+
cancelPendingInputTraitRetryIfEmpty()
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
private func clearPendingInputTraitRetry() {
|
|
1245
|
+
pendingInputTraitChange = PendingInputTraitChange()
|
|
1246
|
+
guard pendingInputTraitRetryScheduled else { return }
|
|
1247
|
+
pendingInputTraitRetryScheduled = false
|
|
1248
|
+
pendingInputTraitRetryGeneration &+= 1
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
private func cancelPendingInputTraitRetryIfEmpty() {
|
|
1252
|
+
guard pendingInputTraitRetryScheduled, pendingInputTraitChange.isEmpty else { return }
|
|
1253
|
+
pendingInputTraitRetryScheduled = false
|
|
1254
|
+
pendingInputTraitRetryGeneration &+= 1
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
private func replayDesiredInputTraitsIfNeeded() {
|
|
1258
|
+
if desiredInputTraitState.autoCapitalize != appliedInputTraitState.autoCapitalize {
|
|
1259
|
+
setAutoCapitalize(desiredInputTraitState.autoCapitalize)
|
|
1260
|
+
}
|
|
1261
|
+
if desiredInputTraitState.autoCorrect != appliedInputTraitState.autoCorrect {
|
|
1262
|
+
setAutoCorrect(desiredInputTraitState.autoCorrect)
|
|
1263
|
+
}
|
|
1264
|
+
if desiredInputTraitState.keyboardType != appliedInputTraitState.keyboardType {
|
|
1265
|
+
setKeyboardType(desiredInputTraitState.keyboardType)
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1101
1269
|
private static func resolvedKeyboardType(from keyboardType: String?) -> UIKeyboardType {
|
|
1102
1270
|
switch keyboardType {
|
|
1103
1271
|
case "ascii-capable":
|
|
@@ -1232,15 +1400,42 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1232
1400
|
let didBecomeFirstResponder = super.becomeFirstResponder()
|
|
1233
1401
|
if didBecomeFirstResponder {
|
|
1234
1402
|
ensureInternalTextViewDelegate()
|
|
1403
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1235
1404
|
DispatchQueue.main.async { [weak self] in
|
|
1236
1405
|
self?.ensureInternalTextViewDelegate()
|
|
1237
1406
|
}
|
|
1238
1407
|
_ = normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded()
|
|
1408
|
+
recordAuthorizedSelectionIfPossible()
|
|
1239
1409
|
refreshTypingAttributesForSelection()
|
|
1240
1410
|
}
|
|
1241
1411
|
return didBecomeFirstResponder
|
|
1242
1412
|
}
|
|
1243
1413
|
|
|
1414
|
+
override func resignFirstResponder() -> Bool {
|
|
1415
|
+
ensureInternalTextViewDelegate()
|
|
1416
|
+
_ = drainPendingNativeTextMutation(allowAfterBlur: false, allowWhileIntercepting: true)
|
|
1417
|
+
|
|
1418
|
+
let wasFirstResponder = isFirstResponder
|
|
1419
|
+
if wasFirstResponder {
|
|
1420
|
+
nativeTextMutationAfterBlurGeneration = nativeTextMutationGeneration
|
|
1421
|
+
nativeTextMutationAfterBlurDeadline = ProcessInfo.processInfo.systemUptime
|
|
1422
|
+
+ nativeTextMutationAfterBlurGraceInterval
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
let didResignFirstResponder = super.resignFirstResponder()
|
|
1426
|
+
if wasFirstResponder || didResignFirstResponder {
|
|
1427
|
+
_ = drainPendingNativeTextMutation(allowAfterBlur: true, allowWhileIntercepting: true)
|
|
1428
|
+
DispatchQueue.main.async { [weak self] in
|
|
1429
|
+
guard let self else { return }
|
|
1430
|
+
_ = self.drainPendingNativeTextMutation(
|
|
1431
|
+
allowAfterBlur: true,
|
|
1432
|
+
allowWhileIntercepting: true
|
|
1433
|
+
)
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return didResignFirstResponder
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1244
1439
|
private func isRenderedContentEmpty() -> Bool {
|
|
1245
1440
|
let renderedText = textStorage.string
|
|
1246
1441
|
guard !renderedText.isEmpty else { return true }
|
|
@@ -1271,6 +1466,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1271
1466
|
let adjustedRange = NSRange(location: 0, length: 0)
|
|
1272
1467
|
guard currentRange != adjustedRange else { return false }
|
|
1273
1468
|
selectedRange = adjustedRange
|
|
1469
|
+
noteSelectionDidChange()
|
|
1274
1470
|
return true
|
|
1275
1471
|
}
|
|
1276
1472
|
|
|
@@ -1306,6 +1502,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1306
1502
|
|
|
1307
1503
|
_ = becomeFirstResponder()
|
|
1308
1504
|
selectedTextRange = textRange
|
|
1505
|
+
noteSelectionDidChange()
|
|
1309
1506
|
refreshNativeSelectionChromeVisibility()
|
|
1310
1507
|
onSelectionOrContentMayChange?()
|
|
1311
1508
|
scheduleSelectionSync()
|
|
@@ -1320,6 +1517,30 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1320
1517
|
return NSRange(location: location, length: length)
|
|
1321
1518
|
}
|
|
1322
1519
|
|
|
1520
|
+
private func noteSelectionDidChange() {
|
|
1521
|
+
selectionRevision &+= 1
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
private func recordAuthorizedSelectionIfPossible() {
|
|
1525
|
+
guard editorId != 0 else {
|
|
1526
|
+
lastAuthorizedSelectedUtf16Range = nil
|
|
1527
|
+
return
|
|
1528
|
+
}
|
|
1529
|
+
let currentText = textStorage.string
|
|
1530
|
+
guard currentText.utf16.count == lastAuthorizedTextStorage.length,
|
|
1531
|
+
currentText == lastAuthorizedText
|
|
1532
|
+
else {
|
|
1533
|
+
return
|
|
1534
|
+
}
|
|
1535
|
+
lastAuthorizedSelectedUtf16Range = selectedUtf16Range()
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
private func scalarRange(forUtf16Range range: NSRange) -> (from: UInt32, to: UInt32) {
|
|
1539
|
+
let start = PositionBridge.utf16OffsetToScalar(range.location, in: self)
|
|
1540
|
+
let end = PositionBridge.utf16OffsetToScalar(NSMaxRange(range), in: self)
|
|
1541
|
+
return (from: min(start, end), to: max(start, end))
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1323
1544
|
private func scheduleDeferredImageSelection(for range: NSRange) {
|
|
1324
1545
|
pendingDeferredImageSelectionRange = range
|
|
1325
1546
|
pendingDeferredImageSelectionGeneration &+= 1
|
|
@@ -1410,6 +1631,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1410
1631
|
lastRenderAppliedPatchForTesting
|
|
1411
1632
|
}
|
|
1412
1633
|
|
|
1634
|
+
func authorizedTextForTesting() -> String {
|
|
1635
|
+
lastAuthorizedText
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1413
1638
|
func lastApplyUpdateTrace() -> ApplyUpdateTrace? {
|
|
1414
1639
|
lastApplyUpdateTraceForTesting
|
|
1415
1640
|
}
|
|
@@ -1612,6 +1837,12 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1612
1837
|
/// - initialHTML: Optional HTML to set as initial content.
|
|
1613
1838
|
func bindEditor(id: UInt64, initialHTML: String? = nil) {
|
|
1614
1839
|
ensureInternalTextViewDelegate()
|
|
1840
|
+
if editorId == id, initialHTML == nil {
|
|
1841
|
+
return
|
|
1842
|
+
}
|
|
1843
|
+
if editorId != id {
|
|
1844
|
+
discardTransientNativeInputForEditorRebind()
|
|
1845
|
+
}
|
|
1615
1846
|
editorId = id
|
|
1616
1847
|
|
|
1617
1848
|
if let html = initialHTML, !html.isEmpty {
|
|
@@ -1623,10 +1854,12 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1623
1854
|
let stateJSON = editorGetCurrentState(id: editorId)
|
|
1624
1855
|
applyUpdateJSON(stateJSON, notifyDelegate: false)
|
|
1625
1856
|
}
|
|
1857
|
+
replayDesiredInputTraitsIfNeeded()
|
|
1626
1858
|
}
|
|
1627
1859
|
|
|
1628
1860
|
/// Unbind from the current editor instance.
|
|
1629
1861
|
func unbindEditor() {
|
|
1862
|
+
discardTransientNativeInputForEditorRebind()
|
|
1630
1863
|
editorId = 0
|
|
1631
1864
|
}
|
|
1632
1865
|
|
|
@@ -1649,6 +1882,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1649
1882
|
super.insertText(text)
|
|
1650
1883
|
return
|
|
1651
1884
|
}
|
|
1885
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1652
1886
|
|
|
1653
1887
|
if markedTextReplacementScalarRange != nil || markedTextRange != nil {
|
|
1654
1888
|
let replacementRange = trackedMarkedTextReplacementRange()
|
|
@@ -1740,6 +1974,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1740
1974
|
super.deleteBackward()
|
|
1741
1975
|
return
|
|
1742
1976
|
}
|
|
1977
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1743
1978
|
|
|
1744
1979
|
if markedTextReplacementScalarRange != nil || markedTextRange != nil {
|
|
1745
1980
|
performTransientTextMutation {
|
|
@@ -1892,6 +2127,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1892
2127
|
guard !isApplyingRustState else { return }
|
|
1893
2128
|
guard editorId != 0 else { return }
|
|
1894
2129
|
guard isEditable else { return }
|
|
2130
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1895
2131
|
guard isCaretInsideList() else { return }
|
|
1896
2132
|
guard let selection = currentScalarSelection() else { return }
|
|
1897
2133
|
|
|
@@ -1943,6 +2179,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1943
2179
|
super.replace(range, withText: text)
|
|
1944
2180
|
return
|
|
1945
2181
|
}
|
|
2182
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1946
2183
|
|
|
1947
2184
|
if markedTextReplacementScalarRange != nil || markedTextRange != nil {
|
|
1948
2185
|
let replacementRange = trackedMarkedTextReplacementRange()
|
|
@@ -1978,21 +2215,36 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1978
2215
|
override func setMarkedText(_ markedText: String?, selectedRange: NSRange) {
|
|
1979
2216
|
ensureInternalTextViewDelegate()
|
|
1980
2217
|
if markedText != nil {
|
|
2218
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1981
2219
|
captureMarkedTextReplacementRangeIfNeeded()
|
|
2220
|
+
} else if markedTextReplacementScalarRange == nil, markedTextRange == nil {
|
|
2221
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1982
2222
|
}
|
|
1983
2223
|
isComposing = markedText != nil || markedTextReplacementScalarRange != nil
|
|
1984
2224
|
Self.inputLog.debug(
|
|
1985
2225
|
"[setMarkedText] marked=\(self.preview(markedText ?? ""), privacy: .public) nsRange=\(selectedRange.location),\(selectedRange.length) selection=\(self.selectionSummary(), privacy: .public)"
|
|
1986
2226
|
)
|
|
1987
|
-
performTransientTextMutation {
|
|
1988
|
-
super.setMarkedText(markedText, selectedRange: selectedRange)
|
|
1989
|
-
}
|
|
1990
2227
|
if markedText == nil {
|
|
2228
|
+
// Some keyboard paths finalize composition by clearing marked text
|
|
2229
|
+
// instead of calling unmarkText().
|
|
2230
|
+
let composedText = validatedTrackedMarkedTextForCommit()
|
|
2231
|
+
let replacementRange = trackedMarkedTextReplacementRange()
|
|
2232
|
+
performTransientTextMutation {
|
|
2233
|
+
super.setMarkedText(nil, selectedRange: selectedRange)
|
|
2234
|
+
}
|
|
1991
2235
|
clearMarkedTextTracking()
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2236
|
+
if shouldCommitMarkedText(composedText, replacementRange: replacementRange) {
|
|
2237
|
+
commitMarkedText(composedText ?? "", replacementRange: replacementRange)
|
|
2238
|
+
} else {
|
|
2239
|
+
restoreAuthorizedTextAfterCancelledCompositionIfNeeded()
|
|
2240
|
+
}
|
|
2241
|
+
return
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
performTransientTextMutation {
|
|
2245
|
+
super.setMarkedText(markedText, selectedRange: selectedRange)
|
|
1995
2246
|
}
|
|
2247
|
+
refreshMarkedTextCompositionText(fallback: markedText)
|
|
1996
2248
|
}
|
|
1997
2249
|
|
|
1998
2250
|
/// Called when composition is finalized (user selects a candidate or
|
|
@@ -2013,6 +2265,8 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2013
2265
|
"[unmarkText] composed=\(self.preview(composed), privacy: .public) replacement=\(self.previewMarkedTextReplacementRange(replacementRange), privacy: .public) selection=\(self.selectionSummary(), privacy: .public)"
|
|
2014
2266
|
)
|
|
2015
2267
|
commitMarkedText(composed, replacementRange: replacementRange)
|
|
2268
|
+
} else if shouldCommitMarkedText(composedText, replacementRange: replacementRange) {
|
|
2269
|
+
commitMarkedText(composedText ?? "", replacementRange: replacementRange)
|
|
2016
2270
|
} else {
|
|
2017
2271
|
restoreAuthorizedTextAfterCancelledCompositionIfNeeded()
|
|
2018
2272
|
}
|
|
@@ -2023,9 +2277,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2023
2277
|
|
|
2024
2278
|
guard let selectedRange = selectedTextRange else {
|
|
2025
2279
|
let scalarPos = PositionBridge.cursorScalarOffset(in: self)
|
|
2280
|
+
let utf16Pos = PositionBridge.scalarToUtf16Offset(scalarPos, in: lastAuthorizedText)
|
|
2026
2281
|
markedTextReplacementScalarRange = (from: scalarPos, to: scalarPos)
|
|
2027
2282
|
markedTextReplacementUtf16Range = NSRange(
|
|
2028
|
-
location:
|
|
2283
|
+
location: utf16Pos,
|
|
2029
2284
|
length: 0
|
|
2030
2285
|
)
|
|
2031
2286
|
return
|
|
@@ -2055,6 +2310,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2055
2310
|
markedTextReplacementScalarRange = nil
|
|
2056
2311
|
markedTextReplacementUtf16Range = nil
|
|
2057
2312
|
markedTextCompositionText = nil
|
|
2313
|
+
markedTextCompositionIsExplicitlyEmpty = false
|
|
2058
2314
|
isComposing = false
|
|
2059
2315
|
}
|
|
2060
2316
|
|
|
@@ -2073,14 +2329,30 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2073
2329
|
}
|
|
2074
2330
|
|
|
2075
2331
|
private func currentMarkedTextForCommit() -> String? {
|
|
2076
|
-
|
|
2332
|
+
if markedTextCompositionIsExplicitlyEmpty { return "" }
|
|
2333
|
+
return transientMarkedTextFromAuthorizedDiff()
|
|
2334
|
+
?? markedTextRange.flatMap { text(in: $0) }
|
|
2077
2335
|
?? markedTextCompositionText
|
|
2078
|
-
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
private func validatedTrackedMarkedTextForCommit() -> String? {
|
|
2339
|
+
guard markedTextReplacementScalarRange != nil || markedTextReplacementUtf16Range != nil else {
|
|
2340
|
+
return nil
|
|
2341
|
+
}
|
|
2342
|
+
if markedTextCompositionIsExplicitlyEmpty { return "" }
|
|
2343
|
+
return transientMarkedTextFromAuthorizedDiff()
|
|
2344
|
+
?? markedTextRange.flatMap { text(in: $0) }
|
|
2079
2345
|
}
|
|
2080
2346
|
|
|
2081
2347
|
private func refreshMarkedTextCompositionText(fallback: String? = nil) {
|
|
2082
|
-
|
|
2083
|
-
|
|
2348
|
+
if fallback?.isEmpty == true {
|
|
2349
|
+
markedTextCompositionText = ""
|
|
2350
|
+
markedTextCompositionIsExplicitlyEmpty = true
|
|
2351
|
+
return
|
|
2352
|
+
}
|
|
2353
|
+
markedTextCompositionIsExplicitlyEmpty = false
|
|
2354
|
+
markedTextCompositionText = transientMarkedTextFromAuthorizedDiff()
|
|
2355
|
+
?? markedTextRange.flatMap { text(in: $0) }
|
|
2084
2356
|
?? fallback
|
|
2085
2357
|
}
|
|
2086
2358
|
|
|
@@ -2103,6 +2375,27 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2103
2375
|
return nil
|
|
2104
2376
|
}
|
|
2105
2377
|
|
|
2378
|
+
if replacementRange.location > 0 {
|
|
2379
|
+
let prefixRange = NSRange(location: 0, length: replacementRange.location)
|
|
2380
|
+
guard currentText.substring(with: prefixRange) == authorizedText.substring(with: prefixRange) else {
|
|
2381
|
+
return nil
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
let insertedEnd = replacementRange.location + insertedLength
|
|
2386
|
+
let authorizedSuffixLength = authorizedText.length - replacementEnd
|
|
2387
|
+
let currentSuffixLength = currentText.length - insertedEnd
|
|
2388
|
+
guard authorizedSuffixLength == currentSuffixLength else { return nil }
|
|
2389
|
+
if authorizedSuffixLength > 0 {
|
|
2390
|
+
let authorizedSuffixRange = NSRange(location: replacementEnd, length: authorizedSuffixLength)
|
|
2391
|
+
let currentSuffixRange = NSRange(location: insertedEnd, length: currentSuffixLength)
|
|
2392
|
+
guard currentText.substring(with: currentSuffixRange)
|
|
2393
|
+
== authorizedText.substring(with: authorizedSuffixRange)
|
|
2394
|
+
else {
|
|
2395
|
+
return nil
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2106
2399
|
return currentText.substring(
|
|
2107
2400
|
with: NSRange(location: replacementRange.location, length: insertedLength)
|
|
2108
2401
|
)
|
|
@@ -2114,13 +2407,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2114
2407
|
) {
|
|
2115
2408
|
guard editorId != 0 else { return }
|
|
2116
2409
|
guard let replacementRange else {
|
|
2117
|
-
performInterceptedInput {
|
|
2410
|
+
performInterceptedInput(flushPendingNativeTextMutation: false) {
|
|
2118
2411
|
insertTextInRust(text, at: PositionBridge.cursorScalarOffset(in: self))
|
|
2119
2412
|
}
|
|
2120
2413
|
return
|
|
2121
2414
|
}
|
|
2122
2415
|
|
|
2123
|
-
performInterceptedInput {
|
|
2416
|
+
performInterceptedInput(flushPendingNativeTextMutation: false) {
|
|
2124
2417
|
if replacementRange.from == replacementRange.to {
|
|
2125
2418
|
insertTextInRust(text, at: replacementRange.from)
|
|
2126
2419
|
} else {
|
|
@@ -2133,6 +2426,16 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2133
2426
|
}
|
|
2134
2427
|
}
|
|
2135
2428
|
|
|
2429
|
+
private func shouldCommitMarkedText(
|
|
2430
|
+
_ text: String?,
|
|
2431
|
+
replacementRange: (from: UInt32, to: UInt32)?
|
|
2432
|
+
) -> Bool {
|
|
2433
|
+
guard let text else { return false }
|
|
2434
|
+
if !text.isEmpty { return true }
|
|
2435
|
+
guard let replacementRange else { return false }
|
|
2436
|
+
return replacementRange.from != replacementRange.to
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2136
2439
|
private func restoreAuthorizedTextAfterCancelledCompositionIfNeeded() {
|
|
2137
2440
|
guard editorId != 0 else { return }
|
|
2138
2441
|
guard textStorage.string != lastAuthorizedText else { return }
|
|
@@ -2163,6 +2466,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2163
2466
|
super.paste(sender)
|
|
2164
2467
|
return
|
|
2165
2468
|
}
|
|
2469
|
+
guard prepareForExternalEditorUpdate() else { return }
|
|
2166
2470
|
|
|
2167
2471
|
Self.inputLog.debug(
|
|
2168
2472
|
"[paste] selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
@@ -2192,7 +2496,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2192
2496
|
documentAttributes: [.documentType: NSAttributedString.DocumentType.html]
|
|
2193
2497
|
), let html = String(data: htmlData, encoding: .utf8) {
|
|
2194
2498
|
performInterceptedInput {
|
|
2195
|
-
pasteHTML(html)
|
|
2499
|
+
if !pasteHTML(html, detectContentChange: true),
|
|
2500
|
+
!attrStr.string.isEmpty {
|
|
2501
|
+
pastePlainText(attrStr.string)
|
|
2502
|
+
}
|
|
2196
2503
|
}
|
|
2197
2504
|
return
|
|
2198
2505
|
}
|
|
@@ -2218,10 +2525,18 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2218
2525
|
func textViewDidChangeSelection(_ textView: UITextView) {
|
|
2219
2526
|
guard textView === self else { return }
|
|
2220
2527
|
ensureInternalTextViewDelegate()
|
|
2221
|
-
|
|
2528
|
+
noteSelectionDidChange()
|
|
2529
|
+
guard !isApplyingRustState,
|
|
2530
|
+
!isComposing,
|
|
2531
|
+
!nativeTextMutationCommitScheduled,
|
|
2532
|
+
pendingNativeTextMutation == nil
|
|
2533
|
+
else {
|
|
2534
|
+
return
|
|
2535
|
+
}
|
|
2222
2536
|
if normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded() {
|
|
2223
2537
|
return
|
|
2224
2538
|
}
|
|
2539
|
+
recordAuthorizedSelectionIfPossible()
|
|
2225
2540
|
refreshNativeSelectionChromeVisibility()
|
|
2226
2541
|
onSelectionOrContentMayChange?()
|
|
2227
2542
|
scheduleSelectionSync()
|
|
@@ -2258,6 +2573,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2258
2573
|
}
|
|
2259
2574
|
|
|
2260
2575
|
selectedTextRange = textRange(from: start, to: end)
|
|
2576
|
+
noteSelectionDidChange()
|
|
2261
2577
|
refreshNativeSelectionChromeVisibility()
|
|
2262
2578
|
onSelectionOrContentMayChange?()
|
|
2263
2579
|
scheduleSelectionSync()
|
|
@@ -2278,7 +2594,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2278
2594
|
delegate = self
|
|
2279
2595
|
}
|
|
2280
2596
|
|
|
2281
|
-
private func performInterceptedInput(
|
|
2597
|
+
private func performInterceptedInput(
|
|
2598
|
+
flushPendingNativeTextMutation: Bool = true,
|
|
2599
|
+
_ action: () -> Void
|
|
2600
|
+
) {
|
|
2601
|
+
if flushPendingNativeTextMutation, interceptedInputDepth == 0 {
|
|
2602
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
2603
|
+
}
|
|
2282
2604
|
interceptedInputDepth += 1
|
|
2283
2605
|
Self.inputLog.debug(
|
|
2284
2606
|
"[intercept.begin] depth=\(self.interceptedInputDepth) selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
@@ -2290,6 +2612,12 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2290
2612
|
Self.inputLog.debug(
|
|
2291
2613
|
"[intercept.end] depth=\(self.interceptedInputDepth) selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
2292
2614
|
)
|
|
2615
|
+
if self.interceptedInputDepth == 0 {
|
|
2616
|
+
_ = self.drainPendingNativeTextMutation(
|
|
2617
|
+
allowAfterBlur: false,
|
|
2618
|
+
allowWhileIntercepting: false
|
|
2619
|
+
)
|
|
2620
|
+
}
|
|
2293
2621
|
}
|
|
2294
2622
|
}
|
|
2295
2623
|
|
|
@@ -2402,6 +2730,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2402
2730
|
guard !isApplyingRustState,
|
|
2403
2731
|
!isComposing,
|
|
2404
2732
|
!nativeTextMutationCommitScheduled,
|
|
2733
|
+
pendingNativeTextMutation == nil,
|
|
2405
2734
|
editorId != 0
|
|
2406
2735
|
else {
|
|
2407
2736
|
return
|
|
@@ -2417,13 +2746,16 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2417
2746
|
)
|
|
2418
2747
|
|
|
2419
2748
|
editorSetSelectionScalar(id: editorId, scalarAnchor: anchor, scalarHead: head)
|
|
2749
|
+
recordAuthorizedSelectionIfPossible()
|
|
2420
2750
|
refreshTypingAttributesForSelection()
|
|
2421
2751
|
editorDelegate?.editorTextView(self, selectionDidChange: docAnchor, head: docHead)
|
|
2422
2752
|
}
|
|
2423
2753
|
|
|
2424
|
-
|
|
2425
|
-
|
|
2754
|
+
@discardableResult
|
|
2755
|
+
func applyTheme(_ theme: EditorTheme?) -> Bool {
|
|
2426
2756
|
if editorId != 0 {
|
|
2757
|
+
guard prepareForExternalEditorUpdate() else { return false }
|
|
2758
|
+
self.theme = theme
|
|
2427
2759
|
let previousOffset = contentOffset
|
|
2428
2760
|
let stateJSON = editorGetCurrentState(id: editorId)
|
|
2429
2761
|
applyUpdateJSON(stateJSON, notifyDelegate: false)
|
|
@@ -2431,11 +2763,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2431
2763
|
preserveScrollOffset(previousOffset)
|
|
2432
2764
|
}
|
|
2433
2765
|
} else {
|
|
2766
|
+
self.theme = theme
|
|
2434
2767
|
refreshTypingAttributesForSelection()
|
|
2435
2768
|
}
|
|
2436
2769
|
if heightBehavior == .autoGrow {
|
|
2437
2770
|
notifyHeightChangeIfNeeded(force: true)
|
|
2438
2771
|
}
|
|
2772
|
+
return true
|
|
2439
2773
|
}
|
|
2440
2774
|
|
|
2441
2775
|
private func preserveScrollOffset(_ previousOffset: CGPoint) {
|
|
@@ -2871,8 +3205,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2871
3205
|
}
|
|
2872
3206
|
|
|
2873
3207
|
func performToolbarToggleMark(_ markName: String) {
|
|
2874
|
-
guard
|
|
2875
|
-
guard isEditable else { return }
|
|
3208
|
+
guard prepareForToolbarCommand() else { return }
|
|
2876
3209
|
guard let selection = currentScalarSelection() else { return }
|
|
2877
3210
|
performInterceptedInput {
|
|
2878
3211
|
let updateJSON = editorToggleMarkAtSelectionScalar(
|
|
@@ -2886,8 +3219,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2886
3219
|
}
|
|
2887
3220
|
|
|
2888
3221
|
func performToolbarToggleList(_ listType: String, isActive: Bool) {
|
|
2889
|
-
guard
|
|
2890
|
-
guard isEditable else { return }
|
|
3222
|
+
guard prepareForToolbarCommand() else { return }
|
|
2891
3223
|
guard let selection = currentScalarSelection() else { return }
|
|
2892
3224
|
performInterceptedInput {
|
|
2893
3225
|
let updateJSON = isActive
|
|
@@ -2907,8 +3239,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2907
3239
|
}
|
|
2908
3240
|
|
|
2909
3241
|
func performToolbarToggleBlockquote() {
|
|
2910
|
-
guard
|
|
2911
|
-
guard isEditable else { return }
|
|
3242
|
+
guard prepareForToolbarCommand() else { return }
|
|
2912
3243
|
guard let selection = currentScalarSelection() else { return }
|
|
2913
3244
|
performInterceptedInput {
|
|
2914
3245
|
let updateJSON = editorToggleBlockquoteAtSelectionScalar(
|
|
@@ -2921,8 +3252,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2921
3252
|
}
|
|
2922
3253
|
|
|
2923
3254
|
func performToolbarToggleHeading(_ level: Int) {
|
|
2924
|
-
guard
|
|
2925
|
-
guard isEditable else { return }
|
|
3255
|
+
guard prepareForToolbarCommand() else { return }
|
|
2926
3256
|
guard let selection = currentScalarSelection() else { return }
|
|
2927
3257
|
guard let level = UInt8(exactly: level), (1...6).contains(level) else { return }
|
|
2928
3258
|
performInterceptedInput {
|
|
@@ -2937,8 +3267,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2937
3267
|
}
|
|
2938
3268
|
|
|
2939
3269
|
func performToolbarIndentListItem() {
|
|
2940
|
-
guard
|
|
2941
|
-
guard isEditable else { return }
|
|
3270
|
+
guard prepareForToolbarCommand() else { return }
|
|
2942
3271
|
guard let selection = currentScalarSelection() else { return }
|
|
2943
3272
|
performInterceptedInput {
|
|
2944
3273
|
let updateJSON = editorIndentListItemAtSelectionScalar(
|
|
@@ -2951,8 +3280,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2951
3280
|
}
|
|
2952
3281
|
|
|
2953
3282
|
func performToolbarOutdentListItem() {
|
|
2954
|
-
guard
|
|
2955
|
-
guard isEditable else { return }
|
|
3283
|
+
guard prepareForToolbarCommand() else { return }
|
|
2956
3284
|
guard let selection = currentScalarSelection() else { return }
|
|
2957
3285
|
performInterceptedInput {
|
|
2958
3286
|
let updateJSON = editorOutdentListItemAtSelectionScalar(
|
|
@@ -2965,16 +3293,14 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2965
3293
|
}
|
|
2966
3294
|
|
|
2967
3295
|
func performToolbarInsertNode(_ nodeType: String) {
|
|
2968
|
-
guard
|
|
2969
|
-
guard isEditable else { return }
|
|
3296
|
+
guard prepareForToolbarCommand() else { return }
|
|
2970
3297
|
performInterceptedInput {
|
|
2971
3298
|
insertNodeInRust(nodeType)
|
|
2972
3299
|
}
|
|
2973
3300
|
}
|
|
2974
3301
|
|
|
2975
3302
|
func performToolbarUndo() {
|
|
2976
|
-
guard
|
|
2977
|
-
guard isEditable else { return }
|
|
3303
|
+
guard prepareForToolbarCommand() else { return }
|
|
2978
3304
|
performInterceptedInput {
|
|
2979
3305
|
let updateJSON = editorUndo(id: editorId)
|
|
2980
3306
|
applyUpdateJSON(updateJSON)
|
|
@@ -2982,14 +3308,19 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2982
3308
|
}
|
|
2983
3309
|
|
|
2984
3310
|
func performToolbarRedo() {
|
|
2985
|
-
guard
|
|
2986
|
-
guard isEditable else { return }
|
|
3311
|
+
guard prepareForToolbarCommand() else { return }
|
|
2987
3312
|
performInterceptedInput {
|
|
2988
3313
|
let updateJSON = editorRedo(id: editorId)
|
|
2989
3314
|
applyUpdateJSON(updateJSON)
|
|
2990
3315
|
}
|
|
2991
3316
|
}
|
|
2992
3317
|
|
|
3318
|
+
private func prepareForToolbarCommand() -> Bool {
|
|
3319
|
+
guard editorId != 0 else { return false }
|
|
3320
|
+
guard isEditable else { return false }
|
|
3321
|
+
return prepareForExternalEditorUpdate()
|
|
3322
|
+
}
|
|
3323
|
+
|
|
2993
3324
|
/// Insert text at a scalar position via the Rust editor.
|
|
2994
3325
|
private func insertTextInRust(_ text: String, at scalarPos: UInt32) {
|
|
2995
3326
|
Self.inputLog.debug(
|
|
@@ -3026,6 +3357,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3026
3357
|
authorized.character(at: prefix) == current.character(at: prefix) {
|
|
3027
3358
|
prefix += 1
|
|
3028
3359
|
}
|
|
3360
|
+
prefix = sharedUtf16ScalarBoundary(atOrBefore: prefix, in: authorizedText, and: currentText)
|
|
3029
3361
|
|
|
3030
3362
|
var authorizedEnd = authorized.length
|
|
3031
3363
|
var currentEnd = current.length
|
|
@@ -3035,6 +3367,8 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3035
3367
|
authorizedEnd -= 1
|
|
3036
3368
|
currentEnd -= 1
|
|
3037
3369
|
}
|
|
3370
|
+
authorizedEnd = utf16ScalarBoundary(atOrAfter: authorizedEnd, in: authorizedText)
|
|
3371
|
+
currentEnd = utf16ScalarBoundary(atOrAfter: currentEnd, in: currentText)
|
|
3038
3372
|
|
|
3039
3373
|
let replacementLength = currentEnd - prefix
|
|
3040
3374
|
guard replacementLength >= 0 else { return nil }
|
|
@@ -3042,16 +3376,378 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3042
3376
|
with: NSRange(location: prefix, length: replacementLength)
|
|
3043
3377
|
)
|
|
3044
3378
|
|
|
3379
|
+
let rawSelectionUtf16Range = selectedUtf16Range()
|
|
3380
|
+
let authorizedSelectionUtf16Range = lastAuthorizedSelectedUtf16Range
|
|
3381
|
+
let targetSelectionUtf16Range = targetSelectionUtf16RangeForNativeTextMutation(
|
|
3382
|
+
rawSelectionUtf16Range: rawSelectionUtf16Range,
|
|
3383
|
+
authorizedSelectionUtf16Range: authorizedSelectionUtf16Range,
|
|
3384
|
+
replacementStartUtf16: prefix,
|
|
3385
|
+
authorizedEndUtf16: authorizedEnd,
|
|
3386
|
+
currentEndUtf16: currentEnd,
|
|
3387
|
+
currentTextUtf16Length: current.length
|
|
3388
|
+
)
|
|
3389
|
+
let selectedScalarRange = targetSelectionUtf16Range.map(scalarRange(forUtf16Range:))
|
|
3390
|
+
let capturedAfterBlur = canAdoptNativeTextMutationAfterBlur()
|
|
3391
|
+
|
|
3045
3392
|
return NativeTextMutation(
|
|
3046
|
-
from: PositionBridge.utf16OffsetToScalar(prefix, in:
|
|
3047
|
-
to: PositionBridge.utf16OffsetToScalar(authorizedEnd, in:
|
|
3393
|
+
from: PositionBridge.utf16OffsetToScalar(prefix, in: lastAuthorizedAttributedTextStorage),
|
|
3394
|
+
to: PositionBridge.utf16OffsetToScalar(authorizedEnd, in: lastAuthorizedAttributedTextStorage),
|
|
3048
3395
|
replacementText: replacementText,
|
|
3049
|
-
resultingText: currentText
|
|
3396
|
+
resultingText: currentText,
|
|
3397
|
+
authorizedText: authorizedText,
|
|
3398
|
+
selectionAnchor: selectedScalarRange?.from,
|
|
3399
|
+
selectionHead: selectedScalarRange?.to,
|
|
3400
|
+
authorizedSelectionUtf16Range: authorizedSelectionUtf16Range,
|
|
3401
|
+
rawSelectionUtf16Range: rawSelectionUtf16Range,
|
|
3402
|
+
selectionRevision: selectionRevision,
|
|
3403
|
+
capturedWhileFirstResponder: isFirstResponder || capturedAfterBlur,
|
|
3404
|
+
capturedWhileEditable: isEditable,
|
|
3405
|
+
capturedAfterBlur: capturedAfterBlur,
|
|
3406
|
+
inputGeneration: nativeTextMutationGeneration
|
|
3050
3407
|
)
|
|
3051
3408
|
}
|
|
3052
3409
|
|
|
3053
|
-
private func
|
|
3054
|
-
|
|
3410
|
+
private func nativeTextMutationWithCurrentSelection(
|
|
3411
|
+
_ mutation: NativeTextMutation
|
|
3412
|
+
) -> NativeTextMutation {
|
|
3413
|
+
let currentSelectionUtf16Range = selectedUtf16Range()
|
|
3414
|
+
let didSelectionChangeAfterCapture = selectionRevision != mutation.selectionRevision
|
|
3415
|
+
let didCurrentRangeMoveAfterCapture: Bool
|
|
3416
|
+
if let currentSelectionUtf16Range,
|
|
3417
|
+
let rawSelectionUtf16Range = mutation.rawSelectionUtf16Range {
|
|
3418
|
+
didCurrentRangeMoveAfterCapture = !NSEqualRanges(
|
|
3419
|
+
currentSelectionUtf16Range,
|
|
3420
|
+
rawSelectionUtf16Range
|
|
3421
|
+
)
|
|
3422
|
+
} else {
|
|
3423
|
+
didCurrentRangeMoveAfterCapture = false
|
|
3424
|
+
}
|
|
3425
|
+
let currentSelectionDiffersFromAuthorized: Bool
|
|
3426
|
+
if let currentSelectionUtf16Range,
|
|
3427
|
+
let authorizedSelectionUtf16Range = mutation.authorizedSelectionUtf16Range {
|
|
3428
|
+
currentSelectionDiffersFromAuthorized = !NSEqualRanges(
|
|
3429
|
+
currentSelectionUtf16Range,
|
|
3430
|
+
authorizedSelectionUtf16Range
|
|
3431
|
+
)
|
|
3432
|
+
} else {
|
|
3433
|
+
currentSelectionDiffersFromAuthorized = currentSelectionUtf16Range != nil
|
|
3434
|
+
}
|
|
3435
|
+
let shouldUseCurrentSelection = currentSelectionUtf16Range != nil
|
|
3436
|
+
&& (
|
|
3437
|
+
(didSelectionChangeAfterCapture && currentSelectionDiffersFromAuthorized)
|
|
3438
|
+
|| didCurrentRangeMoveAfterCapture
|
|
3439
|
+
|| mutation.rawSelectionUtf16Range == nil
|
|
3440
|
+
)
|
|
3441
|
+
let selectedScalarRange = shouldUseCurrentSelection
|
|
3442
|
+
? currentSelectionUtf16Range.map(scalarRange(forUtf16Range:))
|
|
3443
|
+
: nil
|
|
3444
|
+
return NativeTextMutation(
|
|
3445
|
+
from: mutation.from,
|
|
3446
|
+
to: mutation.to,
|
|
3447
|
+
replacementText: mutation.replacementText,
|
|
3448
|
+
resultingText: mutation.resultingText,
|
|
3449
|
+
authorizedText: mutation.authorizedText,
|
|
3450
|
+
selectionAnchor: selectedScalarRange?.from ?? mutation.selectionAnchor,
|
|
3451
|
+
selectionHead: selectedScalarRange?.to ?? mutation.selectionHead,
|
|
3452
|
+
authorizedSelectionUtf16Range: mutation.authorizedSelectionUtf16Range,
|
|
3453
|
+
rawSelectionUtf16Range: shouldUseCurrentSelection
|
|
3454
|
+
? currentSelectionUtf16Range
|
|
3455
|
+
: mutation.rawSelectionUtf16Range,
|
|
3456
|
+
selectionRevision: shouldUseCurrentSelection
|
|
3457
|
+
? selectionRevision
|
|
3458
|
+
: mutation.selectionRevision,
|
|
3459
|
+
capturedWhileFirstResponder: mutation.capturedWhileFirstResponder,
|
|
3460
|
+
capturedWhileEditable: mutation.capturedWhileEditable,
|
|
3461
|
+
capturedAfterBlur: mutation.capturedAfterBlur,
|
|
3462
|
+
inputGeneration: mutation.inputGeneration
|
|
3463
|
+
)
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
private func targetSelectionUtf16RangeForNativeTextMutation(
|
|
3467
|
+
rawSelectionUtf16Range: NSRange?,
|
|
3468
|
+
authorizedSelectionUtf16Range: NSRange?,
|
|
3469
|
+
replacementStartUtf16: Int,
|
|
3470
|
+
authorizedEndUtf16: Int,
|
|
3471
|
+
currentEndUtf16: Int,
|
|
3472
|
+
currentTextUtf16Length: Int
|
|
3473
|
+
) -> NSRange? {
|
|
3474
|
+
guard let authorizedSelection = authorizedSelectionUtf16Range else {
|
|
3475
|
+
return clampedUtf16Range(rawSelectionUtf16Range, length: currentTextUtf16Length)
|
|
3476
|
+
}
|
|
3477
|
+
guard authorizedSelection.location != NSNotFound else {
|
|
3478
|
+
return clampedUtf16Range(rawSelectionUtf16Range, length: currentTextUtf16Length)
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
if let rawSelection = rawSelectionUtf16Range,
|
|
3482
|
+
rawSelection.location != NSNotFound,
|
|
3483
|
+
!NSEqualRanges(rawSelection, authorizedSelection) {
|
|
3484
|
+
return clampedUtf16Range(rawSelection, length: currentTextUtf16Length)
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
if authorizedSelection.length == 0 {
|
|
3488
|
+
let mappedOffset = mapCollapsedAuthorizedSelectionOffsetThroughNativeTextMutation(
|
|
3489
|
+
authorizedSelection.location,
|
|
3490
|
+
replacementStartUtf16: replacementStartUtf16,
|
|
3491
|
+
authorizedEndUtf16: authorizedEndUtf16,
|
|
3492
|
+
currentEndUtf16: currentEndUtf16
|
|
3493
|
+
)
|
|
3494
|
+
let clampedOffset = min(max(mappedOffset, 0), currentTextUtf16Length)
|
|
3495
|
+
return NSRange(location: clampedOffset, length: 0)
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
let mappedStart = mapAuthorizedSelectionOffsetThroughNativeTextMutation(
|
|
3499
|
+
authorizedSelection.location,
|
|
3500
|
+
replacementStartUtf16: replacementStartUtf16,
|
|
3501
|
+
authorizedEndUtf16: authorizedEndUtf16,
|
|
3502
|
+
currentEndUtf16: currentEndUtf16,
|
|
3503
|
+
isRangeStart: true
|
|
3504
|
+
)
|
|
3505
|
+
let mappedEnd = mapAuthorizedSelectionOffsetThroughNativeTextMutation(
|
|
3506
|
+
NSMaxRange(authorizedSelection),
|
|
3507
|
+
replacementStartUtf16: replacementStartUtf16,
|
|
3508
|
+
authorizedEndUtf16: authorizedEndUtf16,
|
|
3509
|
+
currentEndUtf16: currentEndUtf16,
|
|
3510
|
+
isRangeStart: false
|
|
3511
|
+
)
|
|
3512
|
+
let start = min(mappedStart, mappedEnd)
|
|
3513
|
+
let end = max(mappedStart, mappedEnd)
|
|
3514
|
+
let clampedStart = min(max(start, 0), currentTextUtf16Length)
|
|
3515
|
+
let clampedEnd = min(max(end, 0), currentTextUtf16Length)
|
|
3516
|
+
return NSRange(location: clampedStart, length: max(0, clampedEnd - clampedStart))
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
private func clampedUtf16Range(_ range: NSRange?, length: Int) -> NSRange? {
|
|
3520
|
+
guard let range, range.location != NSNotFound else { return nil }
|
|
3521
|
+
let start = min(max(range.location, 0), length)
|
|
3522
|
+
let end = min(max(NSMaxRange(range), 0), length)
|
|
3523
|
+
return NSRange(location: min(start, end), length: abs(end - start))
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
private func mapCollapsedAuthorizedSelectionOffsetThroughNativeTextMutation(
|
|
3527
|
+
_ offset: Int,
|
|
3528
|
+
replacementStartUtf16: Int,
|
|
3529
|
+
authorizedEndUtf16: Int,
|
|
3530
|
+
currentEndUtf16: Int
|
|
3531
|
+
) -> Int {
|
|
3532
|
+
// UIKit can leave a stale caret at the insertion point during autocomplete.
|
|
3533
|
+
// A collapsed authorized caret should stay collapsed after the inserted text.
|
|
3534
|
+
if replacementStartUtf16 == authorizedEndUtf16,
|
|
3535
|
+
offset == replacementStartUtf16,
|
|
3536
|
+
currentEndUtf16 > replacementStartUtf16 {
|
|
3537
|
+
return currentEndUtf16
|
|
3538
|
+
}
|
|
3539
|
+
if offset <= replacementStartUtf16 {
|
|
3540
|
+
return offset
|
|
3541
|
+
}
|
|
3542
|
+
if offset < authorizedEndUtf16 {
|
|
3543
|
+
return currentEndUtf16
|
|
3544
|
+
}
|
|
3545
|
+
return offset + currentEndUtf16 - authorizedEndUtf16
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
private func mapAuthorizedSelectionOffsetThroughNativeTextMutation(
|
|
3549
|
+
_ offset: Int,
|
|
3550
|
+
replacementStartUtf16: Int,
|
|
3551
|
+
authorizedEndUtf16: Int,
|
|
3552
|
+
currentEndUtf16: Int,
|
|
3553
|
+
isRangeStart: Bool
|
|
3554
|
+
) -> Int {
|
|
3555
|
+
if offset <= replacementStartUtf16 {
|
|
3556
|
+
return offset
|
|
3557
|
+
}
|
|
3558
|
+
if offset >= authorizedEndUtf16 {
|
|
3559
|
+
return offset + currentEndUtf16 - authorizedEndUtf16
|
|
3560
|
+
}
|
|
3561
|
+
return isRangeStart ? replacementStartUtf16 : currentEndUtf16
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
private func isUtf16ScalarBoundary(_ offset: Int, in text: String) -> Bool {
|
|
3565
|
+
guard offset >= 0, offset <= text.utf16.count else { return false }
|
|
3566
|
+
let utf16Index = text.utf16.index(text.utf16.startIndex, offsetBy: offset)
|
|
3567
|
+
return String.Index(utf16Index, within: text) != nil
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
private func utf16ScalarBoundary(atOrBefore offset: Int, in text: String) -> Int {
|
|
3571
|
+
var candidate = min(max(offset, 0), text.utf16.count)
|
|
3572
|
+
while candidate > 0, !isUtf16ScalarBoundary(candidate, in: text) {
|
|
3573
|
+
candidate -= 1
|
|
3574
|
+
}
|
|
3575
|
+
return candidate
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
private func utf16ScalarBoundary(atOrAfter offset: Int, in text: String) -> Int {
|
|
3579
|
+
var candidate = min(max(offset, 0), text.utf16.count)
|
|
3580
|
+
while candidate < text.utf16.count, !isUtf16ScalarBoundary(candidate, in: text) {
|
|
3581
|
+
candidate += 1
|
|
3582
|
+
}
|
|
3583
|
+
return candidate
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
private func sharedUtf16ScalarBoundary(atOrBefore offset: Int, in lhs: String, and rhs: String) -> Int {
|
|
3587
|
+
var candidate = min(max(offset, 0), lhs.utf16.count, rhs.utf16.count)
|
|
3588
|
+
while candidate > 0,
|
|
3589
|
+
(!isUtf16ScalarBoundary(candidate, in: lhs) || !isUtf16ScalarBoundary(candidate, in: rhs)) {
|
|
3590
|
+
candidate -= 1
|
|
3591
|
+
}
|
|
3592
|
+
return candidate
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
private func shouldAdoptNativeTextStorageMutation(
|
|
3596
|
+
_ mutation: NativeTextMutation,
|
|
3597
|
+
allowAfterBlur: Bool = false
|
|
3598
|
+
) -> Bool {
|
|
3599
|
+
if isFirstResponder && isEditable {
|
|
3600
|
+
return true
|
|
3601
|
+
}
|
|
3602
|
+
return allowAfterBlur
|
|
3603
|
+
&& mutation.capturedAfterBlur
|
|
3604
|
+
&& mutation.inputGeneration == nativeTextMutationGeneration
|
|
3605
|
+
&& canAdoptNativeTextMutationAfterBlur()
|
|
3606
|
+
&& mutation.capturedWhileFirstResponder
|
|
3607
|
+
&& mutation.capturedWhileEditable
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
private func canAdoptNativeTextMutationAfterBlur() -> Bool {
|
|
3611
|
+
guard let deadline = nativeTextMutationAfterBlurDeadline else {
|
|
3612
|
+
return false
|
|
3613
|
+
}
|
|
3614
|
+
guard nativeTextMutationAfterBlurGeneration == nativeTextMutationGeneration else {
|
|
3615
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3616
|
+
return false
|
|
3617
|
+
}
|
|
3618
|
+
guard ProcessInfo.processInfo.systemUptime <= deadline else {
|
|
3619
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3620
|
+
return false
|
|
3621
|
+
}
|
|
3622
|
+
return true
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
private func clearNativeTextMutationAfterBlurWindow() {
|
|
3626
|
+
nativeTextMutationAfterBlurDeadline = nil
|
|
3627
|
+
nativeTextMutationAfterBlurGeneration = nil
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
private func advanceNativeTextMutationGeneration() {
|
|
3631
|
+
nativeTextMutationGeneration &+= 1
|
|
3632
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
private func resetPendingNativeTextMutationState() {
|
|
3636
|
+
pendingNativeTextMutation = nil
|
|
3637
|
+
nativeTextMutationCommitScheduled = false
|
|
3638
|
+
advanceNativeTextMutationGeneration()
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
func expireNativeTextMutationAfterBlurDeadlineForTesting() {
|
|
3642
|
+
nativeTextMutationAfterBlurDeadline = ProcessInfo.processInfo.systemUptime - 0.001
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
func discardTransientNativeInputForEditorRebind() {
|
|
3646
|
+
resetPendingNativeTextMutationState()
|
|
3647
|
+
lastAuthorizedSelectedUtf16Range = nil
|
|
3648
|
+
clearPendingInputTraitRetry()
|
|
3649
|
+
markedTextReplacementScalarRange = nil
|
|
3650
|
+
markedTextReplacementUtf16Range = nil
|
|
3651
|
+
markedTextCompositionText = nil
|
|
3652
|
+
markedTextCompositionIsExplicitlyEmpty = false
|
|
3653
|
+
isComposing = false
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
@discardableResult
|
|
3657
|
+
private func flushPendingNativeTextMutationCommitIfNeeded() -> Bool {
|
|
3658
|
+
drainPendingNativeTextMutation(
|
|
3659
|
+
allowAfterBlur: false,
|
|
3660
|
+
allowWhileIntercepting: true
|
|
3661
|
+
)
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
@discardableResult
|
|
3665
|
+
func prepareForExternalEditorUpdate() -> Bool {
|
|
3666
|
+
guard prepareActiveCompositionForExternalMutation() else { return false }
|
|
3667
|
+
return drainPendingNativeTextMutation(
|
|
3668
|
+
allowAfterBlur: true,
|
|
3669
|
+
allowWhileIntercepting: true
|
|
3670
|
+
)
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
@discardableResult
|
|
3674
|
+
func prepareForExternalEditorCommand() -> (ready: Bool, updateJSON: String?, blockedReason: String?) {
|
|
3675
|
+
let previousEditorId = editorId
|
|
3676
|
+
let previousAuthorizedText = lastAuthorizedText
|
|
3677
|
+
let previousStateJSON = previousEditorId != 0 ? editorGetCurrentState(id: previousEditorId) : nil
|
|
3678
|
+
guard prepareForExternalEditorUpdate() else {
|
|
3679
|
+
return (false, nil, "composition")
|
|
3680
|
+
}
|
|
3681
|
+
guard editorId != 0 else {
|
|
3682
|
+
return (true, nil, nil)
|
|
3683
|
+
}
|
|
3684
|
+
let currentStateJSON = editorGetCurrentState(id: editorId)
|
|
3685
|
+
guard lastAuthorizedText != previousAuthorizedText
|
|
3686
|
+
|| previousEditorId != editorId
|
|
3687
|
+
|| previousStateJSON != currentStateJSON
|
|
3688
|
+
else {
|
|
3689
|
+
return (true, nil, nil)
|
|
3690
|
+
}
|
|
3691
|
+
return (true, currentStateJSON, nil)
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
private func prepareActiveCompositionForExternalMutation() -> Bool {
|
|
3695
|
+
guard isComposing else { return true }
|
|
3696
|
+
|
|
3697
|
+
let composedText = validatedTrackedMarkedTextForCommit()
|
|
3698
|
+
let replacementRange = trackedMarkedTextReplacementRange()
|
|
3699
|
+
finishTransientMarkedTextMutation()
|
|
3700
|
+
|
|
3701
|
+
guard shouldCommitMarkedText(composedText, replacementRange: replacementRange) else {
|
|
3702
|
+
restoreAuthorizedTextAfterCancelledCompositionIfNeeded()
|
|
3703
|
+
return false
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
commitMarkedText(composedText ?? "", replacementRange: replacementRange)
|
|
3707
|
+
return true
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
@discardableResult
|
|
3711
|
+
private func drainPendingNativeTextMutation(
|
|
3712
|
+
allowAfterBlur: Bool,
|
|
3713
|
+
allowWhileIntercepting: Bool
|
|
3714
|
+
) -> Bool {
|
|
3715
|
+
guard nativeTextMutationCommitScheduled
|
|
3716
|
+
|| pendingNativeTextMutation != nil
|
|
3717
|
+
|| (!isComposing && markedTextRange == nil && textStorage.string != lastAuthorizedText)
|
|
3718
|
+
else {
|
|
3719
|
+
return true
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
nativeTextMutationCommitScheduled = false
|
|
3723
|
+
let currentText = textStorage.string
|
|
3724
|
+
let mutation: NativeTextMutation?
|
|
3725
|
+
if let pendingNativeTextMutation,
|
|
3726
|
+
pendingNativeTextMutation.resultingText == currentText,
|
|
3727
|
+
pendingNativeTextMutation.authorizedText == lastAuthorizedText
|
|
3728
|
+
{
|
|
3729
|
+
mutation = nativeTextMutationWithCurrentSelection(pendingNativeTextMutation)
|
|
3730
|
+
} else {
|
|
3731
|
+
mutation = nativeTextMutationFromAuthorizedDiff(currentText: currentText)
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
guard let mutation else {
|
|
3735
|
+
pendingNativeTextMutation = nil
|
|
3736
|
+
return true
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
switch commitNativeTextMutationIfPossible(
|
|
3740
|
+
mutation,
|
|
3741
|
+
allowAfterBlur: allowAfterBlur,
|
|
3742
|
+
allowWhileIntercepting: allowWhileIntercepting
|
|
3743
|
+
) {
|
|
3744
|
+
case .committed, .rejected:
|
|
3745
|
+
pendingNativeTextMutation = nil
|
|
3746
|
+
return true
|
|
3747
|
+
case .deferred:
|
|
3748
|
+
pendingNativeTextMutation = mutation
|
|
3749
|
+
return false
|
|
3750
|
+
}
|
|
3055
3751
|
}
|
|
3056
3752
|
|
|
3057
3753
|
private func scheduleNativeTextMutationCommit(_ mutation: NativeTextMutation) {
|
|
@@ -3061,43 +3757,101 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3061
3757
|
nativeTextMutationCommitScheduled = true
|
|
3062
3758
|
DispatchQueue.main.async { [weak self] in
|
|
3063
3759
|
guard let self else { return }
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3760
|
+
_ = self.drainPendingNativeTextMutation(
|
|
3761
|
+
allowAfterBlur: true,
|
|
3762
|
+
allowWhileIntercepting: true
|
|
3763
|
+
)
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
@discardableResult
|
|
3768
|
+
private func commitNativeTextMutationIfPossible(
|
|
3769
|
+
_ mutation: NativeTextMutation,
|
|
3770
|
+
allowAfterBlur: Bool,
|
|
3771
|
+
allowWhileIntercepting: Bool
|
|
3772
|
+
) -> NativeTextMutationCommitResult {
|
|
3773
|
+
guard editorId != 0 else {
|
|
3774
|
+
return .rejected
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
guard !isApplyingRustState,
|
|
3778
|
+
(!isInterceptingInput || allowWhileIntercepting),
|
|
3779
|
+
!isComposing
|
|
3780
|
+
else {
|
|
3781
|
+
return .deferred
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
guard shouldAdoptNativeTextStorageMutation(mutation, allowAfterBlur: allowAfterBlur) else {
|
|
3785
|
+
if textStorage.string != lastAuthorizedText {
|
|
3786
|
+
scheduleReconciliationFromRust()
|
|
3078
3787
|
}
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3788
|
+
return .rejected
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
guard textStorage.string == mutation.resultingText else {
|
|
3792
|
+
if let refreshedMutation = nativeTextMutationFromAuthorizedDiff(currentText: textStorage.string) {
|
|
3793
|
+
return commitNativeTextMutationIfPossible(
|
|
3794
|
+
refreshedMutation,
|
|
3795
|
+
allowAfterBlur: allowAfterBlur,
|
|
3796
|
+
allowWhileIntercepting: allowWhileIntercepting
|
|
3797
|
+
)
|
|
3084
3798
|
}
|
|
3799
|
+
return .rejected
|
|
3800
|
+
}
|
|
3085
3801
|
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3802
|
+
guard mutation.authorizedText == lastAuthorizedText else {
|
|
3803
|
+
if let refreshedMutation = nativeTextMutationFromAuthorizedDiff(currentText: textStorage.string) {
|
|
3804
|
+
return commitNativeTextMutationIfPossible(
|
|
3805
|
+
refreshedMutation,
|
|
3806
|
+
allowAfterBlur: allowAfterBlur,
|
|
3807
|
+
allowWhileIntercepting: allowWhileIntercepting
|
|
3808
|
+
)
|
|
3809
|
+
}
|
|
3810
|
+
return .rejected
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
performInterceptedInput(flushPendingNativeTextMutation: false) {
|
|
3814
|
+
if mutation.from == mutation.to {
|
|
3815
|
+
guard !mutation.replacementText.isEmpty else { return }
|
|
3816
|
+
insertTextInRust(mutation.replacementText, at: mutation.from)
|
|
3817
|
+
} else if mutation.replacementText.isEmpty {
|
|
3818
|
+
deleteScalarRangeInRust(from: mutation.from, to: mutation.to)
|
|
3819
|
+
} else {
|
|
3820
|
+
replaceTextRangeInRust(
|
|
3821
|
+
from: mutation.from,
|
|
3822
|
+
to: mutation.to,
|
|
3823
|
+
with: mutation.replacementText
|
|
3824
|
+
)
|
|
3099
3825
|
}
|
|
3826
|
+
restoreNativeTextMutationSelectionIfNeeded(mutation)
|
|
3100
3827
|
}
|
|
3828
|
+
if mutation.capturedAfterBlur {
|
|
3829
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3830
|
+
}
|
|
3831
|
+
return .committed
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
private func restoreNativeTextMutationSelectionIfNeeded(_ mutation: NativeTextMutation) {
|
|
3835
|
+
guard let anchor = mutation.selectionAnchor,
|
|
3836
|
+
let head = mutation.selectionHead,
|
|
3837
|
+
editorId != 0
|
|
3838
|
+
else {
|
|
3839
|
+
return
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
let startUtf16 = PositionBridge.scalarToUtf16Offset(min(anchor, head), in: self)
|
|
3843
|
+
let endUtf16 = PositionBridge.scalarToUtf16Offset(max(anchor, head), in: self)
|
|
3844
|
+
let targetRange = NSRange(location: startUtf16, length: max(0, endUtf16 - startUtf16))
|
|
3845
|
+
if selectedRange != targetRange {
|
|
3846
|
+
selectedRange = targetRange
|
|
3847
|
+
noteSelectionDidChange()
|
|
3848
|
+
}
|
|
3849
|
+
editorSetSelectionScalar(id: editorId, scalarAnchor: anchor, scalarHead: head)
|
|
3850
|
+
recordAuthorizedSelectionIfPossible()
|
|
3851
|
+
refreshTypingAttributesForSelection()
|
|
3852
|
+
let docAnchor = editorScalarToDoc(id: editorId, scalar: anchor)
|
|
3853
|
+
let docHead = editorScalarToDoc(id: editorId, scalar: head)
|
|
3854
|
+
editorDelegate?.editorTextView(self, selectionDidChange: docAnchor, head: docHead)
|
|
3101
3855
|
}
|
|
3102
3856
|
|
|
3103
3857
|
private func insertNodeInRust(_ nodeType: String) {
|
|
@@ -3281,12 +4035,24 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3281
4035
|
}
|
|
3282
4036
|
|
|
3283
4037
|
/// Paste HTML content through Rust.
|
|
3284
|
-
|
|
4038
|
+
@discardableResult
|
|
4039
|
+
private func pasteHTML(_ html: String, detectContentChange: Bool = false) -> Bool {
|
|
4040
|
+
let previousHTML = detectContentChange ? editorGetHtml(id: editorId) : nil
|
|
4041
|
+
syncCurrentUIKitSelectionToRust()
|
|
3285
4042
|
Self.inputLog.debug(
|
|
3286
4043
|
"[rust.pasteHTML] html=\(self.preview(html), privacy: .public) selection=\(self.selectionSummary(), privacy: .public)"
|
|
3287
4044
|
)
|
|
3288
4045
|
let updateJSON = editorInsertContentHtml(id: editorId, html: html)
|
|
3289
4046
|
applyUpdateJSON(updateJSON)
|
|
4047
|
+
guard let previousHTML else { return true }
|
|
4048
|
+
return editorGetHtml(id: editorId) != previousHTML
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
private func syncCurrentUIKitSelectionToRust() {
|
|
4052
|
+
guard editorId != 0, let range = selectedTextRange else { return }
|
|
4053
|
+
let anchor = PositionBridge.textViewToScalar(range.start, in: self)
|
|
4054
|
+
let head = PositionBridge.textViewToScalar(range.end, in: self)
|
|
4055
|
+
editorSetSelectionScalar(id: editorId, scalarAnchor: anchor, scalarHead: head)
|
|
3290
4056
|
}
|
|
3291
4057
|
|
|
3292
4058
|
/// Paste plain text through Rust.
|
|
@@ -3577,7 +4343,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3577
4343
|
_ attrStr: NSAttributedString,
|
|
3578
4344
|
replaceRange: NSRange? = nil,
|
|
3579
4345
|
usedPatch: Bool,
|
|
3580
|
-
positionCacheUpdate: PositionCacheUpdate = .scan
|
|
4346
|
+
positionCacheUpdate: PositionCacheUpdate = .scan,
|
|
4347
|
+
authorizedReplaceRange: NSRange? = nil,
|
|
4348
|
+
authorizedReplacementText: String? = nil,
|
|
4349
|
+
authorizedReplacementAttributedText: NSAttributedString? = nil
|
|
3581
4350
|
) -> ApplyRenderTrace {
|
|
3582
4351
|
let totalStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3583
4352
|
let replaceUtf16Length = replaceRange?.length ?? textStorage.length
|
|
@@ -3626,13 +4395,27 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3626
4395
|
let endEditingNanos = DispatchTime.now().uptimeNanoseconds - endEditingStartedAt
|
|
3627
4396
|
let textMutationNanos = DispatchTime.now().uptimeNanoseconds - textMutationStartedAt
|
|
3628
4397
|
let authorizedTextStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
4398
|
+
let snapshotReplaceRange = authorizedReplaceRange ?? replaceRange
|
|
4399
|
+
let snapshotReplacementText = authorizedReplacementText ?? attrStr.string
|
|
4400
|
+
let snapshotReplacementAttributedText = authorizedReplacementAttributedText ?? attrStr
|
|
4401
|
+
if let snapshotReplaceRange,
|
|
4402
|
+
snapshotReplaceRange.location >= 0,
|
|
4403
|
+
snapshotReplaceRange.location + snapshotReplaceRange.length <= lastAuthorizedTextStorage.length
|
|
3632
4404
|
{
|
|
3633
|
-
lastAuthorizedTextStorage.replaceCharacters(
|
|
4405
|
+
lastAuthorizedTextStorage.replaceCharacters(
|
|
4406
|
+
in: snapshotReplaceRange,
|
|
4407
|
+
with: snapshotReplacementText
|
|
4408
|
+
)
|
|
4409
|
+
lastAuthorizedAttributedTextStorage.replaceCharacters(
|
|
4410
|
+
in: snapshotReplaceRange,
|
|
4411
|
+
with: snapshotReplacementAttributedText
|
|
4412
|
+
)
|
|
3634
4413
|
} else {
|
|
3635
|
-
lastAuthorizedTextStorage.setString(
|
|
4414
|
+
lastAuthorizedTextStorage.setString(replaceRange == nil ? snapshotReplacementText : textStorage.string)
|
|
4415
|
+
let fallbackAttributedSnapshot = replaceRange == nil
|
|
4416
|
+
? snapshotReplacementAttributedText
|
|
4417
|
+
: NSAttributedString(attributedString: textStorage)
|
|
4418
|
+
lastAuthorizedAttributedTextStorage.setAttributedString(fallbackAttributedSnapshot)
|
|
3636
4419
|
}
|
|
3637
4420
|
let authorizedTextNanos = DispatchTime.now().uptimeNanoseconds - authorizedTextStartedAt
|
|
3638
4421
|
let cacheInvalidationStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
@@ -3932,8 +4715,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3932
4715
|
}
|
|
3933
4716
|
|
|
3934
4717
|
let existing = textStorage.attributedSubstring(from: fullReplaceRange)
|
|
3935
|
-
let
|
|
3936
|
-
let
|
|
4718
|
+
let existingRawString = existing.string
|
|
4719
|
+
let replacementRawString = replacement.string
|
|
4720
|
+
let existingString = existingRawString as NSString
|
|
4721
|
+
let replacementString = replacementRawString as NSString
|
|
3937
4722
|
let sharedLength = min(existing.length, replacement.length)
|
|
3938
4723
|
|
|
3939
4724
|
var prefix = 0
|
|
@@ -3996,6 +4781,17 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3996
4781
|
break
|
|
3997
4782
|
}
|
|
3998
4783
|
}
|
|
4784
|
+
prefix = sharedUtf16ScalarBoundary(atOrBefore: prefix, in: existingRawString, and: replacementRawString)
|
|
4785
|
+
while suffix > 0 {
|
|
4786
|
+
let existingSuffixStart = existing.length - suffix
|
|
4787
|
+
let replacementSuffixStart = replacement.length - suffix
|
|
4788
|
+
if suffix <= sharedLength - prefix,
|
|
4789
|
+
isUtf16ScalarBoundary(existingSuffixStart, in: existingRawString),
|
|
4790
|
+
isUtf16ScalarBoundary(replacementSuffixStart, in: replacementRawString) {
|
|
4791
|
+
break
|
|
4792
|
+
}
|
|
4793
|
+
suffix -= 1
|
|
4794
|
+
}
|
|
3999
4795
|
|
|
4000
4796
|
guard prefix > 0 || suffix > 0 else {
|
|
4001
4797
|
return (fullReplaceRange, replacement)
|
|
@@ -4128,7 +4924,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4128
4924
|
patchToApply.replacement,
|
|
4129
4925
|
replaceRange: patchToApply.replaceRange,
|
|
4130
4926
|
usedPatch: true,
|
|
4131
|
-
positionCacheUpdate: positionCacheUpdate
|
|
4927
|
+
positionCacheUpdate: positionCacheUpdate,
|
|
4928
|
+
authorizedReplaceRange: fullReplaceRange,
|
|
4929
|
+
authorizedReplacementText: attrStr.string,
|
|
4930
|
+
authorizedReplacementAttributedText: attrStr
|
|
4132
4931
|
)
|
|
4133
4932
|
let metadataStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4134
4933
|
applyTopLevelChildMetadataPatch(
|
|
@@ -4172,6 +4971,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4172
4971
|
let update = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
4173
4972
|
else { return }
|
|
4174
4973
|
let parseNanos = DispatchTime.now().uptimeNanoseconds - parseStartedAt
|
|
4974
|
+
resetPendingNativeTextMutationState()
|
|
4175
4975
|
|
|
4176
4976
|
let renderElements = update["renderElements"] as? [[String: Any]]
|
|
4177
4977
|
let selectionFromUpdate = (update["selection"] as? [String: Any])
|
|
@@ -4337,6 +5137,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4337
5137
|
postApplyTrace.selectionOrContentCallbackNanos
|
|
4338
5138
|
)
|
|
4339
5139
|
}
|
|
5140
|
+
recordAuthorizedSelectionIfPossible()
|
|
4340
5141
|
Self.updateLog.debug(
|
|
4341
5142
|
"[applyUpdateJSON.end] finalSelection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
4342
5143
|
)
|
|
@@ -4353,6 +5154,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4353
5154
|
/// elements directly, not wrapped in an EditorUpdate).
|
|
4354
5155
|
func applyRenderJSON(_ renderJSON: String) {
|
|
4355
5156
|
ensureInternalTextViewDelegate()
|
|
5157
|
+
resetPendingNativeTextMutationState()
|
|
4356
5158
|
Self.updateLog.debug(
|
|
4357
5159
|
"[applyRenderJSON.begin] before=\(self.textSnapshotSummary(), privacy: .public)"
|
|
4358
5160
|
)
|
|
@@ -4368,6 +5170,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4368
5170
|
|
|
4369
5171
|
refreshPlaceholderVisibility()
|
|
4370
5172
|
_ = performPostApplyMaintenance()
|
|
5173
|
+
recordAuthorizedSelectionIfPossible()
|
|
4371
5174
|
Self.updateLog.debug(
|
|
4372
5175
|
"[applyRenderJSON.end] after=\(self.textSnapshotSummary(), privacy: .public)"
|
|
4373
5176
|
)
|
|
@@ -4425,17 +5228,20 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4425
5228
|
let adjustedRange = NSRange(location: adjustedOffset, length: 0)
|
|
4426
5229
|
if selectedRange != adjustedRange {
|
|
4427
5230
|
selectedRange = adjustedRange
|
|
5231
|
+
noteSelectionDidChange()
|
|
4428
5232
|
}
|
|
4429
5233
|
} else {
|
|
4430
5234
|
let targetRange = NSRange(location: endUtf16, length: 0)
|
|
4431
5235
|
if selectedRange != targetRange {
|
|
4432
5236
|
selectedRange = targetRange
|
|
5237
|
+
noteSelectionDidChange()
|
|
4433
5238
|
}
|
|
4434
5239
|
}
|
|
4435
5240
|
} else {
|
|
4436
5241
|
let targetRange = NSRange(location: startUtf16, length: endUtf16 - startUtf16)
|
|
4437
5242
|
if selectedRange != targetRange {
|
|
4438
5243
|
selectedRange = targetRange
|
|
5244
|
+
noteSelectionDidChange()
|
|
4439
5245
|
}
|
|
4440
5246
|
}
|
|
4441
5247
|
let assignmentNanos = DispatchTime.now().uptimeNanoseconds - assignmentStartedAt
|
|
@@ -4467,6 +5273,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4467
5273
|
let assignmentStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4468
5274
|
if selectedRange != targetRange {
|
|
4469
5275
|
selectedRange = targetRange
|
|
5276
|
+
noteSelectionDidChange()
|
|
4470
5277
|
}
|
|
4471
5278
|
let assignmentNanos = DispatchTime.now().uptimeNanoseconds - assignmentStartedAt
|
|
4472
5279
|
let chromeStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
@@ -4485,6 +5292,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4485
5292
|
case "all":
|
|
4486
5293
|
let assignmentStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4487
5294
|
selectedTextRange = textRange(from: beginningOfDocument, to: endOfDocument)
|
|
5295
|
+
noteSelectionDidChange()
|
|
4488
5296
|
let assignmentNanos = DispatchTime.now().uptimeNanoseconds - assignmentStartedAt
|
|
4489
5297
|
let chromeStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4490
5298
|
showNativeSelectionChromeIfNeeded()
|
|
@@ -4536,19 +5344,30 @@ extension EditorTextView: NSTextStorageDelegate {
|
|
|
4536
5344
|
// Only care about actual character edits, not attribute-only changes.
|
|
4537
5345
|
guard editedMask.contains(.editedCharacters) else { return }
|
|
4538
5346
|
|
|
4539
|
-
// Skip if this change came from our own Rust apply path
|
|
4540
|
-
|
|
5347
|
+
// Skip if this change came from our own Rust apply path, transient IME
|
|
5348
|
+
// composition, or an inline prediction. iOS inline predictions (iOS 17+)
|
|
5349
|
+
// mutate textStorage directly and set markedTextRange without calling
|
|
5350
|
+
// setMarkedText, so isComposing remains false — check markedTextRange too.
|
|
5351
|
+
guard !isApplyingRustState, !isComposing, markedTextRange == nil else { return }
|
|
4541
5352
|
|
|
4542
5353
|
// Skip if no editor is bound yet (nothing to reconcile against).
|
|
4543
5354
|
guard editorId != 0 else { return }
|
|
4544
5355
|
|
|
5356
|
+
PositionBridge.invalidateCache(for: self)
|
|
5357
|
+
|
|
4545
5358
|
// Compare current text storage content against last authorized snapshot.
|
|
4546
5359
|
let currentText = textStorage.string
|
|
4547
5360
|
guard currentText != lastAuthorizedText else { return }
|
|
4548
5361
|
currentTopLevelChildMetadata = nil
|
|
4549
5362
|
|
|
4550
|
-
|
|
4551
|
-
|
|
5363
|
+
let allowAfterBlur = canAdoptNativeTextMutationAfterBlur()
|
|
5364
|
+
if let mutation = nativeTextMutationFromAuthorizedDiff(currentText: currentText),
|
|
5365
|
+
isInterceptingInput
|
|
5366
|
+
|| shouldAdoptNativeTextStorageMutation(
|
|
5367
|
+
mutation,
|
|
5368
|
+
allowAfterBlur: allowAfterBlur
|
|
5369
|
+
)
|
|
5370
|
+
{
|
|
4552
5371
|
scheduleNativeTextMutationCommit(mutation)
|
|
4553
5372
|
return
|
|
4554
5373
|
}
|
|
@@ -4695,6 +5514,8 @@ final class RichTextEditorView: UIView {
|
|
|
4695
5514
|
/// The Rust editor instance ID. Setting this binds/unbinds the editor.
|
|
4696
5515
|
var editorId: UInt64 = 0 {
|
|
4697
5516
|
didSet {
|
|
5517
|
+
guard oldValue != editorId else { return }
|
|
5518
|
+
textView.discardTransientNativeInputForEditorRebind()
|
|
4698
5519
|
if editorId != 0 {
|
|
4699
5520
|
textView.bindEditor(id: editorId)
|
|
4700
5521
|
} else {
|
|
@@ -4815,12 +5636,14 @@ final class RichTextEditorView: UIView {
|
|
|
4815
5636
|
textView.backgroundColor = backgroundColor
|
|
4816
5637
|
}
|
|
4817
5638
|
|
|
4818
|
-
|
|
4819
|
-
|
|
5639
|
+
@discardableResult
|
|
5640
|
+
func applyTheme(_ theme: EditorTheme?) -> Bool {
|
|
5641
|
+
guard textView.applyTheme(theme) else { return false }
|
|
4820
5642
|
let cornerRadius = theme?.borderRadius ?? 0
|
|
4821
5643
|
layer.cornerRadius = cornerRadius
|
|
4822
5644
|
clipsToBounds = cornerRadius > 0
|
|
4823
5645
|
refreshOverlays()
|
|
5646
|
+
return true
|
|
4824
5647
|
}
|
|
4825
5648
|
|
|
4826
5649
|
func setRemoteSelections(_ selections: [RemoteSelectionDecoration]) {
|