@apollohg/react-native-prose-editor 0.5.15 → 0.5.16

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.
@@ -18,6 +18,7 @@ import android.util.Log
18
18
  import android.util.TypedValue
19
19
  import android.view.KeyEvent
20
20
  import android.view.MotionEvent
21
+ import android.view.inputmethod.BaseInputConnection
21
22
  import android.view.inputmethod.EditorInfo
22
23
  import android.view.inputmethod.InputConnection
23
24
  import android.view.inputmethod.InputMethodManager
@@ -107,6 +108,13 @@ class EditorEditText @JvmOverloads constructor(
107
108
  val end: Int
108
109
  )
109
110
 
111
+ private data class NativeTextMutation(
112
+ val scalarFrom: Int,
113
+ val scalarTo: Int,
114
+ val replacementText: String,
115
+ val resultingText: String
116
+ )
117
+
110
118
  /**
111
119
  * Listener interface for editor events, parallel to iOS's EditorTextViewDelegate.
112
120
  */
@@ -1223,6 +1231,66 @@ class EditorEditText @JvmOverloads constructor(
1223
1231
  applyUpdateJSON(updateJSON)
1224
1232
  }
1225
1233
 
1234
+ private fun nativeTextMutationFromAuthorizedDiff(currentText: String): NativeTextMutation? {
1235
+ val authorizedText = lastAuthorizedText
1236
+ if (currentText == authorizedText) return null
1237
+
1238
+ var prefix = 0
1239
+ val sharedLength = minOf(authorizedText.length, currentText.length)
1240
+ while (
1241
+ prefix < sharedLength &&
1242
+ authorizedText[prefix] == currentText[prefix]
1243
+ ) {
1244
+ prefix++
1245
+ }
1246
+
1247
+ var authorizedEnd = authorizedText.length
1248
+ var currentEnd = currentText.length
1249
+ while (
1250
+ authorizedEnd > prefix &&
1251
+ currentEnd > prefix &&
1252
+ authorizedText[authorizedEnd - 1] == currentText[currentEnd - 1]
1253
+ ) {
1254
+ authorizedEnd--
1255
+ currentEnd--
1256
+ }
1257
+
1258
+ val replacementText = currentText.substring(prefix, currentEnd)
1259
+ return NativeTextMutation(
1260
+ scalarFrom = PositionBridge.utf16ToScalar(prefix, authorizedText),
1261
+ scalarTo = PositionBridge.utf16ToScalar(authorizedEnd, authorizedText),
1262
+ replacementText = replacementText,
1263
+ resultingText = currentText
1264
+ )
1265
+ }
1266
+
1267
+ private fun shouldAdoptNativeTextMutation(editable: Editable?): Boolean {
1268
+ if (!isEditable || !hasFocus()) return false
1269
+ if (editable == null) return true
1270
+
1271
+ val composingStart = BaseInputConnection.getComposingSpanStart(editable)
1272
+ val composingEnd = BaseInputConnection.getComposingSpanEnd(editable)
1273
+ return composingStart < 0 || composingEnd < 0 || composingStart == composingEnd
1274
+ }
1275
+
1276
+ private fun commitNativeTextMutation(mutation: NativeTextMutation) {
1277
+ if ((text?.toString() ?: "") != mutation.resultingText) return
1278
+
1279
+ if (mutation.scalarFrom == mutation.scalarTo) {
1280
+ if (mutation.replacementText.isNotEmpty()) {
1281
+ insertTextInRust(mutation.replacementText, mutation.scalarFrom)
1282
+ }
1283
+ } else if (mutation.replacementText.isEmpty()) {
1284
+ deleteRangeInRust(mutation.scalarFrom, mutation.scalarTo)
1285
+ } else {
1286
+ replaceTextRangeInRust(
1287
+ mutation.scalarFrom,
1288
+ mutation.scalarTo,
1289
+ mutation.replacementText
1290
+ )
1291
+ }
1292
+ }
1293
+
1226
1294
  /**
1227
1295
  * Delete a scalar range via the Rust editor.
1228
1296
  *
@@ -2018,6 +2086,12 @@ class EditorEditText @JvmOverloads constructor(
2018
2086
  val currentText = s?.toString() ?: ""
2019
2087
  if (currentText == lastAuthorizedText) return
2020
2088
 
2089
+ val mutation = nativeTextMutationFromAuthorizedDiff(currentText)
2090
+ if (mutation != null && shouldAdoptNativeTextMutation(s)) {
2091
+ commitNativeTextMutation(mutation)
2092
+ return
2093
+ }
2094
+
2021
2095
  // Text has diverged from Rust's authorized state.
2022
2096
  reconciliationCount++
2023
2097
  Log.w(
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>libeditor_core.a</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64_x86_64-simulator</string>
11
+ <string>ios-arm64</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>libeditor_core.a</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
- <string>x86_64</string>
18
17
  </array>
19
18
  <key>SupportedPlatform</key>
20
19
  <string>ios</string>
21
- <key>SupportedPlatformVariant</key>
22
- <string>simulator</string>
23
20
  </dict>
24
21
  <dict>
25
22
  <key>BinaryPath</key>
26
23
  <string>libeditor_core.a</string>
27
24
  <key>LibraryIdentifier</key>
28
- <string>ios-arm64</string>
25
+ <string>ios-arm64_x86_64-simulator</string>
29
26
  <key>LibraryPath</key>
30
27
  <string>libeditor_core.a</string>
31
28
  <key>SupportedArchitectures</key>
32
29
  <array>
33
30
  <string>arm64</string>
31
+ <string>x86_64</string>
34
32
  </array>
35
33
  <key>SupportedPlatform</key>
36
34
  <string>ios</string>
35
+ <key>SupportedPlatformVariant</key>
36
+ <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>
@@ -819,6 +819,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
819
819
  let entries: [TopLevelChildMetadata]
820
820
  }
821
821
 
822
+ private struct NativeTextMutation {
823
+ let from: UInt32
824
+ let to: UInt32
825
+ let replacementText: String
826
+ let resultingText: String
827
+ }
828
+
822
829
  private enum PositionCacheUpdate {
823
830
  case scan
824
831
  case invalidate
@@ -964,6 +971,8 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
964
971
  /// trailing UIKit text-storage callbacks that arrive on the next run loop.
965
972
  private var interceptedInputDepth = 0
966
973
  private var reconciliationWorkScheduled = false
974
+ private var nativeTextMutationCommitScheduled = false
975
+ private var pendingNativeTextMutation: NativeTextMutation?
967
976
 
968
977
  /// Coalesces selection sync until UIKit has finished resolving the
969
978
  /// current tap/drag gesture's final caret position.
@@ -2209,7 +2218,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
2209
2218
  func textViewDidChangeSelection(_ textView: UITextView) {
2210
2219
  guard textView === self else { return }
2211
2220
  ensureInternalTextViewDelegate()
2212
- guard !isApplyingRustState, !isComposing else { return }
2221
+ guard !isApplyingRustState, !isComposing, !nativeTextMutationCommitScheduled else { return }
2213
2222
  if normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded() {
2214
2223
  return
2215
2224
  }
@@ -2390,7 +2399,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
2390
2399
  }
2391
2400
 
2392
2401
  private func syncSelectionToRustAndNotifyDelegate() {
2393
- guard !isApplyingRustState, !isComposing, editorId != 0 else { return }
2402
+ guard !isApplyingRustState,
2403
+ !isComposing,
2404
+ !nativeTextMutationCommitScheduled,
2405
+ editorId != 0
2406
+ else {
2407
+ return
2408
+ }
2394
2409
  guard let range = selectedTextRange else { return }
2395
2410
 
2396
2411
  let anchor = PositionBridge.textViewToScalar(range.start, in: self)
@@ -2997,6 +3012,94 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
2997
3012
  applyUpdateJSON(updateJSON)
2998
3013
  }
2999
3014
 
3015
+ private func nativeTextMutationFromAuthorizedDiff(
3016
+ currentText: String
3017
+ ) -> NativeTextMutation? {
3018
+ let authorizedText = lastAuthorizedText
3019
+ guard currentText != authorizedText else { return nil }
3020
+
3021
+ let authorized = authorizedText as NSString
3022
+ let current = currentText as NSString
3023
+ let sharedLength = min(authorized.length, current.length)
3024
+ var prefix = 0
3025
+ while prefix < sharedLength,
3026
+ authorized.character(at: prefix) == current.character(at: prefix) {
3027
+ prefix += 1
3028
+ }
3029
+
3030
+ var authorizedEnd = authorized.length
3031
+ var currentEnd = current.length
3032
+ while authorizedEnd > prefix,
3033
+ currentEnd > prefix,
3034
+ authorized.character(at: authorizedEnd - 1) == current.character(at: currentEnd - 1) {
3035
+ authorizedEnd -= 1
3036
+ currentEnd -= 1
3037
+ }
3038
+
3039
+ let replacementLength = currentEnd - prefix
3040
+ guard replacementLength >= 0 else { return nil }
3041
+ let replacementText = current.substring(
3042
+ with: NSRange(location: prefix, length: replacementLength)
3043
+ )
3044
+
3045
+ return NativeTextMutation(
3046
+ from: PositionBridge.utf16OffsetToScalar(prefix, in: authorizedText),
3047
+ to: PositionBridge.utf16OffsetToScalar(authorizedEnd, in: authorizedText),
3048
+ replacementText: replacementText,
3049
+ resultingText: currentText
3050
+ )
3051
+ }
3052
+
3053
+ private func shouldAdoptNativeTextStorageMutation() -> Bool {
3054
+ isFirstResponder && isEditable
3055
+ }
3056
+
3057
+ private func scheduleNativeTextMutationCommit(_ mutation: NativeTextMutation) {
3058
+ pendingNativeTextMutation = mutation
3059
+ guard !nativeTextMutationCommitScheduled else { return }
3060
+
3061
+ nativeTextMutationCommitScheduled = true
3062
+ DispatchQueue.main.async { [weak self] in
3063
+ guard let self else { return }
3064
+ self.nativeTextMutationCommitScheduled = false
3065
+ guard let mutation = self.pendingNativeTextMutation else { return }
3066
+ self.pendingNativeTextMutation = nil
3067
+
3068
+ guard self.editorId != 0,
3069
+ !self.isApplyingRustState,
3070
+ !self.isInterceptingInput,
3071
+ !self.isComposing,
3072
+ self.shouldAdoptNativeTextStorageMutation()
3073
+ else {
3074
+ if self.textStorage.string != self.lastAuthorizedText {
3075
+ self.scheduleReconciliationFromRust()
3076
+ }
3077
+ return
3078
+ }
3079
+ guard self.textStorage.string == mutation.resultingText else {
3080
+ if self.textStorage.string != self.lastAuthorizedText {
3081
+ self.scheduleReconciliationFromRust()
3082
+ }
3083
+ return
3084
+ }
3085
+
3086
+ self.performInterceptedInput {
3087
+ if mutation.from == mutation.to {
3088
+ guard !mutation.replacementText.isEmpty else { return }
3089
+ self.insertTextInRust(mutation.replacementText, at: mutation.from)
3090
+ } else if mutation.replacementText.isEmpty {
3091
+ self.deleteScalarRangeInRust(from: mutation.from, to: mutation.to)
3092
+ } else {
3093
+ self.replaceTextRangeInRust(
3094
+ from: mutation.from,
3095
+ to: mutation.to,
3096
+ with: mutation.replacementText
3097
+ )
3098
+ }
3099
+ }
3100
+ }
3101
+ }
3102
+
3000
3103
  private func insertNodeInRust(_ nodeType: String) {
3001
3104
  guard let selection = currentScalarSelection() else { return }
3002
3105
  Self.inputLog.debug(
@@ -4443,6 +4546,13 @@ extension EditorTextView: NSTextStorageDelegate {
4443
4546
  let currentText = textStorage.string
4444
4547
  guard currentText != lastAuthorizedText else { return }
4445
4548
  currentTopLevelChildMetadata = nil
4549
+
4550
+ if shouldAdoptNativeTextStorageMutation(),
4551
+ let mutation = nativeTextMutationFromAuthorizedDiff(currentText: currentText) {
4552
+ scheduleNativeTextMutationCommit(mutation)
4553
+ return
4554
+ }
4555
+
4446
4556
  let authorizedPreview = preview(lastAuthorizedText)
4447
4557
  let storagePreview = preview(currentText)
4448
4558
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollohg/react-native-prose-editor",
3
- "version": "0.5.15",
3
+ "version": "0.5.16",
4
4
  "description": "Native rich text editor with Rust core for React Native",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/apollohg/react-native-prose-editor",