@apollohg/react-native-prose-editor 0.5.16 → 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 +1396 -143
- 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/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 +715 -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,19 @@ 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 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
|
|
827
840
|
}
|
|
828
841
|
|
|
829
842
|
private enum PositionCacheUpdate {
|
|
@@ -922,6 +935,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
922
935
|
/// The plain text from the last Rust render, used by the reconciliation
|
|
923
936
|
/// fallback to detect unauthorized text storage mutations.
|
|
924
937
|
private var lastAuthorizedTextStorage = NSMutableString()
|
|
938
|
+
private var lastAuthorizedAttributedTextStorage = NSMutableAttributedString()
|
|
925
939
|
private var lastAuthorizedText: String {
|
|
926
940
|
lastAuthorizedTextStorage as String
|
|
927
941
|
}
|
|
@@ -973,6 +987,15 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
973
987
|
private var reconciliationWorkScheduled = false
|
|
974
988
|
private var nativeTextMutationCommitScheduled = false
|
|
975
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
|
|
976
999
|
|
|
977
1000
|
/// Coalesces selection sync until UIKit has finished resolving the
|
|
978
1001
|
/// current tap/drag gesture's final caret position.
|
|
@@ -986,9 +1009,29 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
986
1009
|
private var markedTextReplacementScalarRange: (from: UInt32, to: UInt32)?
|
|
987
1010
|
private var markedTextReplacementUtf16Range: NSRange?
|
|
988
1011
|
private var markedTextCompositionText: String?
|
|
1012
|
+
private var markedTextCompositionIsExplicitlyEmpty = false
|
|
989
1013
|
|
|
990
1014
|
private let editorLayoutManager: EditorLayoutManager
|
|
991
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
|
+
|
|
992
1035
|
// MARK: - Placeholder
|
|
993
1036
|
|
|
994
1037
|
private lazy var placeholderLabel: UILabel = {
|
|
@@ -1073,6 +1116,19 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1073
1116
|
}
|
|
1074
1117
|
|
|
1075
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?) {
|
|
1076
1132
|
switch autoCapitalize {
|
|
1077
1133
|
case "none":
|
|
1078
1134
|
autocapitalizationType = .none
|
|
@@ -1083,21 +1139,127 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1083
1139
|
default:
|
|
1084
1140
|
autocapitalizationType = .sentences
|
|
1085
1141
|
}
|
|
1142
|
+
if isFirstResponder {
|
|
1143
|
+
reloadInputViews()
|
|
1144
|
+
}
|
|
1086
1145
|
}
|
|
1087
1146
|
|
|
1088
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?) {
|
|
1089
1161
|
let isEnabled = autoCorrect ?? false
|
|
1090
1162
|
autocorrectionType = isEnabled ? .yes : .no
|
|
1091
1163
|
spellCheckingType = isEnabled ? .default : .no
|
|
1164
|
+
if isFirstResponder {
|
|
1165
|
+
reloadInputViews()
|
|
1166
|
+
}
|
|
1092
1167
|
}
|
|
1093
1168
|
|
|
1094
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?) {
|
|
1095
1183
|
self.keyboardType = Self.resolvedKeyboardType(from: keyboardType)
|
|
1096
1184
|
if isFirstResponder {
|
|
1097
1185
|
reloadInputViews()
|
|
1098
1186
|
}
|
|
1099
1187
|
}
|
|
1100
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
|
+
|
|
1101
1263
|
private static func resolvedKeyboardType(from keyboardType: String?) -> UIKeyboardType {
|
|
1102
1264
|
switch keyboardType {
|
|
1103
1265
|
case "ascii-capable":
|
|
@@ -1232,6 +1394,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1232
1394
|
let didBecomeFirstResponder = super.becomeFirstResponder()
|
|
1233
1395
|
if didBecomeFirstResponder {
|
|
1234
1396
|
ensureInternalTextViewDelegate()
|
|
1397
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1235
1398
|
DispatchQueue.main.async { [weak self] in
|
|
1236
1399
|
self?.ensureInternalTextViewDelegate()
|
|
1237
1400
|
}
|
|
@@ -1241,6 +1404,31 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1241
1404
|
return didBecomeFirstResponder
|
|
1242
1405
|
}
|
|
1243
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
|
+
|
|
1244
1432
|
private func isRenderedContentEmpty() -> Bool {
|
|
1245
1433
|
let renderedText = textStorage.string
|
|
1246
1434
|
guard !renderedText.isEmpty else { return true }
|
|
@@ -1410,6 +1598,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1410
1598
|
lastRenderAppliedPatchForTesting
|
|
1411
1599
|
}
|
|
1412
1600
|
|
|
1601
|
+
func authorizedTextForTesting() -> String {
|
|
1602
|
+
lastAuthorizedText
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1413
1605
|
func lastApplyUpdateTrace() -> ApplyUpdateTrace? {
|
|
1414
1606
|
lastApplyUpdateTraceForTesting
|
|
1415
1607
|
}
|
|
@@ -1612,6 +1804,12 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1612
1804
|
/// - initialHTML: Optional HTML to set as initial content.
|
|
1613
1805
|
func bindEditor(id: UInt64, initialHTML: String? = nil) {
|
|
1614
1806
|
ensureInternalTextViewDelegate()
|
|
1807
|
+
if editorId == id, initialHTML == nil {
|
|
1808
|
+
return
|
|
1809
|
+
}
|
|
1810
|
+
if editorId != id {
|
|
1811
|
+
discardTransientNativeInputForEditorRebind()
|
|
1812
|
+
}
|
|
1615
1813
|
editorId = id
|
|
1616
1814
|
|
|
1617
1815
|
if let html = initialHTML, !html.isEmpty {
|
|
@@ -1623,10 +1821,12 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1623
1821
|
let stateJSON = editorGetCurrentState(id: editorId)
|
|
1624
1822
|
applyUpdateJSON(stateJSON, notifyDelegate: false)
|
|
1625
1823
|
}
|
|
1824
|
+
replayDesiredInputTraitsIfNeeded()
|
|
1626
1825
|
}
|
|
1627
1826
|
|
|
1628
1827
|
/// Unbind from the current editor instance.
|
|
1629
1828
|
func unbindEditor() {
|
|
1829
|
+
discardTransientNativeInputForEditorRebind()
|
|
1630
1830
|
editorId = 0
|
|
1631
1831
|
}
|
|
1632
1832
|
|
|
@@ -1649,6 +1849,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1649
1849
|
super.insertText(text)
|
|
1650
1850
|
return
|
|
1651
1851
|
}
|
|
1852
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1652
1853
|
|
|
1653
1854
|
if markedTextReplacementScalarRange != nil || markedTextRange != nil {
|
|
1654
1855
|
let replacementRange = trackedMarkedTextReplacementRange()
|
|
@@ -1740,6 +1941,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1740
1941
|
super.deleteBackward()
|
|
1741
1942
|
return
|
|
1742
1943
|
}
|
|
1944
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1743
1945
|
|
|
1744
1946
|
if markedTextReplacementScalarRange != nil || markedTextRange != nil {
|
|
1745
1947
|
performTransientTextMutation {
|
|
@@ -1892,6 +2094,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1892
2094
|
guard !isApplyingRustState else { return }
|
|
1893
2095
|
guard editorId != 0 else { return }
|
|
1894
2096
|
guard isEditable else { return }
|
|
2097
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1895
2098
|
guard isCaretInsideList() else { return }
|
|
1896
2099
|
guard let selection = currentScalarSelection() else { return }
|
|
1897
2100
|
|
|
@@ -1943,6 +2146,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1943
2146
|
super.replace(range, withText: text)
|
|
1944
2147
|
return
|
|
1945
2148
|
}
|
|
2149
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1946
2150
|
|
|
1947
2151
|
if markedTextReplacementScalarRange != nil || markedTextRange != nil {
|
|
1948
2152
|
let replacementRange = trackedMarkedTextReplacementRange()
|
|
@@ -1978,21 +2182,36 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
1978
2182
|
override func setMarkedText(_ markedText: String?, selectedRange: NSRange) {
|
|
1979
2183
|
ensureInternalTextViewDelegate()
|
|
1980
2184
|
if markedText != nil {
|
|
2185
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1981
2186
|
captureMarkedTextReplacementRangeIfNeeded()
|
|
2187
|
+
} else if markedTextReplacementScalarRange == nil, markedTextRange == nil {
|
|
2188
|
+
guard flushPendingNativeTextMutationCommitIfNeeded() else { return }
|
|
1982
2189
|
}
|
|
1983
2190
|
isComposing = markedText != nil || markedTextReplacementScalarRange != nil
|
|
1984
2191
|
Self.inputLog.debug(
|
|
1985
2192
|
"[setMarkedText] marked=\(self.preview(markedText ?? ""), privacy: .public) nsRange=\(selectedRange.location),\(selectedRange.length) selection=\(self.selectionSummary(), privacy: .public)"
|
|
1986
2193
|
)
|
|
1987
|
-
performTransientTextMutation {
|
|
1988
|
-
super.setMarkedText(markedText, selectedRange: selectedRange)
|
|
1989
|
-
}
|
|
1990
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
|
+
}
|
|
1991
2202
|
clearMarkedTextTracking()
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
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)
|
|
1995
2213
|
}
|
|
2214
|
+
refreshMarkedTextCompositionText(fallback: markedText)
|
|
1996
2215
|
}
|
|
1997
2216
|
|
|
1998
2217
|
/// Called when composition is finalized (user selects a candidate or
|
|
@@ -2013,6 +2232,8 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2013
2232
|
"[unmarkText] composed=\(self.preview(composed), privacy: .public) replacement=\(self.previewMarkedTextReplacementRange(replacementRange), privacy: .public) selection=\(self.selectionSummary(), privacy: .public)"
|
|
2014
2233
|
)
|
|
2015
2234
|
commitMarkedText(composed, replacementRange: replacementRange)
|
|
2235
|
+
} else if shouldCommitMarkedText(composedText, replacementRange: replacementRange) {
|
|
2236
|
+
commitMarkedText(composedText ?? "", replacementRange: replacementRange)
|
|
2016
2237
|
} else {
|
|
2017
2238
|
restoreAuthorizedTextAfterCancelledCompositionIfNeeded()
|
|
2018
2239
|
}
|
|
@@ -2023,9 +2244,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2023
2244
|
|
|
2024
2245
|
guard let selectedRange = selectedTextRange else {
|
|
2025
2246
|
let scalarPos = PositionBridge.cursorScalarOffset(in: self)
|
|
2247
|
+
let utf16Pos = PositionBridge.scalarToUtf16Offset(scalarPos, in: lastAuthorizedText)
|
|
2026
2248
|
markedTextReplacementScalarRange = (from: scalarPos, to: scalarPos)
|
|
2027
2249
|
markedTextReplacementUtf16Range = NSRange(
|
|
2028
|
-
location:
|
|
2250
|
+
location: utf16Pos,
|
|
2029
2251
|
length: 0
|
|
2030
2252
|
)
|
|
2031
2253
|
return
|
|
@@ -2055,6 +2277,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2055
2277
|
markedTextReplacementScalarRange = nil
|
|
2056
2278
|
markedTextReplacementUtf16Range = nil
|
|
2057
2279
|
markedTextCompositionText = nil
|
|
2280
|
+
markedTextCompositionIsExplicitlyEmpty = false
|
|
2058
2281
|
isComposing = false
|
|
2059
2282
|
}
|
|
2060
2283
|
|
|
@@ -2073,14 +2296,30 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2073
2296
|
}
|
|
2074
2297
|
|
|
2075
2298
|
private func currentMarkedTextForCommit() -> String? {
|
|
2076
|
-
|
|
2299
|
+
if markedTextCompositionIsExplicitlyEmpty { return "" }
|
|
2300
|
+
return transientMarkedTextFromAuthorizedDiff()
|
|
2301
|
+
?? markedTextRange.flatMap { text(in: $0) }
|
|
2077
2302
|
?? markedTextCompositionText
|
|
2078
|
-
|
|
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) }
|
|
2079
2312
|
}
|
|
2080
2313
|
|
|
2081
2314
|
private func refreshMarkedTextCompositionText(fallback: String? = nil) {
|
|
2082
|
-
|
|
2083
|
-
|
|
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) }
|
|
2084
2323
|
?? fallback
|
|
2085
2324
|
}
|
|
2086
2325
|
|
|
@@ -2103,6 +2342,27 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2103
2342
|
return nil
|
|
2104
2343
|
}
|
|
2105
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
|
+
|
|
2106
2366
|
return currentText.substring(
|
|
2107
2367
|
with: NSRange(location: replacementRange.location, length: insertedLength)
|
|
2108
2368
|
)
|
|
@@ -2114,13 +2374,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2114
2374
|
) {
|
|
2115
2375
|
guard editorId != 0 else { return }
|
|
2116
2376
|
guard let replacementRange else {
|
|
2117
|
-
performInterceptedInput {
|
|
2377
|
+
performInterceptedInput(flushPendingNativeTextMutation: false) {
|
|
2118
2378
|
insertTextInRust(text, at: PositionBridge.cursorScalarOffset(in: self))
|
|
2119
2379
|
}
|
|
2120
2380
|
return
|
|
2121
2381
|
}
|
|
2122
2382
|
|
|
2123
|
-
performInterceptedInput {
|
|
2383
|
+
performInterceptedInput(flushPendingNativeTextMutation: false) {
|
|
2124
2384
|
if replacementRange.from == replacementRange.to {
|
|
2125
2385
|
insertTextInRust(text, at: replacementRange.from)
|
|
2126
2386
|
} else {
|
|
@@ -2133,6 +2393,16 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2133
2393
|
}
|
|
2134
2394
|
}
|
|
2135
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
|
+
|
|
2136
2406
|
private func restoreAuthorizedTextAfterCancelledCompositionIfNeeded() {
|
|
2137
2407
|
guard editorId != 0 else { return }
|
|
2138
2408
|
guard textStorage.string != lastAuthorizedText else { return }
|
|
@@ -2163,6 +2433,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2163
2433
|
super.paste(sender)
|
|
2164
2434
|
return
|
|
2165
2435
|
}
|
|
2436
|
+
guard prepareForExternalEditorUpdate() else { return }
|
|
2166
2437
|
|
|
2167
2438
|
Self.inputLog.debug(
|
|
2168
2439
|
"[paste] selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
@@ -2192,7 +2463,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2192
2463
|
documentAttributes: [.documentType: NSAttributedString.DocumentType.html]
|
|
2193
2464
|
), let html = String(data: htmlData, encoding: .utf8) {
|
|
2194
2465
|
performInterceptedInput {
|
|
2195
|
-
pasteHTML(html)
|
|
2466
|
+
if !pasteHTML(html, detectContentChange: true),
|
|
2467
|
+
!attrStr.string.isEmpty {
|
|
2468
|
+
pastePlainText(attrStr.string)
|
|
2469
|
+
}
|
|
2196
2470
|
}
|
|
2197
2471
|
return
|
|
2198
2472
|
}
|
|
@@ -2218,7 +2492,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2218
2492
|
func textViewDidChangeSelection(_ textView: UITextView) {
|
|
2219
2493
|
guard textView === self else { return }
|
|
2220
2494
|
ensureInternalTextViewDelegate()
|
|
2221
|
-
guard !isApplyingRustState,
|
|
2495
|
+
guard !isApplyingRustState,
|
|
2496
|
+
!isComposing,
|
|
2497
|
+
!nativeTextMutationCommitScheduled,
|
|
2498
|
+
pendingNativeTextMutation == nil
|
|
2499
|
+
else {
|
|
2500
|
+
return
|
|
2501
|
+
}
|
|
2222
2502
|
if normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded() {
|
|
2223
2503
|
return
|
|
2224
2504
|
}
|
|
@@ -2278,7 +2558,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2278
2558
|
delegate = self
|
|
2279
2559
|
}
|
|
2280
2560
|
|
|
2281
|
-
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
|
+
}
|
|
2282
2568
|
interceptedInputDepth += 1
|
|
2283
2569
|
Self.inputLog.debug(
|
|
2284
2570
|
"[intercept.begin] depth=\(self.interceptedInputDepth) selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
@@ -2290,6 +2576,12 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2290
2576
|
Self.inputLog.debug(
|
|
2291
2577
|
"[intercept.end] depth=\(self.interceptedInputDepth) selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
2292
2578
|
)
|
|
2579
|
+
if self.interceptedInputDepth == 0 {
|
|
2580
|
+
_ = self.drainPendingNativeTextMutation(
|
|
2581
|
+
allowAfterBlur: false,
|
|
2582
|
+
allowWhileIntercepting: false
|
|
2583
|
+
)
|
|
2584
|
+
}
|
|
2293
2585
|
}
|
|
2294
2586
|
}
|
|
2295
2587
|
|
|
@@ -2402,6 +2694,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2402
2694
|
guard !isApplyingRustState,
|
|
2403
2695
|
!isComposing,
|
|
2404
2696
|
!nativeTextMutationCommitScheduled,
|
|
2697
|
+
pendingNativeTextMutation == nil,
|
|
2405
2698
|
editorId != 0
|
|
2406
2699
|
else {
|
|
2407
2700
|
return
|
|
@@ -2421,9 +2714,11 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2421
2714
|
editorDelegate?.editorTextView(self, selectionDidChange: docAnchor, head: docHead)
|
|
2422
2715
|
}
|
|
2423
2716
|
|
|
2424
|
-
|
|
2425
|
-
|
|
2717
|
+
@discardableResult
|
|
2718
|
+
func applyTheme(_ theme: EditorTheme?) -> Bool {
|
|
2426
2719
|
if editorId != 0 {
|
|
2720
|
+
guard prepareForExternalEditorUpdate() else { return false }
|
|
2721
|
+
self.theme = theme
|
|
2427
2722
|
let previousOffset = contentOffset
|
|
2428
2723
|
let stateJSON = editorGetCurrentState(id: editorId)
|
|
2429
2724
|
applyUpdateJSON(stateJSON, notifyDelegate: false)
|
|
@@ -2431,11 +2726,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2431
2726
|
preserveScrollOffset(previousOffset)
|
|
2432
2727
|
}
|
|
2433
2728
|
} else {
|
|
2729
|
+
self.theme = theme
|
|
2434
2730
|
refreshTypingAttributesForSelection()
|
|
2435
2731
|
}
|
|
2436
2732
|
if heightBehavior == .autoGrow {
|
|
2437
2733
|
notifyHeightChangeIfNeeded(force: true)
|
|
2438
2734
|
}
|
|
2735
|
+
return true
|
|
2439
2736
|
}
|
|
2440
2737
|
|
|
2441
2738
|
private func preserveScrollOffset(_ previousOffset: CGPoint) {
|
|
@@ -2871,8 +3168,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2871
3168
|
}
|
|
2872
3169
|
|
|
2873
3170
|
func performToolbarToggleMark(_ markName: String) {
|
|
2874
|
-
guard
|
|
2875
|
-
guard isEditable else { return }
|
|
3171
|
+
guard prepareForToolbarCommand() else { return }
|
|
2876
3172
|
guard let selection = currentScalarSelection() else { return }
|
|
2877
3173
|
performInterceptedInput {
|
|
2878
3174
|
let updateJSON = editorToggleMarkAtSelectionScalar(
|
|
@@ -2886,8 +3182,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2886
3182
|
}
|
|
2887
3183
|
|
|
2888
3184
|
func performToolbarToggleList(_ listType: String, isActive: Bool) {
|
|
2889
|
-
guard
|
|
2890
|
-
guard isEditable else { return }
|
|
3185
|
+
guard prepareForToolbarCommand() else { return }
|
|
2891
3186
|
guard let selection = currentScalarSelection() else { return }
|
|
2892
3187
|
performInterceptedInput {
|
|
2893
3188
|
let updateJSON = isActive
|
|
@@ -2907,8 +3202,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2907
3202
|
}
|
|
2908
3203
|
|
|
2909
3204
|
func performToolbarToggleBlockquote() {
|
|
2910
|
-
guard
|
|
2911
|
-
guard isEditable else { return }
|
|
3205
|
+
guard prepareForToolbarCommand() else { return }
|
|
2912
3206
|
guard let selection = currentScalarSelection() else { return }
|
|
2913
3207
|
performInterceptedInput {
|
|
2914
3208
|
let updateJSON = editorToggleBlockquoteAtSelectionScalar(
|
|
@@ -2921,8 +3215,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2921
3215
|
}
|
|
2922
3216
|
|
|
2923
3217
|
func performToolbarToggleHeading(_ level: Int) {
|
|
2924
|
-
guard
|
|
2925
|
-
guard isEditable else { return }
|
|
3218
|
+
guard prepareForToolbarCommand() else { return }
|
|
2926
3219
|
guard let selection = currentScalarSelection() else { return }
|
|
2927
3220
|
guard let level = UInt8(exactly: level), (1...6).contains(level) else { return }
|
|
2928
3221
|
performInterceptedInput {
|
|
@@ -2937,8 +3230,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2937
3230
|
}
|
|
2938
3231
|
|
|
2939
3232
|
func performToolbarIndentListItem() {
|
|
2940
|
-
guard
|
|
2941
|
-
guard isEditable else { return }
|
|
3233
|
+
guard prepareForToolbarCommand() else { return }
|
|
2942
3234
|
guard let selection = currentScalarSelection() else { return }
|
|
2943
3235
|
performInterceptedInput {
|
|
2944
3236
|
let updateJSON = editorIndentListItemAtSelectionScalar(
|
|
@@ -2951,8 +3243,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2951
3243
|
}
|
|
2952
3244
|
|
|
2953
3245
|
func performToolbarOutdentListItem() {
|
|
2954
|
-
guard
|
|
2955
|
-
guard isEditable else { return }
|
|
3246
|
+
guard prepareForToolbarCommand() else { return }
|
|
2956
3247
|
guard let selection = currentScalarSelection() else { return }
|
|
2957
3248
|
performInterceptedInput {
|
|
2958
3249
|
let updateJSON = editorOutdentListItemAtSelectionScalar(
|
|
@@ -2965,16 +3256,14 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2965
3256
|
}
|
|
2966
3257
|
|
|
2967
3258
|
func performToolbarInsertNode(_ nodeType: String) {
|
|
2968
|
-
guard
|
|
2969
|
-
guard isEditable else { return }
|
|
3259
|
+
guard prepareForToolbarCommand() else { return }
|
|
2970
3260
|
performInterceptedInput {
|
|
2971
3261
|
insertNodeInRust(nodeType)
|
|
2972
3262
|
}
|
|
2973
3263
|
}
|
|
2974
3264
|
|
|
2975
3265
|
func performToolbarUndo() {
|
|
2976
|
-
guard
|
|
2977
|
-
guard isEditable else { return }
|
|
3266
|
+
guard prepareForToolbarCommand() else { return }
|
|
2978
3267
|
performInterceptedInput {
|
|
2979
3268
|
let updateJSON = editorUndo(id: editorId)
|
|
2980
3269
|
applyUpdateJSON(updateJSON)
|
|
@@ -2982,14 +3271,19 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
2982
3271
|
}
|
|
2983
3272
|
|
|
2984
3273
|
func performToolbarRedo() {
|
|
2985
|
-
guard
|
|
2986
|
-
guard isEditable else { return }
|
|
3274
|
+
guard prepareForToolbarCommand() else { return }
|
|
2987
3275
|
performInterceptedInput {
|
|
2988
3276
|
let updateJSON = editorRedo(id: editorId)
|
|
2989
3277
|
applyUpdateJSON(updateJSON)
|
|
2990
3278
|
}
|
|
2991
3279
|
}
|
|
2992
3280
|
|
|
3281
|
+
private func prepareForToolbarCommand() -> Bool {
|
|
3282
|
+
guard editorId != 0 else { return false }
|
|
3283
|
+
guard isEditable else { return false }
|
|
3284
|
+
return prepareForExternalEditorUpdate()
|
|
3285
|
+
}
|
|
3286
|
+
|
|
2993
3287
|
/// Insert text at a scalar position via the Rust editor.
|
|
2994
3288
|
private func insertTextInRust(_ text: String, at scalarPos: UInt32) {
|
|
2995
3289
|
Self.inputLog.debug(
|
|
@@ -3026,6 +3320,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3026
3320
|
authorized.character(at: prefix) == current.character(at: prefix) {
|
|
3027
3321
|
prefix += 1
|
|
3028
3322
|
}
|
|
3323
|
+
prefix = sharedUtf16ScalarBoundary(atOrBefore: prefix, in: authorizedText, and: currentText)
|
|
3029
3324
|
|
|
3030
3325
|
var authorizedEnd = authorized.length
|
|
3031
3326
|
var currentEnd = current.length
|
|
@@ -3035,6 +3330,8 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3035
3330
|
authorizedEnd -= 1
|
|
3036
3331
|
currentEnd -= 1
|
|
3037
3332
|
}
|
|
3333
|
+
authorizedEnd = utf16ScalarBoundary(atOrAfter: authorizedEnd, in: authorizedText)
|
|
3334
|
+
currentEnd = utf16ScalarBoundary(atOrAfter: currentEnd, in: currentText)
|
|
3038
3335
|
|
|
3039
3336
|
let replacementLength = currentEnd - prefix
|
|
3040
3337
|
guard replacementLength >= 0 else { return nil }
|
|
@@ -3042,16 +3339,229 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3042
3339
|
with: NSRange(location: prefix, length: replacementLength)
|
|
3043
3340
|
)
|
|
3044
3341
|
|
|
3342
|
+
let selectedScalarRange = selectedTextRange.map {
|
|
3343
|
+
PositionBridge.textRangeToScalarRange($0, in: self)
|
|
3344
|
+
}
|
|
3345
|
+
let capturedAfterBlur = canAdoptNativeTextMutationAfterBlur()
|
|
3346
|
+
|
|
3045
3347
|
return NativeTextMutation(
|
|
3046
|
-
from: PositionBridge.utf16OffsetToScalar(prefix, in:
|
|
3047
|
-
to: PositionBridge.utf16OffsetToScalar(authorizedEnd, in:
|
|
3348
|
+
from: PositionBridge.utf16OffsetToScalar(prefix, in: lastAuthorizedAttributedTextStorage),
|
|
3349
|
+
to: PositionBridge.utf16OffsetToScalar(authorizedEnd, in: lastAuthorizedAttributedTextStorage),
|
|
3048
3350
|
replacementText: replacementText,
|
|
3049
|
-
resultingText: currentText
|
|
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
|
|
3050
3380
|
)
|
|
3051
3381
|
}
|
|
3052
3382
|
|
|
3053
|
-
private func
|
|
3054
|
-
|
|
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
|
+
}
|
|
3055
3565
|
}
|
|
3056
3566
|
|
|
3057
3567
|
private func scheduleNativeTextMutationCommit(_ mutation: NativeTextMutation) {
|
|
@@ -3061,43 +3571,99 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3061
3571
|
nativeTextMutationCommitScheduled = true
|
|
3062
3572
|
DispatchQueue.main.async { [weak self] in
|
|
3063
3573
|
guard let self else { return }
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
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()
|
|
3078
3601
|
}
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
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
|
+
)
|
|
3084
3612
|
}
|
|
3613
|
+
return .rejected
|
|
3614
|
+
}
|
|
3085
3615
|
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
self.replaceTextRangeInRust(
|
|
3094
|
-
from: mutation.from,
|
|
3095
|
-
to: mutation.to,
|
|
3096
|
-
with: mutation.replacementText
|
|
3097
|
-
)
|
|
3098
|
-
}
|
|
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
|
+
)
|
|
3099
3623
|
}
|
|
3624
|
+
return .rejected
|
|
3100
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)
|
|
3101
3667
|
}
|
|
3102
3668
|
|
|
3103
3669
|
private func insertNodeInRust(_ nodeType: String) {
|
|
@@ -3281,12 +3847,24 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3281
3847
|
}
|
|
3282
3848
|
|
|
3283
3849
|
/// Paste HTML content through Rust.
|
|
3284
|
-
|
|
3850
|
+
@discardableResult
|
|
3851
|
+
private func pasteHTML(_ html: String, detectContentChange: Bool = false) -> Bool {
|
|
3852
|
+
let previousHTML = detectContentChange ? editorGetHtml(id: editorId) : nil
|
|
3853
|
+
syncCurrentUIKitSelectionToRust()
|
|
3285
3854
|
Self.inputLog.debug(
|
|
3286
3855
|
"[rust.pasteHTML] html=\(self.preview(html), privacy: .public) selection=\(self.selectionSummary(), privacy: .public)"
|
|
3287
3856
|
)
|
|
3288
3857
|
let updateJSON = editorInsertContentHtml(id: editorId, html: html)
|
|
3289
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)
|
|
3290
3868
|
}
|
|
3291
3869
|
|
|
3292
3870
|
/// Paste plain text through Rust.
|
|
@@ -3577,7 +4155,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3577
4155
|
_ attrStr: NSAttributedString,
|
|
3578
4156
|
replaceRange: NSRange? = nil,
|
|
3579
4157
|
usedPatch: Bool,
|
|
3580
|
-
positionCacheUpdate: PositionCacheUpdate = .scan
|
|
4158
|
+
positionCacheUpdate: PositionCacheUpdate = .scan,
|
|
4159
|
+
authorizedReplaceRange: NSRange? = nil,
|
|
4160
|
+
authorizedReplacementText: String? = nil,
|
|
4161
|
+
authorizedReplacementAttributedText: NSAttributedString? = nil
|
|
3581
4162
|
) -> ApplyRenderTrace {
|
|
3582
4163
|
let totalStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3583
4164
|
let replaceUtf16Length = replaceRange?.length ?? textStorage.length
|
|
@@ -3626,13 +4207,27 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3626
4207
|
let endEditingNanos = DispatchTime.now().uptimeNanoseconds - endEditingStartedAt
|
|
3627
4208
|
let textMutationNanos = DispatchTime.now().uptimeNanoseconds - textMutationStartedAt
|
|
3628
4209
|
let authorizedTextStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
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
|
|
3632
4216
|
{
|
|
3633
|
-
lastAuthorizedTextStorage.replaceCharacters(
|
|
4217
|
+
lastAuthorizedTextStorage.replaceCharacters(
|
|
4218
|
+
in: snapshotReplaceRange,
|
|
4219
|
+
with: snapshotReplacementText
|
|
4220
|
+
)
|
|
4221
|
+
lastAuthorizedAttributedTextStorage.replaceCharacters(
|
|
4222
|
+
in: snapshotReplaceRange,
|
|
4223
|
+
with: snapshotReplacementAttributedText
|
|
4224
|
+
)
|
|
3634
4225
|
} else {
|
|
3635
|
-
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)
|
|
3636
4231
|
}
|
|
3637
4232
|
let authorizedTextNanos = DispatchTime.now().uptimeNanoseconds - authorizedTextStartedAt
|
|
3638
4233
|
let cacheInvalidationStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
@@ -3932,8 +4527,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3932
4527
|
}
|
|
3933
4528
|
|
|
3934
4529
|
let existing = textStorage.attributedSubstring(from: fullReplaceRange)
|
|
3935
|
-
let
|
|
3936
|
-
let
|
|
4530
|
+
let existingRawString = existing.string
|
|
4531
|
+
let replacementRawString = replacement.string
|
|
4532
|
+
let existingString = existingRawString as NSString
|
|
4533
|
+
let replacementString = replacementRawString as NSString
|
|
3937
4534
|
let sharedLength = min(existing.length, replacement.length)
|
|
3938
4535
|
|
|
3939
4536
|
var prefix = 0
|
|
@@ -3996,6 +4593,17 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
3996
4593
|
break
|
|
3997
4594
|
}
|
|
3998
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
|
+
}
|
|
3999
4607
|
|
|
4000
4608
|
guard prefix > 0 || suffix > 0 else {
|
|
4001
4609
|
return (fullReplaceRange, replacement)
|
|
@@ -4128,7 +4736,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4128
4736
|
patchToApply.replacement,
|
|
4129
4737
|
replaceRange: patchToApply.replaceRange,
|
|
4130
4738
|
usedPatch: true,
|
|
4131
|
-
positionCacheUpdate: positionCacheUpdate
|
|
4739
|
+
positionCacheUpdate: positionCacheUpdate,
|
|
4740
|
+
authorizedReplaceRange: fullReplaceRange,
|
|
4741
|
+
authorizedReplacementText: attrStr.string,
|
|
4742
|
+
authorizedReplacementAttributedText: attrStr
|
|
4132
4743
|
)
|
|
4133
4744
|
let metadataStartedAt = DispatchTime.now().uptimeNanoseconds
|
|
4134
4745
|
applyTopLevelChildMetadataPatch(
|
|
@@ -4172,6 +4783,9 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
|
|
|
4172
4783
|
let update = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
4173
4784
|
else { return }
|
|
4174
4785
|
let parseNanos = DispatchTime.now().uptimeNanoseconds - parseStartedAt
|
|
4786
|
+
pendingNativeTextMutation = nil
|
|
4787
|
+
nativeTextMutationCommitScheduled = false
|
|
4788
|
+
advanceNativeTextMutationGeneration()
|
|
4175
4789
|
|
|
4176
4790
|
let renderElements = update["renderElements"] as? [[String: Any]]
|
|
4177
4791
|
let selectionFromUpdate = (update["selection"] as? [String: Any])
|
|
@@ -4536,19 +5150,27 @@ extension EditorTextView: NSTextStorageDelegate {
|
|
|
4536
5150
|
// Only care about actual character edits, not attribute-only changes.
|
|
4537
5151
|
guard editedMask.contains(.editedCharacters) else { return }
|
|
4538
5152
|
|
|
4539
|
-
// Skip if this change came from our own Rust apply path.
|
|
4540
|
-
guard !isApplyingRustState, !
|
|
5153
|
+
// Skip if this change came from our own Rust apply path or transient IME composition.
|
|
5154
|
+
guard !isApplyingRustState, !isComposing else { return }
|
|
4541
5155
|
|
|
4542
5156
|
// Skip if no editor is bound yet (nothing to reconcile against).
|
|
4543
5157
|
guard editorId != 0 else { return }
|
|
4544
5158
|
|
|
5159
|
+
PositionBridge.invalidateCache(for: self)
|
|
5160
|
+
|
|
4545
5161
|
// Compare current text storage content against last authorized snapshot.
|
|
4546
5162
|
let currentText = textStorage.string
|
|
4547
5163
|
guard currentText != lastAuthorizedText else { return }
|
|
4548
5164
|
currentTopLevelChildMetadata = nil
|
|
4549
5165
|
|
|
4550
|
-
|
|
4551
|
-
|
|
5166
|
+
let allowAfterBlur = canAdoptNativeTextMutationAfterBlur()
|
|
5167
|
+
if let mutation = nativeTextMutationFromAuthorizedDiff(currentText: currentText),
|
|
5168
|
+
isInterceptingInput
|
|
5169
|
+
|| shouldAdoptNativeTextStorageMutation(
|
|
5170
|
+
mutation,
|
|
5171
|
+
allowAfterBlur: allowAfterBlur
|
|
5172
|
+
)
|
|
5173
|
+
{
|
|
4552
5174
|
scheduleNativeTextMutationCommit(mutation)
|
|
4553
5175
|
return
|
|
4554
5176
|
}
|
|
@@ -4695,6 +5317,8 @@ final class RichTextEditorView: UIView {
|
|
|
4695
5317
|
/// The Rust editor instance ID. Setting this binds/unbinds the editor.
|
|
4696
5318
|
var editorId: UInt64 = 0 {
|
|
4697
5319
|
didSet {
|
|
5320
|
+
guard oldValue != editorId else { return }
|
|
5321
|
+
textView.discardTransientNativeInputForEditorRebind()
|
|
4698
5322
|
if editorId != 0 {
|
|
4699
5323
|
textView.bindEditor(id: editorId)
|
|
4700
5324
|
} else {
|
|
@@ -4815,12 +5439,14 @@ final class RichTextEditorView: UIView {
|
|
|
4815
5439
|
textView.backgroundColor = backgroundColor
|
|
4816
5440
|
}
|
|
4817
5441
|
|
|
4818
|
-
|
|
4819
|
-
|
|
5442
|
+
@discardableResult
|
|
5443
|
+
func applyTheme(_ theme: EditorTheme?) -> Bool {
|
|
5444
|
+
guard textView.applyTheme(theme) else { return false }
|
|
4820
5445
|
let cornerRadius = theme?.borderRadius ?? 0
|
|
4821
5446
|
layer.cornerRadius = cornerRadius
|
|
4822
5447
|
clipsToBounds = cornerRadius > 0
|
|
4823
5448
|
refreshOverlays()
|
|
5449
|
+
return true
|
|
4824
5450
|
}
|
|
4825
5451
|
|
|
4826
5452
|
func setRemoteSelections(_ selections: [RemoteSelectionDecoration]) {
|