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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,13 @@
1
1
  package com.apollohg.editor
2
2
 
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import android.os.SystemClock
6
+ import android.text.Selection
3
7
  import android.view.KeyEvent
4
8
  import android.view.inputmethod.BaseInputConnection
9
+ import android.view.inputmethod.CompletionInfo
10
+ import android.view.inputmethod.CorrectionInfo
5
11
  import android.view.inputmethod.InputConnection
6
12
  import android.view.inputmethod.InputConnectionWrapper
7
13
 
@@ -30,10 +36,32 @@ import android.view.inputmethod.InputConnectionWrapper
30
36
  */
31
37
  class EditorInputConnection(
32
38
  private val editorView: EditorEditText,
33
- baseConnection: InputConnection
39
+ baseConnection: InputConnection,
40
+ private val boundEditorId: Long,
41
+ private val boundGeneration: Long
34
42
  ) : InputConnectionWrapper(baseConnection, true) {
43
+ private data class SurroundingDeleteRange(
44
+ val scalarStart: Int,
45
+ val scalarEnd: Int
46
+ )
47
+
35
48
 
36
49
  companion object {
50
+ private fun textTraceSummary(text: CharSequence?): String {
51
+ if (text == null) return "text=null"
52
+ val value = text.toString()
53
+ val codePoints = mutableListOf<String>()
54
+ var index = 0
55
+ while (index < value.length && codePoints.size < 4) {
56
+ val codePoint = Character.codePointAt(value, index)
57
+ codePoints.add(codePoint.toString(16))
58
+ index += Character.charCount(codePoint)
59
+ }
60
+ return "textLength=${value.length} codePoints=${codePoints.joinToString(",")}"
61
+ }
62
+
63
+ private const val DUPLICATE_CORRECTION_COMMIT_WINDOW_MS = 1_000L
64
+
37
65
  internal fun codePointsToUtf16Length(
38
66
  text: String,
39
67
  fromUtf16Offset: Int,
@@ -69,10 +97,20 @@ class EditorInputConnection(
69
97
  }
70
98
  }
71
99
 
72
- /** Tracks the current composing text for CJK/swipe input. */
73
- private var composingText: String? = null
74
- private var composingReplacementStartUtf16: Int? = null
75
- private var composingReplacementEndUtf16: Int? = null
100
+ private data class PendingDuplicateCorrectionCommit(
101
+ val text: String,
102
+ val deadlineMs: Long
103
+ )
104
+
105
+ private data class PendingCompositionCorrectionCommit(
106
+ val text: String,
107
+ val deadlineMs: Long,
108
+ val generation: Long
109
+ )
110
+
111
+ private var pendingDuplicateCorrectionCommit: PendingDuplicateCorrectionCommit? = null
112
+ private var pendingCompositionCorrectionCommit: PendingCompositionCorrectionCommit? = null
113
+ private var pendingCompositionCorrectionGeneration: Long = 0L
76
114
 
77
115
  /**
78
116
  * Called when the IME commits finalized text (single character, word,
@@ -81,31 +119,272 @@ class EditorInputConnection(
81
119
  * Routes the text through Rust instead of directly inserting into the EditText.
82
120
  */
83
121
  override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean {
84
- if (!editorView.isEditable) return false
122
+ if (!isCurrentInputSessionFor("commitText")) return true
123
+ if (!editorView.isEditable) return true
85
124
  if (editorView.isApplyingRustState) {
125
+ editorView.recordImeTraceForTesting(
126
+ "commitTextPassthrough",
127
+ "reason=applyingRust ${textTraceSummary(text)} cursor=$newCursorPosition"
128
+ )
86
129
  return super.commitText(text, newCursorPosition)
87
130
  }
88
131
  if (editorView.editorId == 0L) {
132
+ editorView.recordImeTraceForTesting(
133
+ "commitTextPassthrough",
134
+ "reason=noEditor ${textTraceSummary(text)} cursor=$newCursorPosition"
135
+ )
89
136
  return super.commitText(text, newCursorPosition)
90
137
  }
91
138
 
139
+ editorView.recordImeTraceForTesting(
140
+ "commitText",
141
+ "${textTraceSummary(text)} cursor=$newCursorPosition"
142
+ )
92
143
  val committedText = text?.toString()
93
- val replacementRange = trackedCompositionReplacementRange()
94
- if (replacementRange != null && committedText != null) {
144
+ if (consumePendingCompositionCorrectionCommitIfNeeded(committedText, newCursorPosition)) {
145
+ return true
146
+ }
147
+ applyPendingCompositionCorrectionCommitIfNeeded("commitTextBeforePlain")
148
+ if (consumePendingDuplicateCorrectionCommitIfNeeded(committedText)) {
149
+ editorView.recordImeTraceForTesting(
150
+ "commitTextDuplicateCorrectionIgnored",
151
+ "textLength=${committedText?.length ?: 0}"
152
+ )
153
+ return true
154
+ }
155
+ commitTextToEditor(committedText, newCursorPosition)
156
+ return true
157
+ }
158
+
159
+ override fun commitCompletion(text: CompletionInfo?): Boolean {
160
+ if (!isCurrentInputSessionFor("commitCompletion")) return true
161
+ if (!editorView.isEditable) return true
162
+ if (editorView.isApplyingRustState) {
163
+ return super.commitCompletion(text)
164
+ }
165
+ if (editorView.editorId == 0L) {
166
+ return super.commitCompletion(text)
167
+ }
168
+ editorView.recordImeTraceForTesting(
169
+ "commitCompletion",
170
+ textTraceSummary(text?.text)
171
+ )
172
+ commitTextToEditor(text?.text?.toString(), 1)
173
+ return true
174
+ }
175
+
176
+ override fun getCursorCapsMode(reqModes: Int): Int {
177
+ val baseCapsMode = super.getCursorCapsMode(reqModes)
178
+ if (!isCurrentInputSession()) return baseCapsMode
179
+ val capsMode = editorView.cursorCapsModeForEditor(reqModes, baseCapsMode)
180
+ if (capsMode != baseCapsMode) {
181
+ editorView.recordImeTraceForTesting(
182
+ "getCursorCapsModeAdjusted",
183
+ "req=$reqModes base=$baseCapsMode caps=$capsMode"
184
+ )
185
+ }
186
+ return capsMode
187
+ }
188
+
189
+ override fun getTextBeforeCursor(n: Int, flags: Int): CharSequence? {
190
+ if (!isCurrentInputSession()) return super.getTextBeforeCursor(n, flags)
191
+ val textBeforeCursor = editorView.textBeforeCursorForImeContextForEditor(n, flags)
192
+ ?: return super.getTextBeforeCursor(n, flags)
193
+ val raw = super.getTextBeforeCursor(n, flags)
194
+ if (raw?.toString() != textBeforeCursor.toString()) {
195
+ editorView.recordImeTraceForTesting(
196
+ "getTextBeforeCursorAdjusted",
197
+ "requested=$n rawLength=${raw?.length ?: -1} adjustedLength=${textBeforeCursor.length}"
198
+ )
199
+ }
200
+ return textBeforeCursor
201
+ }
202
+
203
+ override fun commitCorrection(correctionInfo: CorrectionInfo?): Boolean {
204
+ if (!isCurrentInputSessionFor("commitCorrection")) return true
205
+ if (!editorView.isEditable) return true
206
+ if (editorView.isApplyingRustState) {
207
+ return super.commitCorrection(correctionInfo)
208
+ }
209
+ if (editorView.editorId == 0L) {
210
+ return super.commitCorrection(correctionInfo)
211
+ }
212
+ val newText = correctionInfo?.newText?.toString()
213
+ if (newText == null) return true
214
+ editorView.recordImeTraceForTesting(
215
+ "commitCorrection",
216
+ "offset=${correctionInfo.offset} oldMissing=${correctionInfo.oldText == null} newLength=${newText.length}"
217
+ )
218
+ if (trackedCompositionReplacementRange() != null) {
219
+ editorView.recordImeTraceForTesting(
220
+ "commitCorrectionComposition",
221
+ "newLength=${newText.length}"
222
+ )
223
+ rememberPendingCompositionCorrectionCommit(newText)
224
+ return true
225
+ }
226
+ if (consumeInvalidatedCompositionReplacementRangeAndRestore()) {
227
+ editorView.recordImeTraceForTesting("commitCorrectionRestoredInvalidComposition")
228
+ return true
229
+ }
230
+ val oldText = correctionInfo.oldText?.toString()
231
+ val offset = correctionInfo.offset
232
+ val applied = if (oldText != null && offset >= 0) {
233
+ editorView.handleCorrectionCommit(offset, oldText, newText)
234
+ } else if (oldText == null) {
235
+ editorView.handleMissingOldTextCorrectionCommit(offset, newText)
236
+ } else {
237
+ false
238
+ }
239
+ editorView.recordImeTraceForTesting(
240
+ "commitCorrectionResult",
241
+ "applied=$applied"
242
+ )
243
+ if (applied) {
244
+ rememberPendingDuplicateCorrectionCommit(newText)
245
+ }
246
+ return true
247
+ }
248
+
249
+ private fun rememberPendingDuplicateCorrectionCommit(text: String) {
250
+ pendingDuplicateCorrectionCommit = PendingDuplicateCorrectionCommit(
251
+ text = text,
252
+ deadlineMs = SystemClock.uptimeMillis() + DUPLICATE_CORRECTION_COMMIT_WINDOW_MS
253
+ )
254
+ }
255
+
256
+ private fun consumePendingDuplicateCorrectionCommitIfNeeded(text: String?): Boolean {
257
+ val pending = pendingDuplicateCorrectionCommit ?: return false
258
+ pendingDuplicateCorrectionCommit = null
259
+ if (text == null) return false
260
+ if (SystemClock.uptimeMillis() > pending.deadlineMs) return false
261
+ return text == pending.text
262
+ }
263
+
264
+ private fun rememberPendingCompositionCorrectionCommit(text: String) {
265
+ val generation = ++pendingCompositionCorrectionGeneration
266
+ pendingCompositionCorrectionCommit = PendingCompositionCorrectionCommit(
267
+ text = text,
268
+ deadlineMs = SystemClock.uptimeMillis() + DUPLICATE_CORRECTION_COMMIT_WINDOW_MS,
269
+ generation = generation
270
+ )
271
+ Handler(Looper.getMainLooper()).post {
272
+ val pending = pendingCompositionCorrectionCommit ?: return@post
273
+ if (pending.generation != generation) return@post
274
+ applyPendingCompositionCorrectionCommitIfNeeded("commitCorrectionDeferred")
275
+ }
276
+ }
277
+
278
+ private fun consumePendingCompositionCorrectionCommitIfNeeded(
279
+ text: String?,
280
+ newCursorPosition: Int
281
+ ): Boolean {
282
+ val pending = pendingCompositionCorrectionCommit ?: return false
283
+ if (SystemClock.uptimeMillis() > pending.deadlineMs) {
284
+ pendingCompositionCorrectionCommit = null
285
+ return false
286
+ }
287
+ if (text != pending.text) return false
288
+ pendingCompositionCorrectionCommit = null
289
+ pendingCompositionCorrectionGeneration += 1L
290
+ editorView.recordImeTraceForTesting(
291
+ "commitTextConsumesPendingCorrection",
292
+ "textLength=${text.length}"
293
+ )
294
+ commitTextToEditor(text, newCursorPosition)
295
+ return true
296
+ }
297
+
298
+ private fun applyPendingCompositionCorrectionCommitIfNeeded(source: String): Boolean {
299
+ val pending = pendingCompositionCorrectionCommit ?: return false
300
+ pendingCompositionCorrectionCommit = null
301
+ pendingCompositionCorrectionGeneration += 1L
302
+ if (!isCurrentInputSessionFor("applyPendingCompositionCorrection")) return false
303
+ if (!editorView.isEditable || editorView.editorId == 0L) return false
304
+ editorView.recordImeTraceForTesting(
305
+ "applyPendingCompositionCorrection",
306
+ "source=$source textLength=${pending.text.length}"
307
+ )
308
+ commitTextToEditor(pending.text, 1)
309
+ return true
310
+ }
311
+
312
+ private fun commitTextToEditor(committedText: String?, newCursorPosition: Int) {
313
+ val startedAt = System.nanoTime()
314
+ val trackedReplacementRange = trackedCompositionReplacementRange()
315
+ val rawComposingSpanRange = currentComposingSpanRawRange()
316
+ val currentAuthorizedComposingSpanRange = currentComposingSpanRange()
317
+ val visibleReplacementRange = rawComposingSpanRange ?: trackedReplacementRange
318
+ val replacementRange = trackedReplacementRange?.let { range ->
319
+ if (range.first == range.second) {
320
+ currentAuthorizedComposingSpanRange ?: range
321
+ } else {
322
+ range
323
+ }
324
+ }
325
+ if (replacementRange != null) {
326
+ editorView.recordImeTraceForTesting(
327
+ "commitTextRoute",
328
+ "route=composition replacement=${replacementRange.first}..${replacementRange.second} visible=${visibleReplacementRange?.first}..${visibleReplacementRange?.second} textLength=${committedText?.length ?: 0}"
329
+ )
95
330
  clearCompositionTracking()
96
331
  editorView.runWithTransientInputMutationGuard {
97
332
  super.finishComposingText()
98
333
  }
99
- editorView.handleCompositionCommit(
100
- committedText,
101
- replacementRange.first,
102
- replacementRange.second
103
- )
334
+ if (committedText != null) {
335
+ var didCommitAlreadyVisibleMutation = false
336
+ if (
337
+ trackedReplacementRange?.first == trackedReplacementRange?.second &&
338
+ rawComposingSpanRange == null
339
+ ) {
340
+ editorView.runWithDeferredRustUpdateApplication {
341
+ didCommitAlreadyVisibleMutation =
342
+ editorView.commitAlreadyVisibleCompositionMutationForPendingImeOperationForEditor(
343
+ committedText,
344
+ newCursorPosition
345
+ )
346
+ }
347
+ }
348
+ if (!didCommitAlreadyVisibleMutation) {
349
+ visibleReplacementRange?.let { visibleRange ->
350
+ editorView.applyVisibleCompositionCommitForPendingImeOperationForEditor(
351
+ committedText,
352
+ visibleRange.first,
353
+ visibleRange.second,
354
+ newCursorPosition
355
+ )
356
+ }
357
+ editorView.runWithDeferredRustUpdateApplication {
358
+ editorView.handleCompositionCommit(
359
+ committedText,
360
+ replacementRange.first,
361
+ replacementRange.second,
362
+ newCursorPosition
363
+ )
364
+ }
365
+ }
366
+ } else {
367
+ editorView.restoreAuthorizedTextIfNeeded()
368
+ }
104
369
  } else {
370
+ if (consumeInvalidatedCompositionReplacementRangeAndRestore()) {
371
+ editorView.recordImeTraceForTesting(
372
+ "commitTextRoute",
373
+ "route=restoreInvalidComposition textLength=${committedText?.length ?: 0}"
374
+ )
375
+ return
376
+ }
105
377
  clearCompositionTracking()
106
- committedText?.let { editorView.handleTextCommit(it) }
378
+ editorView.recordImeTraceForTesting(
379
+ "commitTextRoute",
380
+ "route=plain textLength=${committedText?.length ?: 0}"
381
+ )
382
+ committedText?.let { editorView.handleTextCommit(it, newCursorPosition) }
107
383
  }
108
- return true
384
+ editorView.recordImeTraceForTesting(
385
+ "commitTextRouteDone",
386
+ "textLength=${committedText?.length ?: 0} totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
387
+ )
109
388
  }
110
389
 
111
390
  /**
@@ -117,32 +396,97 @@ class EditorInputConnection(
117
396
  * @param afterLength Number of UTF-16 code units to delete after the cursor.
118
397
  */
119
398
  override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
120
- if (!editorView.isEditable) return false
399
+ if (!isCurrentInputSessionFor("deleteSurroundingText")) return true
400
+ if (!editorView.isEditable) return true
121
401
  if (editorView.isApplyingRustState) {
122
402
  return super.deleteSurroundingText(beforeLength, afterLength)
123
403
  }
404
+ editorView.recordImeTraceForTesting(
405
+ "deleteSurroundingText",
406
+ "before=$beforeLength after=$afterLength"
407
+ )
408
+ if (
409
+ editorView.hasInvalidatedCompositionReplacementRangeForEditor() &&
410
+ isNoOpSurroundingDelete(beforeLength, afterLength)
411
+ ) {
412
+ return finishStaleComposingUpdateAfterInvalidation()
413
+ }
414
+ if (consumeInvalidatedCompositionReplacementRangeAndRestore()) {
415
+ return true
416
+ }
124
417
  if (trackedCompositionReplacementRange() != null) {
418
+ val beforeText = editorView.text?.toString()
419
+ var didFallbackDelete = false
125
420
  val result = editorView.runWithTransientInputMutationGuard {
126
- super.deleteSurroundingText(beforeLength, afterLength)
421
+ val baseResult = super.deleteSurroundingText(beforeLength, afterLength)
422
+ if (
423
+ beforeText != null &&
424
+ beforeText == editorView.text?.toString() &&
425
+ (beforeLength > 0 || afterLength > 0)
426
+ ) {
427
+ didFallbackDelete = deleteTransientTextAroundSelection(beforeLength, afterLength)
428
+ }
429
+ baseResult
127
430
  }
128
431
  refreshComposingTextFromEditable()
129
- return result
432
+ return result || didFallbackDelete
433
+ }
434
+ if (shouldDeferPlainSurroundingDelete(beforeLength, afterLength)) {
435
+ return performDeferredPlainSurroundingDelete(
436
+ beforeLength = beforeLength,
437
+ afterLength = afterLength,
438
+ deleteInCodePoints = false
439
+ )
130
440
  }
131
441
  editorView.handleDelete(beforeLength, afterLength)
132
442
  return true
133
443
  }
134
444
 
135
445
  override fun deleteSurroundingTextInCodePoints(beforeLength: Int, afterLength: Int): Boolean {
136
- if (!editorView.isEditable) return false
446
+ if (!isCurrentInputSessionFor("deleteSurroundingTextInCodePoints")) return true
447
+ if (!editorView.isEditable) return true
137
448
  if (editorView.isApplyingRustState) {
138
449
  return super.deleteSurroundingTextInCodePoints(beforeLength, afterLength)
139
450
  }
451
+ editorView.recordImeTraceForTesting(
452
+ "deleteSurroundingTextInCodePoints",
453
+ "before=$beforeLength after=$afterLength"
454
+ )
455
+ if (
456
+ editorView.hasInvalidatedCompositionReplacementRangeForEditor() &&
457
+ isNoOpSurroundingDelete(beforeLength, afterLength)
458
+ ) {
459
+ return finishStaleComposingUpdateAfterInvalidation()
460
+ }
461
+ if (consumeInvalidatedCompositionReplacementRangeAndRestore()) {
462
+ return true
463
+ }
140
464
  if (trackedCompositionReplacementRange() != null) {
465
+ val beforeText = editorView.text?.toString()
466
+ var didFallbackDelete = false
141
467
  val result = editorView.runWithTransientInputMutationGuard {
142
- super.deleteSurroundingTextInCodePoints(beforeLength, afterLength)
468
+ val baseResult = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength)
469
+ if (
470
+ beforeText != null &&
471
+ beforeText == editorView.text?.toString() &&
472
+ (beforeLength > 0 || afterLength > 0)
473
+ ) {
474
+ didFallbackDelete = deleteTransientTextAroundSelectionInCodePoints(
475
+ beforeLength,
476
+ afterLength
477
+ )
478
+ }
479
+ baseResult
143
480
  }
144
481
  refreshComposingTextFromEditable()
145
- return result
482
+ return result || didFallbackDelete
483
+ }
484
+ if (shouldDeferPlainSurroundingDelete(beforeLength, afterLength)) {
485
+ return performDeferredPlainSurroundingDelete(
486
+ beforeLength = beforeLength,
487
+ afterLength = afterLength,
488
+ deleteInCodePoints = true
489
+ )
146
490
  }
147
491
 
148
492
  val currentText = editorView.text?.toString().orEmpty()
@@ -163,6 +507,114 @@ class EditorInputConnection(
163
507
  return true
164
508
  }
165
509
 
510
+ private fun shouldDeferPlainSurroundingDelete(beforeLength: Int, afterLength: Int): Boolean =
511
+ beforeLength.coerceAtLeast(0) + afterLength.coerceAtLeast(0) > 0
512
+
513
+ private fun performDeferredPlainSurroundingDelete(
514
+ beforeLength: Int,
515
+ afterLength: Int,
516
+ deleteInCodePoints: Boolean
517
+ ): Boolean {
518
+ val beforeText = editorView.text?.toString() ?: return true
519
+ val beforeUtf16Length: Int
520
+ val afterUtf16Length: Int
521
+ if (deleteInCodePoints) {
522
+ val cursor = editorView.selectionStart.coerceAtLeast(0)
523
+ beforeUtf16Length = codePointsToUtf16Length(
524
+ text = beforeText,
525
+ fromUtf16Offset = cursor,
526
+ codePointCount = beforeLength,
527
+ forward = false
528
+ )
529
+ afterUtf16Length = codePointsToUtf16Length(
530
+ text = beforeText,
531
+ fromUtf16Offset = editorView.selectionEnd.coerceAtLeast(cursor),
532
+ codePointCount = afterLength,
533
+ forward = true
534
+ )
535
+ } else {
536
+ beforeUtf16Length = beforeLength
537
+ afterUtf16Length = afterLength
538
+ }
539
+ val deleteRange = surroundingDeleteRange(
540
+ text = beforeText,
541
+ beforeUtf16Length = beforeUtf16Length,
542
+ afterUtf16Length = afterUtf16Length
543
+ )
544
+
545
+ editorView.recordImeTraceForTesting(
546
+ "deferredSurroundingDeleteBegin",
547
+ "before=$beforeLength after=$afterLength codePoints=$deleteInCodePoints utf16=$beforeUtf16Length,$afterUtf16Length scalar=${deleteRange?.scalarStart}..${deleteRange?.scalarEnd}"
548
+ )
549
+
550
+ var didFallbackDelete = false
551
+ val result = editorView.runWithTransientInputMutationGuard {
552
+ val baseResult = if (deleteInCodePoints) {
553
+ super.deleteSurroundingTextInCodePoints(beforeLength, afterLength)
554
+ } else {
555
+ super.deleteSurroundingText(beforeLength, afterLength)
556
+ }
557
+ if (
558
+ beforeText == editorView.text?.toString() &&
559
+ (beforeLength > 0 || afterLength > 0)
560
+ ) {
561
+ didFallbackDelete = if (deleteInCodePoints) {
562
+ deleteTransientTextAroundSelectionInCodePoints(beforeLength, afterLength)
563
+ } else {
564
+ deleteTransientTextAroundSelection(beforeLength, afterLength)
565
+ }
566
+ }
567
+ baseResult
568
+ }
569
+ val didDeleteVisibleText = editorView.text?.toString() != beforeText
570
+ if (didDeleteVisibleText && deleteRange != null) {
571
+ editorView.authorizeCurrentVisibleTextForPendingImeOperationForEditor()
572
+ editorView.runWithDeferredRustUpdateApplication {
573
+ editorView.deleteScalarRangeForPendingImeOperationForEditor(
574
+ deleteRange.scalarStart,
575
+ deleteRange.scalarEnd
576
+ )
577
+ }
578
+ }
579
+ editorView.recordImeTraceForTesting(
580
+ "deferredSurroundingDeleteEnd",
581
+ "result=$result fallback=$didFallbackDelete visibleDeleted=$didDeleteVisibleText visibleLength=${editorView.text?.length ?: -1}"
582
+ )
583
+ return result || didFallbackDelete
584
+ }
585
+
586
+ private fun surroundingDeleteRange(
587
+ text: String,
588
+ beforeUtf16Length: Int,
589
+ afterUtf16Length: Int
590
+ ): SurroundingDeleteRange? {
591
+ val rawStart = editorView.selectionStart
592
+ val rawEnd = editorView.selectionEnd
593
+ if (rawStart < 0 || rawEnd < 0) return null
594
+ val selectionStart = rawStart.coerceIn(0, text.length)
595
+ val selectionEnd = rawEnd.coerceIn(0, text.length)
596
+ val normalizedStart = minOf(selectionStart, selectionEnd)
597
+ val normalizedEnd = maxOf(selectionStart, selectionEnd)
598
+ val rawDeleteStart: Int
599
+ val rawDeleteEnd: Int
600
+ if (normalizedStart != normalizedEnd) {
601
+ rawDeleteStart = normalizedStart
602
+ rawDeleteEnd = normalizedEnd
603
+ } else {
604
+ rawDeleteStart = maxOf(0, normalizedStart - beforeUtf16Length.coerceAtLeast(0))
605
+ rawDeleteEnd = minOf(text.length, normalizedEnd + afterUtf16Length.coerceAtLeast(0))
606
+ }
607
+ val (deleteStart, deleteEnd) = PositionBridge.snapRangeToScalarBoundaries(
608
+ rawDeleteStart,
609
+ rawDeleteEnd,
610
+ text
611
+ )
612
+ val scalarStart = PositionBridge.utf16ToScalar(deleteStart, text)
613
+ val scalarEnd = PositionBridge.utf16ToScalar(deleteEnd, text)
614
+ if (scalarStart >= scalarEnd) return null
615
+ return SurroundingDeleteRange(scalarStart, scalarEnd)
616
+ }
617
+
166
618
  /**
167
619
  * Called when the IME sets composing (in-progress) text for CJK/swipe input.
168
620
  *
@@ -171,26 +623,76 @@ class EditorInputConnection(
171
623
  * to Rust during composition — only when the IME commits or finishes it.
172
624
  */
173
625
  override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean {
174
- if (!editorView.isEditable) return super.setComposingText(text, newCursorPosition)
626
+ if (!isCurrentInputSessionFor("setComposingText")) return true
627
+ if (!editorView.isEditable) return true
175
628
  if (editorView.editorId == 0L) return super.setComposingText(text, newCursorPosition)
629
+ if (editorView.hasInvalidatedCompositionReplacementRangeForEditor()) {
630
+ return finishStaleComposingUpdateAfterInvalidation()
631
+ }
176
632
  captureCompositionReplacementRangeIfNeeded()
177
- composingText = text?.toString()
633
+ val composingText = text?.toString()
634
+ val adjustedComposingText =
635
+ editorView.samsungSentenceCapsComposingTextForEditor(composingText)
636
+ val textForBaseConnection = if (adjustedComposingText != composingText) {
637
+ adjustedComposingText
638
+ } else {
639
+ text
640
+ }
641
+ editorView.recordImeTraceForTesting(
642
+ "setComposingText",
643
+ "${textTraceSummary(text)} cursor=$newCursorPosition adjusted=${adjustedComposingText != composingText}"
644
+ )
645
+ editorView.setComposingTextForEditor(adjustedComposingText)
178
646
  return editorView.runWithTransientInputMutationGuard {
179
- super.setComposingText(text, newCursorPosition)
647
+ val result = super.setComposingText(textForBaseConnection, newCursorPosition)
648
+ if (result) {
649
+ editorView.applyTransientComposingTextStyleForEditor()
650
+ }
651
+ result
180
652
  }
181
653
  }
182
654
 
183
655
  override fun setComposingRegion(start: Int, end: Int): Boolean {
184
- if (!editorView.isEditable) return super.setComposingRegion(start, end)
656
+ if (!isCurrentInputSessionFor("setComposingRegion")) return true
657
+ if (!editorView.isEditable) return true
185
658
  if (editorView.editorId == 0L) return super.setComposingRegion(start, end)
186
- val authorizedLength = editorView.text?.length ?: 0
187
- composingReplacementStartUtf16 = minOf(start, end).coerceIn(0, authorizedLength)
188
- composingReplacementEndUtf16 = maxOf(start, end).coerceIn(0, authorizedLength)
659
+ if (editorView.hasInvalidatedCompositionReplacementRangeForEditor()) {
660
+ return finishStaleComposingUpdateAfterInvalidation()
661
+ }
662
+ if (editorView.isCurrentTextAuthorizedForEditor()) {
663
+ editorView.setCompositionReplacementRange(start, end)
664
+ }
665
+ editorView.recordImeTraceForTesting(
666
+ "setComposingRegion",
667
+ "range=$start..$end"
668
+ )
189
669
  return editorView.runWithTransientInputMutationGuard {
190
- super.setComposingRegion(start, end)
670
+ val result = super.setComposingRegion(start, end)
671
+ if (result) {
672
+ editorView.applyTransientComposingTextStyleForEditor()
673
+ }
674
+ result
191
675
  }
192
676
  }
193
677
 
678
+ override fun setSelection(start: Int, end: Int): Boolean {
679
+ if (!isCurrentInputSessionFor("setSelection")) return true
680
+ if (!editorView.isEditable) {
681
+ consumeInvalidatedCompositionReplacementRangeAndRestore()
682
+ return true
683
+ }
684
+ if (editorView.isApplyingRustState) {
685
+ return super.setSelection(start, end)
686
+ }
687
+ if (editorView.editorId == 0L) {
688
+ return super.setSelection(start, end)
689
+ }
690
+ if (editorView.hasInvalidatedCompositionReplacementRangeForEditor()) {
691
+ return finishStaleComposingUpdateAfterInvalidation()
692
+ }
693
+ return super.setSelection(start, end)
694
+ }
695
+
194
696
  /**
195
697
  * Called when IME composition is finalized (user selects a candidate or
196
698
  * presses space/enter to commit the composing text).
@@ -199,10 +701,67 @@ class EditorInputConnection(
199
701
  * so it can capture the result and send it to Rust.
200
702
  */
201
703
  override fun finishComposingText(): Boolean {
202
- if (!editorView.isEditable) return super.finishComposingText()
704
+ if (applyPendingCompositionCorrectionCommitIfNeeded("finishComposingText")) return true
705
+ return finishComposingTextInternal(blockWhenCompositionWasCancelled = false)
706
+ }
707
+
708
+ internal fun flushPendingCompositionForExternalMutation(): Boolean {
709
+ if (!isCurrentInputSessionFor("flushPendingComposition")) return true
710
+ if (!hasPendingComposition()) return true
711
+ return finishComposingTextInternal(blockWhenCompositionWasCancelled = true)
712
+ }
713
+
714
+ internal fun hasPendingComposition(): Boolean {
715
+ if (!isCurrentInputSessionFor("hasPendingComposition")) return false
716
+ if (trackedCompositionReplacementRange() != null) return true
717
+ val editable = editorView.text ?: return false
718
+ val start = BaseInputConnection.getComposingSpanStart(editable)
719
+ val end = BaseInputConnection.getComposingSpanEnd(editable)
720
+ return start >= 0 && end >= 0 && start != end
721
+ }
722
+
723
+ internal fun refreshComposingTextFromEditableForEditor() {
724
+ if (!isCurrentInputSessionFor("refreshComposingText")) return
725
+ refreshComposingTextFromEditable()
726
+ }
727
+
728
+ internal fun clearCompositionTrackingForEditor() {
729
+ if (!isCurrentInputSessionFor("clearCompositionTracking")) return
730
+ clearCompositionTracking()
731
+ }
732
+
733
+ internal fun deleteTransientTextForHardwareKeyEvent(event: KeyEvent): Boolean =
734
+ if (!isCurrentInputSession()) {
735
+ false
736
+ } else {
737
+ when (event.keyCode) {
738
+ KeyEvent.KEYCODE_DEL -> deleteTransientTextAroundSelectionInCodePoints(1, 0)
739
+ KeyEvent.KEYCODE_FORWARD_DEL -> deleteTransientTextAroundSelectionInCodePoints(0, 1)
740
+ else -> false
741
+ }
742
+ }
743
+
744
+ private fun finishComposingTextInternal(blockWhenCompositionWasCancelled: Boolean): Boolean {
745
+ if (!isCurrentInputSessionFor("finishComposingText")) return true
746
+ if (!editorView.isEditable) {
747
+ clearCompositionTracking()
748
+ editorView.restoreAuthorizedTextIfNeeded()
749
+ return true
750
+ }
203
751
  if (editorView.editorId == 0L) return super.finishComposingText()
204
- val composed = composingText
205
- val replacementRange = trackedCompositionReplacementRange()
752
+ refreshComposingTextFromEditable()
753
+ val composed = editorView.composingTextForEditor() ?: currentComposingSpanText()
754
+ val trackedReplacementRange = trackedCompositionReplacementRange()
755
+ val didInvalidateReplacementRange = consumeInvalidatedCompositionReplacementRange()
756
+ val replacementRange = if (didInvalidateReplacementRange) {
757
+ null
758
+ } else {
759
+ trackedReplacementRange ?: currentComposingSpanRange()
760
+ }
761
+ editorView.recordImeTraceForTesting(
762
+ "finishComposingText",
763
+ "replacement=${replacementRange?.first}..${replacementRange?.second} composedLength=${composed?.length ?: 0} invalidated=$didInvalidateReplacementRange"
764
+ )
206
765
  clearCompositionTracking()
207
766
 
208
767
  // Prevent selection sync while the base connection commits the composed
@@ -212,46 +771,183 @@ class EditorInputConnection(
212
771
  }
213
772
 
214
773
  // Now route the composed text through Rust.
215
- if (replacementRange != null && !composed.isNullOrEmpty()) {
216
- editorView.handleCompositionCommit(
217
- composed,
218
- replacementRange.first,
219
- replacementRange.second
220
- )
774
+ if (
775
+ replacementRange != null &&
776
+ (!composed.isNullOrEmpty() || replacementRange.first != replacementRange.second)
777
+ ) {
778
+ editorView.runWithDeferredRustUpdateApplication {
779
+ editorView.handleCompositionCommit(
780
+ composed.orEmpty(),
781
+ replacementRange.first,
782
+ replacementRange.second
783
+ )
784
+ }
785
+ return true
786
+ } else if (replacementRange != null) {
787
+ editorView.restoreAuthorizedTextIfNeeded()
788
+ return !blockWhenCompositionWasCancelled
789
+ } else if (didInvalidateReplacementRange) {
790
+ editorView.restoreAuthorizedTextIfNeeded()
791
+ return !blockWhenCompositionWasCancelled
221
792
  }
222
793
  return result
223
794
  }
224
795
 
225
796
  private fun captureCompositionReplacementRangeIfNeeded() {
226
- if (trackedCompositionReplacementRange() != null) return
227
- val start = editorView.selectionStart.coerceAtLeast(0)
228
- val end = editorView.selectionEnd.coerceAtLeast(0)
229
- val authorizedLength = editorView.text?.length ?: 0
230
- composingReplacementStartUtf16 = minOf(start, end).coerceIn(0, authorizedLength)
231
- composingReplacementEndUtf16 = maxOf(start, end).coerceIn(0, authorizedLength)
797
+ editorView.captureCompositionReplacementRangeIfNeeded()
232
798
  }
233
799
 
234
800
  private fun trackedCompositionReplacementRange(): Pair<Int, Int>? {
235
- val start = composingReplacementStartUtf16 ?: return null
236
- val end = composingReplacementEndUtf16 ?: return null
237
- return start to end
801
+ return editorView.compositionReplacementRange()
238
802
  }
239
803
 
240
804
  private fun clearCompositionTracking() {
241
- composingText = null
242
- composingReplacementStartUtf16 = null
243
- composingReplacementEndUtf16 = null
805
+ editorView.clearCompositionTrackingForEditor()
806
+ }
807
+
808
+ private fun consumeInvalidatedCompositionReplacementRange(): Boolean =
809
+ editorView.consumeInvalidatedCompositionReplacementRangeForEditor()
810
+
811
+ private fun consumeInvalidatedCompositionReplacementRangeAndRestore(): Boolean {
812
+ if (!consumeInvalidatedCompositionReplacementRange()) return false
813
+ clearCompositionTracking()
814
+ editorView.runWithTransientInputMutationGuard {
815
+ super.finishComposingText()
816
+ }
817
+ editorView.restoreAuthorizedTextIfNeeded()
818
+ return true
819
+ }
820
+
821
+ private fun finishStaleComposingUpdateAfterInvalidation(): Boolean {
822
+ clearCompositionTracking()
823
+ val result = editorView.runWithTransientInputMutationGuard {
824
+ super.finishComposingText()
825
+ }
826
+ editorView.restoreAuthorizedTextIfNeeded()
827
+ return result
828
+ }
829
+
830
+ private fun isCurrentInputSession(): Boolean =
831
+ editorView.isInputConnectionCurrentForEditor(boundEditorId, boundGeneration)
832
+
833
+ private fun nanosToMicros(nanos: Long): Long = nanos / 1_000L
834
+
835
+ private fun isCurrentInputSessionFor(event: String): Boolean {
836
+ val isCurrent = isCurrentInputSession()
837
+ if (!isCurrent) {
838
+ editorView.recordImeTraceForTesting(
839
+ "${event}Ignored",
840
+ "reason=stale boundEditor=$boundEditorId boundGen=$boundGeneration"
841
+ )
842
+ }
843
+ return isCurrent
244
844
  }
245
845
 
246
846
  private fun refreshComposingTextFromEditable() {
247
847
  val editable = editorView.text ?: return
848
+ val visibleReplacementText = editorView.composingTextFromVisibleReplacementForEditor()
849
+ if (visibleReplacementText != null) {
850
+ editorView.setComposingTextForEditor(visibleReplacementText)
851
+ return
852
+ }
248
853
  val start = BaseInputConnection.getComposingSpanStart(editable)
249
854
  val end = BaseInputConnection.getComposingSpanEnd(editable)
250
855
  if (start < 0 || end < 0 || start > end || end > editable.length) {
251
- composingText = null
856
+ editorView.setComposingTextForEditor(null)
252
857
  return
253
858
  }
254
- composingText = editable.subSequence(start, end).toString()
859
+ editorView.setComposingTextForEditor(editable.subSequence(start, end).toString())
860
+ }
861
+
862
+ private fun deleteTransientTextAroundSelection(beforeLength: Int, afterLength: Int): Boolean {
863
+ val editable = editorView.text ?: return false
864
+ val rawStart = editorView.selectionStart
865
+ val rawEnd = editorView.selectionEnd
866
+ if (rawStart < 0 || rawEnd < 0) return false
867
+ val selectionStart = rawStart.coerceIn(0, editable.length)
868
+ val selectionEnd = rawEnd.coerceIn(0, editable.length)
869
+ val normalizedStart = minOf(selectionStart, selectionEnd)
870
+ val normalizedEnd = maxOf(selectionStart, selectionEnd)
871
+ val deleteStart: Int
872
+ val deleteEnd: Int
873
+ if (normalizedStart != normalizedEnd) {
874
+ deleteStart = normalizedStart
875
+ deleteEnd = normalizedEnd
876
+ } else {
877
+ deleteStart = maxOf(0, normalizedStart - beforeLength.coerceAtLeast(0))
878
+ deleteEnd = minOf(editable.length, normalizedEnd + afterLength.coerceAtLeast(0))
879
+ }
880
+ if (deleteStart >= deleteEnd) return false
881
+ val (snappedStart, snappedEnd) = PositionBridge.snapRangeToScalarBoundaries(
882
+ deleteStart,
883
+ deleteEnd,
884
+ editable.toString()
885
+ )
886
+ if (snappedStart >= snappedEnd) return false
887
+ editable.delete(snappedStart, snappedEnd)
888
+ Selection.setSelection(editable, snappedStart.coerceIn(0, editable.length))
889
+ return true
890
+ }
891
+
892
+ private fun deleteTransientTextAroundSelectionInCodePoints(
893
+ beforeLength: Int,
894
+ afterLength: Int
895
+ ): Boolean {
896
+ val currentText = editorView.text?.toString() ?: return false
897
+ val rawStart = editorView.selectionStart
898
+ val rawEnd = editorView.selectionEnd
899
+ if (rawStart < 0 || rawEnd < 0) return false
900
+ val selectionStart = rawStart.coerceIn(0, currentText.length)
901
+ val selectionEnd = rawEnd.coerceIn(0, currentText.length)
902
+ val normalizedStart = minOf(selectionStart, selectionEnd)
903
+ val normalizedEnd = maxOf(selectionStart, selectionEnd)
904
+ if (normalizedStart != normalizedEnd) {
905
+ return deleteTransientTextAroundSelection(0, 0)
906
+ }
907
+ val beforeUtf16Length = codePointsToUtf16Length(
908
+ text = currentText,
909
+ fromUtf16Offset = normalizedStart,
910
+ codePointCount = beforeLength,
911
+ forward = false
912
+ )
913
+ val afterUtf16Length = codePointsToUtf16Length(
914
+ text = currentText,
915
+ fromUtf16Offset = normalizedEnd,
916
+ codePointCount = afterLength,
917
+ forward = true
918
+ )
919
+ return deleteTransientTextAroundSelection(beforeUtf16Length, afterUtf16Length)
920
+ }
921
+
922
+ private fun currentComposingSpanText(): String? {
923
+ val editable = editorView.text ?: return null
924
+ val start = BaseInputConnection.getComposingSpanStart(editable)
925
+ val end = BaseInputConnection.getComposingSpanEnd(editable)
926
+ if (start < 0 || end < 0 || start > end || end > editable.length) {
927
+ return null
928
+ }
929
+ return editable.subSequence(start, end).toString()
930
+ }
931
+
932
+ private fun currentComposingSpanRange(): Pair<Int, Int>? {
933
+ if (!editorView.isCurrentTextAuthorizedForEditor()) return null
934
+ val editable = editorView.text ?: return null
935
+ val start = BaseInputConnection.getComposingSpanStart(editable)
936
+ val end = BaseInputConnection.getComposingSpanEnd(editable)
937
+ if (start < 0 || end < 0 || start > end || end > editable.length) {
938
+ return null
939
+ }
940
+ return editorView.authorizedUtf16Range(start, end)
941
+ }
942
+
943
+ private fun currentComposingSpanRawRange(): Pair<Int, Int>? {
944
+ val editable = editorView.text ?: return null
945
+ val start = BaseInputConnection.getComposingSpanStart(editable)
946
+ val end = BaseInputConnection.getComposingSpanEnd(editable)
947
+ if (start < 0 || end < 0 || start > end || end > editable.length) {
948
+ return null
949
+ }
950
+ return start to end
255
951
  }
256
952
 
257
953
  /**
@@ -261,20 +957,43 @@ class EditorInputConnection(
261
957
  * events are passed through to the base connection.
262
958
  */
263
959
  override fun sendKeyEvent(event: KeyEvent?): Boolean {
264
- if (!editorView.isEditable) return false
265
- if (event != null && event.action == KeyEvent.ACTION_DOWN) {
266
- if (editorView.handleHardwareKeyDown(event.keyCode, event.isShiftPressed)) {
267
- return true
268
- }
960
+ if (!isCurrentInputSession()) return true
961
+ if (
962
+ event?.action == KeyEvent.ACTION_UP &&
963
+ editorView.hasInvalidatedCompositionReplacementRangeForEditor()
964
+ ) {
965
+ return finishStaleComposingUpdateAfterInvalidation()
269
966
  }
270
- if (event != null && event.action == KeyEvent.ACTION_UP) {
271
- when (event.keyCode) {
272
- KeyEvent.KEYCODE_DEL,
273
- KeyEvent.KEYCODE_ENTER,
274
- KeyEvent.KEYCODE_NUMPAD_ENTER,
275
- KeyEvent.KEYCODE_TAB -> return true
276
- }
967
+ if (
968
+ shouldConsumeInvalidatedCompositionForKeyEvent(event) &&
969
+ consumeInvalidatedCompositionReplacementRangeAndRestore()
970
+ ) {
971
+ return true
972
+ }
973
+ if (!editorView.isEditable && event?.let { editorView.isReadOnlyTextMutationKeyEvent(it) } == true) {
974
+ return true
975
+ }
976
+ if (event != null && editorView.handleCompositionKeyEvent(event) {
977
+ super.sendKeyEvent(event)
978
+ }) {
979
+ return true
980
+ }
981
+ if (event != null && editorView.handleHardwareKeyEvent(event)) {
982
+ return true
983
+ }
984
+ if (event != null && editorView.handlePrintableHardwareKeyEvent(event) {
985
+ super.sendKeyEvent(event)
986
+ }) {
987
+ return true
277
988
  }
278
989
  return super.sendKeyEvent(event)
279
990
  }
991
+
992
+ private fun shouldConsumeInvalidatedCompositionForKeyEvent(event: KeyEvent?): Boolean {
993
+ if (event == null || event.action == KeyEvent.ACTION_UP) return false
994
+ return editorView.isReadOnlyTextMutationKeyEvent(event)
995
+ }
996
+
997
+ private fun isNoOpSurroundingDelete(beforeLength: Int, afterLength: Int): Boolean =
998
+ beforeLength <= 0 && afterLength <= 0
280
999
  }