@openeditor/react-native-prose-editor 0.0.8 → 0.0.10

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,1009 +0,0 @@
1
- package com.openeditor.editor
2
-
3
- import android.os.Handler
4
- import android.os.Looper
5
- import android.os.SystemClock
6
- import android.text.Selection
7
- import android.view.KeyEvent
8
- import android.view.inputmethod.BaseInputConnection
9
- import android.view.inputmethod.CompletionInfo
10
- import android.view.inputmethod.CorrectionInfo
11
- import android.view.inputmethod.InputConnection
12
- import android.view.inputmethod.InputConnectionWrapper
13
-
14
- /**
15
- * Custom [InputConnectionWrapper] that intercepts all text input from the soft keyboard
16
- * and routes it through the Rust editor-core engine via the hosting [EditorEditText].
17
- *
18
- * Instead of letting Android's EditText text storage handle insertions and deletions
19
- * directly, this class captures the user's intent (typing, deleting, IME composition)
20
- * and delegates to the Rust editor. The Rust editor returns the canonical
21
- * plain-text input snapshot applied to the hidden EditText adapter; the
22
- * recursive block tree is rendered by [NativeBlockEditorSurface].
23
- *
24
- * ## Composition (IME) Handling
25
- *
26
- * For CJK input methods, swipe keyboards, and some autocorrect flows, [setComposingText],
27
- * [commitText], and [finishComposingText] are used together. During composition, we let
28
- * the base [InputConnection] render transient composing text, but keep the original
29
- * Rust-authorized replacement range so the final committed text lands at the correct
30
- * document position.
31
- *
32
- * ## Key Events
33
- *
34
- * Hardware keyboard events (backspace, enter) arrive via [sendKeyEvent]. We intercept
35
- * DEL and ENTER to route through the Rust editor.
36
- */
37
- class EditorInputConnection(
38
- private val editorView: EditorEditText,
39
- baseConnection: InputConnection,
40
- private val boundEditorId: Long,
41
- private val boundGeneration: Long
42
- ) : InputConnectionWrapper(baseConnection, true) {
43
- private data class SurroundingDeleteRange(
44
- val scalarStart: Int,
45
- val scalarEnd: Int
46
- )
47
-
48
-
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
-
65
- internal fun codePointsToUtf16Length(
66
- text: String,
67
- fromUtf16Offset: Int,
68
- codePointCount: Int,
69
- forward: Boolean
70
- ): Int {
71
- if (codePointCount <= 0 || text.isEmpty()) return 0
72
-
73
- var remaining = codePointCount
74
- var utf16Length = 0
75
-
76
- if (forward) {
77
- var index = fromUtf16Offset.coerceIn(0, text.length)
78
- while (index < text.length && remaining > 0) {
79
- val codePoint = Character.codePointAt(text, index)
80
- val charCount = Character.charCount(codePoint)
81
- utf16Length += charCount
82
- index += charCount
83
- remaining--
84
- }
85
- } else {
86
- var index = fromUtf16Offset.coerceIn(0, text.length)
87
- while (index > 0 && remaining > 0) {
88
- val codePoint = Character.codePointBefore(text, index)
89
- val charCount = Character.charCount(codePoint)
90
- utf16Length += charCount
91
- index -= charCount
92
- remaining--
93
- }
94
- }
95
-
96
- return utf16Length
97
- }
98
- }
99
-
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
114
-
115
- /**
116
- * Called when the IME commits finalized text (single character, word,
117
- * autocomplete selection, etc.).
118
- *
119
- * Routes the text through Rust instead of directly inserting into the EditText.
120
- */
121
- override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean {
122
- if (!isCurrentInputSessionFor("commitText")) return true
123
- if (!editorView.isEditable) return true
124
- if (editorView.isApplyingRustState) {
125
- editorView.recordImeTraceForTesting(
126
- "commitTextPassthrough",
127
- "reason=applyingRust ${textTraceSummary(text)} cursor=$newCursorPosition"
128
- )
129
- return super.commitText(text, newCursorPosition)
130
- }
131
- if (editorView.editorId == 0L) {
132
- editorView.recordImeTraceForTesting(
133
- "commitTextPassthrough",
134
- "reason=noEditor ${textTraceSummary(text)} cursor=$newCursorPosition"
135
- )
136
- return super.commitText(text, newCursorPosition)
137
- }
138
-
139
- editorView.recordImeTraceForTesting(
140
- "commitText",
141
- "${textTraceSummary(text)} cursor=$newCursorPosition"
142
- )
143
- val committedText = text?.toString()
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
- )
330
- clearCompositionTracking()
331
- editorView.runWithTransientInputMutationGuard {
332
- super.finishComposingText()
333
- }
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
- }
369
- } else {
370
- if (consumeInvalidatedCompositionReplacementRangeAndRestore()) {
371
- editorView.recordImeTraceForTesting(
372
- "commitTextRoute",
373
- "route=restoreInvalidComposition textLength=${committedText?.length ?: 0}"
374
- )
375
- return
376
- }
377
- clearCompositionTracking()
378
- editorView.recordImeTraceForTesting(
379
- "commitTextRoute",
380
- "route=plain textLength=${committedText?.length ?: 0}"
381
- )
382
- committedText?.let { editorView.handleTextCommit(it, newCursorPosition) }
383
- }
384
- editorView.recordImeTraceForTesting(
385
- "commitTextRouteDone",
386
- "textLength=${committedText?.length ?: 0} totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
387
- )
388
- }
389
-
390
- /**
391
- * Called when the IME requests deletion of text surrounding the cursor.
392
- *
393
- * Routes the deletion through Rust instead of directly modifying the EditText.
394
- *
395
- * @param beforeLength Number of UTF-16 code units to delete before the cursor.
396
- * @param afterLength Number of UTF-16 code units to delete after the cursor.
397
- */
398
- override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
399
- if (!isCurrentInputSessionFor("deleteSurroundingText")) return true
400
- if (!editorView.isEditable) return true
401
- if (editorView.isApplyingRustState) {
402
- return super.deleteSurroundingText(beforeLength, afterLength)
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
- }
417
- if (trackedCompositionReplacementRange() != null) {
418
- val beforeText = editorView.text?.toString()
419
- var didFallbackDelete = false
420
- val result = editorView.runWithTransientInputMutationGuard {
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
430
- }
431
- refreshComposingTextFromEditable()
432
- return result || didFallbackDelete
433
- }
434
- if (shouldDeferPlainSurroundingDelete(beforeLength, afterLength)) {
435
- return performDeferredPlainSurroundingDelete(
436
- beforeLength = beforeLength,
437
- afterLength = afterLength,
438
- deleteInCodePoints = false
439
- )
440
- }
441
- editorView.handleDelete(beforeLength, afterLength)
442
- return true
443
- }
444
-
445
- override fun deleteSurroundingTextInCodePoints(beforeLength: Int, afterLength: Int): Boolean {
446
- if (!isCurrentInputSessionFor("deleteSurroundingTextInCodePoints")) return true
447
- if (!editorView.isEditable) return true
448
- if (editorView.isApplyingRustState) {
449
- return super.deleteSurroundingTextInCodePoints(beforeLength, afterLength)
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
- }
464
- if (trackedCompositionReplacementRange() != null) {
465
- val beforeText = editorView.text?.toString()
466
- var didFallbackDelete = false
467
- val result = editorView.runWithTransientInputMutationGuard {
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
480
- }
481
- refreshComposingTextFromEditable()
482
- return result || didFallbackDelete
483
- }
484
- if (shouldDeferPlainSurroundingDelete(beforeLength, afterLength)) {
485
- return performDeferredPlainSurroundingDelete(
486
- beforeLength = beforeLength,
487
- afterLength = afterLength,
488
- deleteInCodePoints = true
489
- )
490
- }
491
-
492
- val currentText = editorView.text?.toString().orEmpty()
493
- val cursor = editorView.selectionStart.coerceAtLeast(0)
494
- val beforeUtf16Length = codePointsToUtf16Length(
495
- text = currentText,
496
- fromUtf16Offset = cursor,
497
- codePointCount = beforeLength,
498
- forward = false
499
- )
500
- val afterUtf16Length = codePointsToUtf16Length(
501
- text = currentText,
502
- fromUtf16Offset = editorView.selectionEnd.coerceAtLeast(cursor),
503
- codePointCount = afterLength,
504
- forward = true
505
- )
506
- editorView.handleDelete(beforeUtf16Length, afterUtf16Length)
507
- return true
508
- }
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
-
618
- /**
619
- * Called when the IME sets composing (in-progress) text for CJK/swipe input.
620
- *
621
- * We let the base InputConnection handle this normally so the user sees
622
- * the composing text with its underline decoration. The text is NOT sent
623
- * to Rust during composition — only when the IME commits or finishes it.
624
- */
625
- override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean {
626
- if (!isCurrentInputSessionFor("setComposingText")) return true
627
- if (!editorView.isEditable) return true
628
- if (editorView.editorId == 0L) return super.setComposingText(text, newCursorPosition)
629
- if (editorView.hasInvalidatedCompositionReplacementRangeForEditor()) {
630
- return finishStaleComposingUpdateAfterInvalidation()
631
- }
632
- captureCompositionReplacementRangeIfNeeded()
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)
646
- return editorView.runWithTransientInputMutationGuard {
647
- editorView.selectSyntheticPlaceholderForTransientComposition()
648
- val result = super.setComposingText(textForBaseConnection, newCursorPosition)
649
- if (result) {
650
- // BaseInputConnection implementations (notably some OEM IMEs,
651
- // and Robolectric's faithful approximation of them) may leave
652
- // the composing range selected. Android's cursor contract says
653
- // a positive newCursorPosition places the caret after the new
654
- // composing text. Normalize that state here so the next
655
- // hardware character extends, rather than replaces, it.
656
- if (newCursorPosition > 0) {
657
- editorView.collapseSelectionToComposingEndForEditor()
658
- }
659
- editorView.applyTransientComposingTextStyleForEditor()
660
- }
661
- result
662
- }
663
- }
664
-
665
- override fun setComposingRegion(start: Int, end: Int): Boolean {
666
- if (!isCurrentInputSessionFor("setComposingRegion")) return true
667
- if (!editorView.isEditable) return true
668
- if (editorView.editorId == 0L) return super.setComposingRegion(start, end)
669
- if (editorView.hasInvalidatedCompositionReplacementRangeForEditor()) {
670
- return finishStaleComposingUpdateAfterInvalidation()
671
- }
672
- if (editorView.isCurrentTextAuthorizedForEditor()) {
673
- editorView.setCompositionReplacementRange(start, end)
674
- }
675
- editorView.recordImeTraceForTesting(
676
- "setComposingRegion",
677
- "range=$start..$end"
678
- )
679
- return editorView.runWithTransientInputMutationGuard {
680
- val result = super.setComposingRegion(start, end)
681
- if (result) {
682
- editorView.applyTransientComposingTextStyleForEditor()
683
- }
684
- result
685
- }
686
- }
687
-
688
- override fun setSelection(start: Int, end: Int): Boolean {
689
- if (!isCurrentInputSessionFor("setSelection")) return true
690
- if (!editorView.isEditable) {
691
- consumeInvalidatedCompositionReplacementRangeAndRestore()
692
- return true
693
- }
694
- if (editorView.isApplyingRustState) {
695
- return super.setSelection(start, end)
696
- }
697
- if (editorView.editorId == 0L) {
698
- return super.setSelection(start, end)
699
- }
700
- if (editorView.hasInvalidatedCompositionReplacementRangeForEditor()) {
701
- return finishStaleComposingUpdateAfterInvalidation()
702
- }
703
- return super.setSelection(start, end)
704
- }
705
-
706
- /**
707
- * Called when IME composition is finalized (user selects a candidate or
708
- * presses space/enter to commit the composing text).
709
- *
710
- * At this point, the composed text is final. We notify the [EditorEditText]
711
- * so it can capture the result and send it to Rust.
712
- */
713
- override fun finishComposingText(): Boolean {
714
- if (applyPendingCompositionCorrectionCommitIfNeeded("finishComposingText")) return true
715
- return finishComposingTextInternal(blockWhenCompositionWasCancelled = false)
716
- }
717
-
718
- internal fun flushPendingCompositionForExternalMutation(): Boolean {
719
- if (!isCurrentInputSessionFor("flushPendingComposition")) return true
720
- if (!hasPendingComposition()) return true
721
- return finishComposingTextInternal(blockWhenCompositionWasCancelled = true)
722
- }
723
-
724
- internal fun hasPendingComposition(): Boolean {
725
- if (!isCurrentInputSessionFor("hasPendingComposition")) return false
726
- if (trackedCompositionReplacementRange() != null) return true
727
- val editable = editorView.text ?: return false
728
- val start = BaseInputConnection.getComposingSpanStart(editable)
729
- val end = BaseInputConnection.getComposingSpanEnd(editable)
730
- return start >= 0 && end >= 0 && start != end
731
- }
732
-
733
- internal fun refreshComposingTextFromEditableForEditor() {
734
- if (!isCurrentInputSessionFor("refreshComposingText")) return
735
- refreshComposingTextFromEditable()
736
- }
737
-
738
- internal fun clearCompositionTrackingForEditor() {
739
- if (!isCurrentInputSessionFor("clearCompositionTracking")) return
740
- clearCompositionTracking()
741
- }
742
-
743
- internal fun deleteTransientTextForHardwareKeyEvent(event: KeyEvent): Boolean =
744
- if (!isCurrentInputSession()) {
745
- false
746
- } else {
747
- when (event.keyCode) {
748
- KeyEvent.KEYCODE_DEL -> deleteTransientTextAroundSelectionInCodePoints(1, 0)
749
- KeyEvent.KEYCODE_FORWARD_DEL -> deleteTransientTextAroundSelectionInCodePoints(0, 1)
750
- else -> false
751
- }
752
- }
753
-
754
- private fun finishComposingTextInternal(blockWhenCompositionWasCancelled: Boolean): Boolean {
755
- if (!isCurrentInputSessionFor("finishComposingText")) return true
756
- if (!editorView.isEditable) {
757
- clearCompositionTracking()
758
- editorView.restoreAuthorizedTextIfNeeded()
759
- return true
760
- }
761
- if (editorView.editorId == 0L) return super.finishComposingText()
762
- refreshComposingTextFromEditable()
763
- val composed = editorView.composingTextForEditor() ?: currentComposingSpanText()
764
- val trackedReplacementRange = trackedCompositionReplacementRange()
765
- val didInvalidateReplacementRange = consumeInvalidatedCompositionReplacementRange()
766
- val replacementRange = if (didInvalidateReplacementRange) {
767
- null
768
- } else {
769
- trackedReplacementRange ?: currentComposingSpanRange()
770
- }
771
- editorView.recordImeTraceForTesting(
772
- "finishComposingText",
773
- "replacement=${replacementRange?.first}..${replacementRange?.second} composedLength=${composed?.length ?: 0} invalidated=$didInvalidateReplacementRange"
774
- )
775
- clearCompositionTracking()
776
-
777
- // Prevent selection sync while the base connection commits the composed
778
- // text, since the Rust document doesn't have it yet.
779
- val result = editorView.runWithTransientInputMutationGuard {
780
- super.finishComposingText()
781
- }
782
-
783
- // Now route the composed text through Rust.
784
- if (
785
- replacementRange != null &&
786
- (!composed.isNullOrEmpty() || replacementRange.first != replacementRange.second)
787
- ) {
788
- editorView.runWithDeferredRustUpdateApplication {
789
- editorView.handleCompositionCommit(
790
- composed.orEmpty(),
791
- replacementRange.first,
792
- replacementRange.second
793
- )
794
- }
795
- return true
796
- } else if (replacementRange != null) {
797
- editorView.restoreAuthorizedTextIfNeeded()
798
- return !blockWhenCompositionWasCancelled
799
- } else if (didInvalidateReplacementRange) {
800
- editorView.restoreAuthorizedTextIfNeeded()
801
- return !blockWhenCompositionWasCancelled
802
- }
803
- return result
804
- }
805
-
806
- private fun captureCompositionReplacementRangeIfNeeded() {
807
- editorView.captureCompositionReplacementRangeIfNeeded()
808
- }
809
-
810
- private fun trackedCompositionReplacementRange(): Pair<Int, Int>? {
811
- return editorView.compositionReplacementRange()
812
- }
813
-
814
- private fun clearCompositionTracking() {
815
- editorView.clearCompositionTrackingForEditor()
816
- }
817
-
818
- private fun consumeInvalidatedCompositionReplacementRange(): Boolean =
819
- editorView.consumeInvalidatedCompositionReplacementRangeForEditor()
820
-
821
- private fun consumeInvalidatedCompositionReplacementRangeAndRestore(): Boolean {
822
- if (!consumeInvalidatedCompositionReplacementRange()) return false
823
- clearCompositionTracking()
824
- editorView.runWithTransientInputMutationGuard {
825
- super.finishComposingText()
826
- }
827
- editorView.restoreAuthorizedTextIfNeeded()
828
- return true
829
- }
830
-
831
- private fun finishStaleComposingUpdateAfterInvalidation(): Boolean {
832
- clearCompositionTracking()
833
- val result = editorView.runWithTransientInputMutationGuard {
834
- super.finishComposingText()
835
- }
836
- editorView.restoreAuthorizedTextIfNeeded()
837
- return result
838
- }
839
-
840
- private fun isCurrentInputSession(): Boolean =
841
- editorView.isInputConnectionCurrentForEditor(boundEditorId, boundGeneration)
842
-
843
- private fun nanosToMicros(nanos: Long): Long = nanos / 1_000L
844
-
845
- private fun isCurrentInputSessionFor(event: String): Boolean {
846
- val isCurrent = isCurrentInputSession()
847
- if (!isCurrent) {
848
- editorView.recordImeTraceForTesting(
849
- "${event}Ignored",
850
- "reason=stale boundEditor=$boundEditorId boundGen=$boundGeneration"
851
- )
852
- }
853
- return isCurrent
854
- }
855
-
856
- private fun refreshComposingTextFromEditable() {
857
- val editable = editorView.text ?: return
858
- val visibleReplacementText = editorView.composingTextFromVisibleReplacementForEditor()
859
- if (visibleReplacementText != null) {
860
- editorView.setComposingTextForEditor(visibleReplacementText)
861
- return
862
- }
863
- val start = BaseInputConnection.getComposingSpanStart(editable)
864
- val end = BaseInputConnection.getComposingSpanEnd(editable)
865
- if (start < 0 || end < 0 || start > end || end > editable.length) {
866
- editorView.setComposingTextForEditor(null)
867
- return
868
- }
869
- editorView.setComposingTextForEditor(editable.subSequence(start, end).toString())
870
- }
871
-
872
- private fun deleteTransientTextAroundSelection(beforeLength: Int, afterLength: Int): Boolean {
873
- val editable = editorView.text ?: return false
874
- val rawStart = editorView.selectionStart
875
- val rawEnd = editorView.selectionEnd
876
- if (rawStart < 0 || rawEnd < 0) return false
877
- val selectionStart = rawStart.coerceIn(0, editable.length)
878
- val selectionEnd = rawEnd.coerceIn(0, editable.length)
879
- val normalizedStart = minOf(selectionStart, selectionEnd)
880
- val normalizedEnd = maxOf(selectionStart, selectionEnd)
881
- val deleteStart: Int
882
- val deleteEnd: Int
883
- if (normalizedStart != normalizedEnd) {
884
- deleteStart = normalizedStart
885
- deleteEnd = normalizedEnd
886
- } else {
887
- deleteStart = maxOf(0, normalizedStart - beforeLength.coerceAtLeast(0))
888
- deleteEnd = minOf(editable.length, normalizedEnd + afterLength.coerceAtLeast(0))
889
- }
890
- if (deleteStart >= deleteEnd) return false
891
- val (snappedStart, snappedEnd) = PositionBridge.snapRangeToScalarBoundaries(
892
- deleteStart,
893
- deleteEnd,
894
- editable.toString()
895
- )
896
- if (snappedStart >= snappedEnd) return false
897
- editable.delete(snappedStart, snappedEnd)
898
- Selection.setSelection(editable, snappedStart.coerceIn(0, editable.length))
899
- return true
900
- }
901
-
902
- private fun deleteTransientTextAroundSelectionInCodePoints(
903
- beforeLength: Int,
904
- afterLength: Int
905
- ): Boolean {
906
- val currentText = editorView.text?.toString() ?: return false
907
- val rawStart = editorView.selectionStart
908
- val rawEnd = editorView.selectionEnd
909
- if (rawStart < 0 || rawEnd < 0) return false
910
- val selectionStart = rawStart.coerceIn(0, currentText.length)
911
- val selectionEnd = rawEnd.coerceIn(0, currentText.length)
912
- val normalizedStart = minOf(selectionStart, selectionEnd)
913
- val normalizedEnd = maxOf(selectionStart, selectionEnd)
914
- if (normalizedStart != normalizedEnd) {
915
- return deleteTransientTextAroundSelection(0, 0)
916
- }
917
- val beforeUtf16Length = codePointsToUtf16Length(
918
- text = currentText,
919
- fromUtf16Offset = normalizedStart,
920
- codePointCount = beforeLength,
921
- forward = false
922
- )
923
- val afterUtf16Length = codePointsToUtf16Length(
924
- text = currentText,
925
- fromUtf16Offset = normalizedEnd,
926
- codePointCount = afterLength,
927
- forward = true
928
- )
929
- return deleteTransientTextAroundSelection(beforeUtf16Length, afterUtf16Length)
930
- }
931
-
932
- private fun currentComposingSpanText(): String? {
933
- val editable = editorView.text ?: return null
934
- val start = BaseInputConnection.getComposingSpanStart(editable)
935
- val end = BaseInputConnection.getComposingSpanEnd(editable)
936
- if (start < 0 || end < 0 || start > end || end > editable.length) {
937
- return null
938
- }
939
- return editable.subSequence(start, end).toString()
940
- }
941
-
942
- private fun currentComposingSpanRange(): Pair<Int, Int>? {
943
- if (!editorView.isCurrentTextAuthorizedForEditor()) return null
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 editorView.authorizedUtf16Range(start, end)
951
- }
952
-
953
- private fun currentComposingSpanRawRange(): Pair<Int, Int>? {
954
- val editable = editorView.text ?: return null
955
- val start = BaseInputConnection.getComposingSpanStart(editable)
956
- val end = BaseInputConnection.getComposingSpanEnd(editable)
957
- if (start < 0 || end < 0 || start > end || end > editable.length) {
958
- return null
959
- }
960
- return start to end
961
- }
962
-
963
- /**
964
- * Called for hardware keyboard key events.
965
- *
966
- * Intercepts DEL (backspace) and ENTER to route through Rust. Other key
967
- * events are passed through to the base connection.
968
- */
969
- override fun sendKeyEvent(event: KeyEvent?): Boolean {
970
- if (!isCurrentInputSession()) return true
971
- if (
972
- event?.action == KeyEvent.ACTION_UP &&
973
- editorView.hasInvalidatedCompositionReplacementRangeForEditor()
974
- ) {
975
- return finishStaleComposingUpdateAfterInvalidation()
976
- }
977
- if (
978
- shouldConsumeInvalidatedCompositionForKeyEvent(event) &&
979
- consumeInvalidatedCompositionReplacementRangeAndRestore()
980
- ) {
981
- return true
982
- }
983
- if (!editorView.isEditable && event?.let { editorView.isReadOnlyTextMutationKeyEvent(it) } == true) {
984
- return true
985
- }
986
- if (event != null && editorView.handleCompositionKeyEvent(event) {
987
- super.sendKeyEvent(event)
988
- }) {
989
- return true
990
- }
991
- if (event != null && editorView.handleHardwareKeyEvent(event)) {
992
- return true
993
- }
994
- if (event != null && editorView.handlePrintableHardwareKeyEvent(event) {
995
- super.sendKeyEvent(event)
996
- }) {
997
- return true
998
- }
999
- return super.sendKeyEvent(event)
1000
- }
1001
-
1002
- private fun shouldConsumeInvalidatedCompositionForKeyEvent(event: KeyEvent?): Boolean {
1003
- if (event == null || event.action == KeyEvent.ACTION_UP) return false
1004
- return editorView.isReadOnlyTextMutationKeyEvent(event)
1005
- }
1006
-
1007
- private fun isNoOpSurroundingDelete(beforeLength: Int, afterLength: Int): Boolean =
1008
- beforeLength <= 0 && afterLength <= 0
1009
- }