@apollohg/react-native-prose-editor 0.5.17 → 0.5.19

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.
@@ -827,6 +827,9 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
827
827
  let authorizedText: String
828
828
  let selectionAnchor: UInt32?
829
829
  let selectionHead: UInt32?
830
+ let authorizedSelectionUtf16Range: NSRange?
831
+ let rawSelectionUtf16Range: NSRange?
832
+ let selectionRevision: UInt64
830
833
  let capturedWhileFirstResponder: Bool
831
834
  let capturedWhileEditable: Bool
832
835
  let capturedAfterBlur: Bool
@@ -991,6 +994,9 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
991
994
  private var nativeTextMutationAfterBlurDeadline: TimeInterval?
992
995
  private var nativeTextMutationAfterBlurGeneration: UInt64?
993
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
994
1000
  private var desiredInputTraitState = InputTraitState()
995
1001
  private var appliedInputTraitState = InputTraitState()
996
1002
  private var pendingInputTraitChange = PendingInputTraitChange()
@@ -1399,6 +1405,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1399
1405
  self?.ensureInternalTextViewDelegate()
1400
1406
  }
1401
1407
  _ = normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded()
1408
+ recordAuthorizedSelectionIfPossible()
1402
1409
  refreshTypingAttributesForSelection()
1403
1410
  }
1404
1411
  return didBecomeFirstResponder
@@ -1459,6 +1466,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1459
1466
  let adjustedRange = NSRange(location: 0, length: 0)
1460
1467
  guard currentRange != adjustedRange else { return false }
1461
1468
  selectedRange = adjustedRange
1469
+ noteSelectionDidChange()
1462
1470
  return true
1463
1471
  }
1464
1472
 
@@ -1494,6 +1502,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1494
1502
 
1495
1503
  _ = becomeFirstResponder()
1496
1504
  selectedTextRange = textRange
1505
+ noteSelectionDidChange()
1497
1506
  refreshNativeSelectionChromeVisibility()
1498
1507
  onSelectionOrContentMayChange?()
1499
1508
  scheduleSelectionSync()
@@ -1508,6 +1517,30 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1508
1517
  return NSRange(location: location, length: length)
1509
1518
  }
1510
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
+
1511
1544
  private func scheduleDeferredImageSelection(for range: NSRange) {
1512
1545
  pendingDeferredImageSelectionRange = range
1513
1546
  pendingDeferredImageSelectionGeneration &+= 1
@@ -2492,6 +2525,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
2492
2525
  func textViewDidChangeSelection(_ textView: UITextView) {
2493
2526
  guard textView === self else { return }
2494
2527
  ensureInternalTextViewDelegate()
2528
+ noteSelectionDidChange()
2495
2529
  guard !isApplyingRustState,
2496
2530
  !isComposing,
2497
2531
  !nativeTextMutationCommitScheduled,
@@ -2502,6 +2536,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
2502
2536
  if normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded() {
2503
2537
  return
2504
2538
  }
2539
+ recordAuthorizedSelectionIfPossible()
2505
2540
  refreshNativeSelectionChromeVisibility()
2506
2541
  onSelectionOrContentMayChange?()
2507
2542
  scheduleSelectionSync()
@@ -2538,6 +2573,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
2538
2573
  }
2539
2574
 
2540
2575
  selectedTextRange = textRange(from: start, to: end)
2576
+ noteSelectionDidChange()
2541
2577
  refreshNativeSelectionChromeVisibility()
2542
2578
  onSelectionOrContentMayChange?()
2543
2579
  scheduleSelectionSync()
@@ -2710,6 +2746,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
2710
2746
  )
2711
2747
 
2712
2748
  editorSetSelectionScalar(id: editorId, scalarAnchor: anchor, scalarHead: head)
2749
+ recordAuthorizedSelectionIfPossible()
2713
2750
  refreshTypingAttributesForSelection()
2714
2751
  editorDelegate?.editorTextView(self, selectionDidChange: docAnchor, head: docHead)
2715
2752
  }
@@ -3339,9 +3376,17 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
3339
3376
  with: NSRange(location: prefix, length: replacementLength)
3340
3377
  )
3341
3378
 
3342
- let selectedScalarRange = selectedTextRange.map {
3343
- PositionBridge.textRangeToScalarRange($0, in: self)
3344
- }
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:))
3345
3390
  let capturedAfterBlur = canAdoptNativeTextMutationAfterBlur()
3346
3391
 
3347
3392
  return NativeTextMutation(
@@ -3352,6 +3397,9 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
3352
3397
  authorizedText: authorizedText,
3353
3398
  selectionAnchor: selectedScalarRange?.from,
3354
3399
  selectionHead: selectedScalarRange?.to,
3400
+ authorizedSelectionUtf16Range: authorizedSelectionUtf16Range,
3401
+ rawSelectionUtf16Range: rawSelectionUtf16Range,
3402
+ selectionRevision: selectionRevision,
3355
3403
  capturedWhileFirstResponder: isFirstResponder || capturedAfterBlur,
3356
3404
  capturedWhileEditable: isEditable,
3357
3405
  capturedAfterBlur: capturedAfterBlur,
@@ -3362,9 +3410,37 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
3362
3410
  private func nativeTextMutationWithCurrentSelection(
3363
3411
  _ mutation: NativeTextMutation
3364
3412
  ) -> NativeTextMutation {
3365
- let selectedScalarRange = selectedTextRange.map {
3366
- PositionBridge.textRangeToScalarRange($0, in: self)
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
3367
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
3368
3444
  return NativeTextMutation(
3369
3445
  from: mutation.from,
3370
3446
  to: mutation.to,
@@ -3373,6 +3449,13 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
3373
3449
  authorizedText: mutation.authorizedText,
3374
3450
  selectionAnchor: selectedScalarRange?.from ?? mutation.selectionAnchor,
3375
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,
3376
3459
  capturedWhileFirstResponder: mutation.capturedWhileFirstResponder,
3377
3460
  capturedWhileEditable: mutation.capturedWhileEditable,
3378
3461
  capturedAfterBlur: mutation.capturedAfterBlur,
@@ -3380,6 +3463,104 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
3380
3463
  )
3381
3464
  }
3382
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
+
3383
3564
  private func isUtf16ScalarBoundary(_ offset: Int, in text: String) -> Bool {
3384
3565
  guard offset >= 0, offset <= text.utf16.count else { return false }
3385
3566
  let utf16Index = text.utf16.index(text.utf16.startIndex, offsetBy: offset)
@@ -3451,20 +3632,25 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
3451
3632
  clearNativeTextMutationAfterBlurWindow()
3452
3633
  }
3453
3634
 
3635
+ private func resetPendingNativeTextMutationState() {
3636
+ pendingNativeTextMutation = nil
3637
+ nativeTextMutationCommitScheduled = false
3638
+ advanceNativeTextMutationGeneration()
3639
+ }
3640
+
3454
3641
  func expireNativeTextMutationAfterBlurDeadlineForTesting() {
3455
3642
  nativeTextMutationAfterBlurDeadline = ProcessInfo.processInfo.systemUptime - 0.001
3456
3643
  }
3457
3644
 
3458
3645
  func discardTransientNativeInputForEditorRebind() {
3459
- pendingNativeTextMutation = nil
3460
- nativeTextMutationCommitScheduled = false
3646
+ resetPendingNativeTextMutationState()
3647
+ lastAuthorizedSelectedUtf16Range = nil
3461
3648
  clearPendingInputTraitRetry()
3462
3649
  markedTextReplacementScalarRange = nil
3463
3650
  markedTextReplacementUtf16Range = nil
3464
3651
  markedTextCompositionText = nil
3465
3652
  markedTextCompositionIsExplicitlyEmpty = false
3466
3653
  isComposing = false
3467
- advanceNativeTextMutationGeneration()
3468
3654
  }
3469
3655
 
3470
3656
  @discardableResult
@@ -3528,7 +3714,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
3528
3714
  ) -> Bool {
3529
3715
  guard nativeTextMutationCommitScheduled
3530
3716
  || pendingNativeTextMutation != nil
3531
- || (!isComposing && textStorage.string != lastAuthorizedText)
3717
+ || (!isComposing && markedTextRange == nil && textStorage.string != lastAuthorizedText)
3532
3718
  else {
3533
3719
  return true
3534
3720
  }
@@ -3658,8 +3844,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
3658
3844
  let targetRange = NSRange(location: startUtf16, length: max(0, endUtf16 - startUtf16))
3659
3845
  if selectedRange != targetRange {
3660
3846
  selectedRange = targetRange
3847
+ noteSelectionDidChange()
3661
3848
  }
3662
3849
  editorSetSelectionScalar(id: editorId, scalarAnchor: anchor, scalarHead: head)
3850
+ recordAuthorizedSelectionIfPossible()
3663
3851
  refreshTypingAttributesForSelection()
3664
3852
  let docAnchor = editorScalarToDoc(id: editorId, scalar: anchor)
3665
3853
  let docHead = editorScalarToDoc(id: editorId, scalar: head)
@@ -4783,9 +4971,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
4783
4971
  let update = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
4784
4972
  else { return }
4785
4973
  let parseNanos = DispatchTime.now().uptimeNanoseconds - parseStartedAt
4786
- pendingNativeTextMutation = nil
4787
- nativeTextMutationCommitScheduled = false
4788
- advanceNativeTextMutationGeneration()
4974
+ resetPendingNativeTextMutationState()
4789
4975
 
4790
4976
  let renderElements = update["renderElements"] as? [[String: Any]]
4791
4977
  let selectionFromUpdate = (update["selection"] as? [String: Any])
@@ -4951,6 +5137,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
4951
5137
  postApplyTrace.selectionOrContentCallbackNanos
4952
5138
  )
4953
5139
  }
5140
+ recordAuthorizedSelectionIfPossible()
4954
5141
  Self.updateLog.debug(
4955
5142
  "[applyUpdateJSON.end] finalSelection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
4956
5143
  )
@@ -4967,6 +5154,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
4967
5154
  /// elements directly, not wrapped in an EditorUpdate).
4968
5155
  func applyRenderJSON(_ renderJSON: String) {
4969
5156
  ensureInternalTextViewDelegate()
5157
+ resetPendingNativeTextMutationState()
4970
5158
  Self.updateLog.debug(
4971
5159
  "[applyRenderJSON.begin] before=\(self.textSnapshotSummary(), privacy: .public)"
4972
5160
  )
@@ -4982,6 +5170,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
4982
5170
 
4983
5171
  refreshPlaceholderVisibility()
4984
5172
  _ = performPostApplyMaintenance()
5173
+ recordAuthorizedSelectionIfPossible()
4985
5174
  Self.updateLog.debug(
4986
5175
  "[applyRenderJSON.end] after=\(self.textSnapshotSummary(), privacy: .public)"
4987
5176
  )
@@ -5039,17 +5228,20 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
5039
5228
  let adjustedRange = NSRange(location: adjustedOffset, length: 0)
5040
5229
  if selectedRange != adjustedRange {
5041
5230
  selectedRange = adjustedRange
5231
+ noteSelectionDidChange()
5042
5232
  }
5043
5233
  } else {
5044
5234
  let targetRange = NSRange(location: endUtf16, length: 0)
5045
5235
  if selectedRange != targetRange {
5046
5236
  selectedRange = targetRange
5237
+ noteSelectionDidChange()
5047
5238
  }
5048
5239
  }
5049
5240
  } else {
5050
5241
  let targetRange = NSRange(location: startUtf16, length: endUtf16 - startUtf16)
5051
5242
  if selectedRange != targetRange {
5052
5243
  selectedRange = targetRange
5244
+ noteSelectionDidChange()
5053
5245
  }
5054
5246
  }
5055
5247
  let assignmentNanos = DispatchTime.now().uptimeNanoseconds - assignmentStartedAt
@@ -5081,6 +5273,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
5081
5273
  let assignmentStartedAt = DispatchTime.now().uptimeNanoseconds
5082
5274
  if selectedRange != targetRange {
5083
5275
  selectedRange = targetRange
5276
+ noteSelectionDidChange()
5084
5277
  }
5085
5278
  let assignmentNanos = DispatchTime.now().uptimeNanoseconds - assignmentStartedAt
5086
5279
  let chromeStartedAt = DispatchTime.now().uptimeNanoseconds
@@ -5099,6 +5292,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
5099
5292
  case "all":
5100
5293
  let assignmentStartedAt = DispatchTime.now().uptimeNanoseconds
5101
5294
  selectedTextRange = textRange(from: beginningOfDocument, to: endOfDocument)
5295
+ noteSelectionDidChange()
5102
5296
  let assignmentNanos = DispatchTime.now().uptimeNanoseconds - assignmentStartedAt
5103
5297
  let chromeStartedAt = DispatchTime.now().uptimeNanoseconds
5104
5298
  showNativeSelectionChromeIfNeeded()
@@ -5150,8 +5344,11 @@ extension EditorTextView: NSTextStorageDelegate {
5150
5344
  // Only care about actual character edits, not attribute-only changes.
5151
5345
  guard editedMask.contains(.editedCharacters) else { return }
5152
5346
 
5153
- // Skip if this change came from our own Rust apply path or transient IME composition.
5154
- guard !isApplyingRustState, !isComposing else { return }
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 }
5155
5352
 
5156
5353
  // Skip if no editor is bound yet (nothing to reconcile against).
5157
5354
  guard editorId != 0 else { return }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollohg/react-native-prose-editor",
3
- "version": "0.5.17",
3
+ "version": "0.5.19",
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",