@apollohg/react-native-prose-editor 0.5.16 → 0.5.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1396 -143
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +403 -59
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1666 -79
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +209 -87
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +27 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +58 -9
- package/dist/NativeEditorBridge.d.ts +34 -1
- package/dist/NativeEditorBridge.js +243 -83
- package/dist/NativeRichTextEditor.js +998 -137
- package/dist/addons.d.ts +7 -0
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/NativeEditorExpoView.swift +830 -17
- package/ios/NativeEditorModule.swift +304 -108
- package/ios/PositionBridge.swift +24 -1
- package/ios/RichTextEditorView.swift +715 -89
- package/package.json +2 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
package com.apollohg.editor
|
|
2
2
|
|
|
3
|
+
import android.content.ClipData
|
|
3
4
|
import android.content.ClipboardManager
|
|
4
5
|
import android.content.Context
|
|
5
6
|
import android.graphics.Typeface
|
|
6
7
|
import android.graphics.Rect
|
|
7
8
|
import android.graphics.RectF
|
|
9
|
+
import android.os.SystemClock
|
|
8
10
|
import android.text.Annotation
|
|
9
11
|
import android.text.Editable
|
|
10
12
|
import android.text.InputType
|
|
11
13
|
import android.text.Layout
|
|
12
14
|
import android.text.Spanned
|
|
15
|
+
import android.text.SpannableStringBuilder
|
|
13
16
|
import android.text.StaticLayout
|
|
14
17
|
import android.text.TextPaint
|
|
15
18
|
import android.text.TextWatcher
|
|
@@ -80,6 +83,17 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
80
83
|
val label: String
|
|
81
84
|
)
|
|
82
85
|
|
|
86
|
+
data class CommandPreparation(
|
|
87
|
+
val ready: Boolean,
|
|
88
|
+
val updateJSON: String?
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
private data class HardwareKeyEventSignature(
|
|
92
|
+
val keyCode: Int,
|
|
93
|
+
val downTime: Long,
|
|
94
|
+
val repeatCount: Int
|
|
95
|
+
)
|
|
96
|
+
|
|
83
97
|
data class LinkHit(
|
|
84
98
|
val href: String,
|
|
85
99
|
val text: String
|
|
@@ -112,7 +126,21 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
112
126
|
val scalarFrom: Int,
|
|
113
127
|
val scalarTo: Int,
|
|
114
128
|
val replacementText: String,
|
|
115
|
-
val resultingText: String
|
|
129
|
+
val resultingText: String,
|
|
130
|
+
val selectionScalarAnchor: Int?,
|
|
131
|
+
val selectionScalarHead: Int?
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
private data class NativeTextMutationAfterBlurWindow(
|
|
135
|
+
val editorId: Long,
|
|
136
|
+
val authorizedTextRevision: Long,
|
|
137
|
+
val deadlineMs: Long,
|
|
138
|
+
var didAdoptMutation: Boolean = false
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
private data class NativeTextMutationAdoptionSuppression(
|
|
142
|
+
val editorId: Long,
|
|
143
|
+
val authorizedTextRevision: Long
|
|
116
144
|
)
|
|
117
145
|
|
|
118
146
|
/**
|
|
@@ -137,6 +165,13 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
137
165
|
* focus, text selection, and copy capability.
|
|
138
166
|
*/
|
|
139
167
|
var isEditable: Boolean = true
|
|
168
|
+
set(value) {
|
|
169
|
+
if (field == value) return
|
|
170
|
+
if (!value) {
|
|
171
|
+
discardTransientNativeInputForReadOnly()
|
|
172
|
+
}
|
|
173
|
+
field = value
|
|
174
|
+
}
|
|
140
175
|
|
|
141
176
|
/**
|
|
142
177
|
* Guard flag to prevent re-entrant input interception while we're
|
|
@@ -192,12 +227,25 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
192
227
|
var reconciliationCount: Int = 0
|
|
193
228
|
private set
|
|
194
229
|
|
|
195
|
-
private var
|
|
196
|
-
private var
|
|
230
|
+
private var lastHandledHardwareKeySignature: HardwareKeyEventSignature? = null
|
|
231
|
+
private var recentHandledHardwareKeyDownSignature: HardwareKeyEventSignature? = null
|
|
232
|
+
private var recentHandledHardwareKeyDownUptimeMs: Long = 0L
|
|
233
|
+
private var activeInputConnection: EditorInputConnection? = null
|
|
234
|
+
private var inputConnectionGeneration: Long = 0L
|
|
235
|
+
private var composingText: String? = null
|
|
236
|
+
private var composingReplacementStartUtf16: Int? = null
|
|
237
|
+
private var composingReplacementEndUtf16: Int? = null
|
|
238
|
+
private var composingReplacementAuthorizedTextRevision: Long? = null
|
|
239
|
+
private var didInvalidateCompositionReplacementRange = false
|
|
240
|
+
private var nativeTextMutationAfterBlurWindow: NativeTextMutationAfterBlurWindow? = null
|
|
241
|
+
private var nativeTextMutationAdoptionSuppression: NativeTextMutationAdoptionSuppression? = null
|
|
242
|
+
private var lastAuthorizedTextRevision: Long = 0L
|
|
243
|
+
private var lastAuthorizedRenderedText: CharSequence? = null
|
|
197
244
|
private var explicitSelectedImageRange: ImageSelectionRange? = null
|
|
198
245
|
private var lastRenderAppliedPatchForTesting: Boolean = false
|
|
199
246
|
internal var captureApplyUpdateTraceForTesting: Boolean = false
|
|
200
247
|
private var lastApplyUpdateTraceForTesting: ApplyUpdateTrace? = null
|
|
248
|
+
private val imeTraceForTesting = java.util.ArrayDeque<String>()
|
|
201
249
|
private var currentRenderBlocksJson: org.json.JSONArray? = null
|
|
202
250
|
private var renderAppearanceRevision: Long = 1L
|
|
203
251
|
private var lastAppliedRenderAppearanceRevision: Long = 0L
|
|
@@ -205,10 +253,32 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
205
253
|
internal var onDeleteBackwardAtSelectionScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
206
254
|
internal var onInsertTextInRustForTesting: ((String, Int) -> Unit)? = null
|
|
207
255
|
internal var onReplaceTextInRustForTesting: ((Int, Int, String) -> Unit)? = null
|
|
256
|
+
internal var onSetSelectionScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
257
|
+
internal var onDeleteAndSplitScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
258
|
+
internal var onInsertContentHtmlInRustForTesting: ((String) -> Unit)? = null
|
|
259
|
+
internal var onInsertContentJsonAtSelectionScalarForTesting: ((Int, Int, String) -> Unit)? = null
|
|
260
|
+
internal var blockExternalEditorUpdatePreparationForTesting = false
|
|
261
|
+
internal var blockExternalEditorCommandPreparationForTesting = false
|
|
208
262
|
|
|
209
263
|
fun lastRenderAppliedPatch(): Boolean = lastRenderAppliedPatchForTesting
|
|
210
264
|
fun lastApplyUpdateTrace(): ApplyUpdateTrace? = lastApplyUpdateTraceForTesting
|
|
211
265
|
|
|
266
|
+
internal fun recordImeTraceForTesting(event: String, details: String = "") {
|
|
267
|
+
if (imeTraceForTesting.size >= IME_TRACE_LIMIT_FOR_TESTING) {
|
|
268
|
+
imeTraceForTesting.removeFirst()
|
|
269
|
+
}
|
|
270
|
+
imeTraceForTesting.addLast(
|
|
271
|
+
if (details.isEmpty()) event else "$event:$details"
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
internal fun clearImeTraceForTesting() {
|
|
276
|
+
imeTraceForTesting.clear()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
internal fun imeTraceSnapshotForTesting(): List<String> =
|
|
280
|
+
imeTraceForTesting.toList()
|
|
281
|
+
|
|
212
282
|
init {
|
|
213
283
|
// Configure for rich text editing.
|
|
214
284
|
inputType = resolvedInputType()
|
|
@@ -283,22 +353,23 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
283
353
|
|
|
284
354
|
val currentStart = selectionStart
|
|
285
355
|
val currentEnd = selectionEnd
|
|
356
|
+
val authorizedSelection = authorizedSelectionForTransientInputRestore(
|
|
357
|
+
currentStart,
|
|
358
|
+
currentEnd
|
|
359
|
+
)
|
|
360
|
+
discardTransientInputAndRestoreAuthorizedTextForEditor()
|
|
286
361
|
setRawInputType(nextInputType)
|
|
287
362
|
|
|
288
363
|
val editable = text
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
currentEnd <= editable.length
|
|
295
|
-
) {
|
|
296
|
-
setSelection(currentStart, currentEnd)
|
|
364
|
+
if (editable != null && authorizedSelection != null) {
|
|
365
|
+
setSelection(
|
|
366
|
+
authorizedSelection.first.coerceIn(0, editable.length),
|
|
367
|
+
authorizedSelection.second.coerceIn(0, editable.length)
|
|
368
|
+
)
|
|
297
369
|
}
|
|
298
370
|
|
|
299
371
|
if (hasFocus()) {
|
|
300
|
-
|
|
301
|
-
imm?.restartInput(this)
|
|
372
|
+
restartInputForEditor()
|
|
302
373
|
}
|
|
303
374
|
}
|
|
304
375
|
|
|
@@ -346,16 +417,72 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
346
417
|
*/
|
|
347
418
|
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
|
348
419
|
val baseConnection = super.onCreateInputConnection(outAttrs) ?: return null
|
|
349
|
-
|
|
420
|
+
val generation = nextInputConnectionGenerationForEditor()
|
|
421
|
+
return EditorInputConnection(this, baseConnection, editorId, generation).also {
|
|
422
|
+
activeInputConnection = it
|
|
423
|
+
}
|
|
350
424
|
}
|
|
351
425
|
|
|
352
426
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
|
427
|
+
if (!isEditable && isReadOnlyTextMutationKeyEvent(event)) {
|
|
428
|
+
return true
|
|
429
|
+
}
|
|
430
|
+
if (handleCompositionKeyEvent(event) { super.dispatchKeyEvent(event) }) {
|
|
431
|
+
return true
|
|
432
|
+
}
|
|
353
433
|
if (handleHardwareKeyEvent(event)) {
|
|
354
434
|
return true
|
|
355
435
|
}
|
|
436
|
+
if (handlePrintableHardwareKeyEvent(event) { super.dispatchKeyEvent(event) }) {
|
|
437
|
+
return true
|
|
438
|
+
}
|
|
356
439
|
return super.dispatchKeyEvent(event)
|
|
357
440
|
}
|
|
358
441
|
|
|
442
|
+
internal fun handleCompositionKeyEvent(event: KeyEvent, applyBaseEvent: () -> Boolean): Boolean {
|
|
443
|
+
val inputConnection = activeInputConnection ?: return false
|
|
444
|
+
if (!inputConnection.hasPendingComposition()) return false
|
|
445
|
+
if (!isCompositionKeyCode(event.keyCode)) return false
|
|
446
|
+
if (event.action == KeyEvent.ACTION_DOWN) {
|
|
447
|
+
val signature = hardwareKeyEventSignature(event)
|
|
448
|
+
if (
|
|
449
|
+
lastHandledHardwareKeySignature == signature ||
|
|
450
|
+
didRecentlyHandleHardwareKeyDown(signature)
|
|
451
|
+
) {
|
|
452
|
+
return true
|
|
453
|
+
}
|
|
454
|
+
markHandledHardwareKeyDown(signature)
|
|
455
|
+
runWithTransientInputMutationGuard {
|
|
456
|
+
when (event.keyCode) {
|
|
457
|
+
KeyEvent.KEYCODE_DEL,
|
|
458
|
+
KeyEvent.KEYCODE_FORWARD_DEL -> inputConnection.deleteTransientTextForHardwareKeyEvent(event)
|
|
459
|
+
else -> applyBaseEvent()
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
inputConnection.refreshComposingTextFromEditableForEditor()
|
|
463
|
+
return true
|
|
464
|
+
}
|
|
465
|
+
if (event.action == KeyEvent.ACTION_UP) {
|
|
466
|
+
if (lastHandledHardwareKeySignature?.let {
|
|
467
|
+
it.keyCode == event.keyCode && it.downTime == event.downTime
|
|
468
|
+
} == true) {
|
|
469
|
+
lastHandledHardwareKeySignature = null
|
|
470
|
+
}
|
|
471
|
+
return true
|
|
472
|
+
}
|
|
473
|
+
return false
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private fun isCompositionKeyCode(keyCode: Int): Boolean =
|
|
477
|
+
when (keyCode) {
|
|
478
|
+
KeyEvent.KEYCODE_DEL,
|
|
479
|
+
KeyEvent.KEYCODE_FORWARD_DEL,
|
|
480
|
+
KeyEvent.KEYCODE_ENTER,
|
|
481
|
+
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
|
482
|
+
KeyEvent.KEYCODE_TAB -> true
|
|
483
|
+
else -> false
|
|
484
|
+
}
|
|
485
|
+
|
|
359
486
|
override fun onDraw(canvas: android.graphics.Canvas) {
|
|
360
487
|
super.onDraw(canvas)
|
|
361
488
|
|
|
@@ -492,7 +619,15 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
492
619
|
* @param id The editor ID from `editor_create()`.
|
|
493
620
|
* @param initialHTML Optional HTML to set as initial content.
|
|
494
621
|
*/
|
|
495
|
-
fun bindEditor(id: Long, initialHTML: String? = null) {
|
|
622
|
+
fun bindEditor(id: Long, initialHTML: String? = null, notifyListener: Boolean = true) {
|
|
623
|
+
if (id != 0L && NativeEditorViewRegistry.isDestroyed(id)) {
|
|
624
|
+
discardTransientNativeInputForEditorRebind()
|
|
625
|
+
editorId = 0L
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
if (editorId != id) {
|
|
629
|
+
discardTransientNativeInputForEditorRebind()
|
|
630
|
+
}
|
|
496
631
|
editorId = id
|
|
497
632
|
|
|
498
633
|
if (!initialHTML.isNullOrEmpty()) {
|
|
@@ -502,7 +637,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
502
637
|
} else {
|
|
503
638
|
// Pull current state from Rust (content may already be loaded via bridge).
|
|
504
639
|
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
505
|
-
applyUpdateJSON(stateJSON)
|
|
640
|
+
applyUpdateJSON(stateJSON, notifyListener = notifyListener)
|
|
506
641
|
}
|
|
507
642
|
}
|
|
508
643
|
|
|
@@ -510,6 +645,9 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
510
645
|
* Unbind from the current editor instance.
|
|
511
646
|
*/
|
|
512
647
|
fun unbindEditor() {
|
|
648
|
+
if (editorId != 0L) {
|
|
649
|
+
discardTransientNativeInputForEditorRebind()
|
|
650
|
+
}
|
|
513
651
|
editorId = 0
|
|
514
652
|
}
|
|
515
653
|
|
|
@@ -530,7 +668,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
530
668
|
renderAppearanceRevision += 1L
|
|
531
669
|
setBackgroundColor(theme?.backgroundColor ?: baseBackgroundColor)
|
|
532
670
|
applyContentInsets(theme?.contentInsets)
|
|
533
|
-
if (
|
|
671
|
+
if (hasLiveEditor()) {
|
|
534
672
|
val previousScrollX = scrollX
|
|
535
673
|
val previousScrollY = scrollY
|
|
536
674
|
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
@@ -685,17 +823,18 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
685
823
|
* Called by [EditorInputConnection.commitText]. Routes the text through
|
|
686
824
|
* the Rust editor instead of directly inserting into the EditText.
|
|
687
825
|
*/
|
|
688
|
-
fun handleTextCommit(text: String) {
|
|
826
|
+
fun handleTextCommit(text: String, newCursorPosition: Int = 1) {
|
|
689
827
|
if (!isEditable) return
|
|
690
828
|
if (isApplyingRustState) return
|
|
829
|
+
val selectionRange = normalizedUtf16SelectionRange() ?: return
|
|
691
830
|
if (editorId == 0L) {
|
|
692
831
|
// No Rust editor bound — fall through to direct editing (dev mode).
|
|
693
832
|
val editable = this.text ?: return
|
|
694
|
-
val start =
|
|
695
|
-
val end = selectionEnd
|
|
833
|
+
val (start, end) = selectionRange
|
|
696
834
|
editable.replace(start, end, text)
|
|
697
835
|
return
|
|
698
836
|
}
|
|
837
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
699
838
|
|
|
700
839
|
// Handle Enter/Return as a block split operation.
|
|
701
840
|
if (text == "\n") {
|
|
@@ -704,18 +843,19 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
704
843
|
}
|
|
705
844
|
|
|
706
845
|
val currentText = this.text?.toString() ?: ""
|
|
707
|
-
val
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
846
|
+
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
847
|
+
insertPlainTextRangeInRust(
|
|
848
|
+
scalarStart,
|
|
849
|
+
scalarEnd,
|
|
850
|
+
text,
|
|
851
|
+
requestedCursorScalar = requestedCursorScalar(
|
|
852
|
+
scalarStart,
|
|
853
|
+
scalarEnd,
|
|
854
|
+
currentText,
|
|
855
|
+
text,
|
|
856
|
+
newCursorPosition
|
|
857
|
+
)
|
|
858
|
+
)
|
|
719
859
|
}
|
|
720
860
|
|
|
721
861
|
internal fun runWithTransientInputMutationGuard(block: () -> Boolean): Boolean {
|
|
@@ -728,31 +868,497 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
728
868
|
}
|
|
729
869
|
}
|
|
730
870
|
|
|
731
|
-
fun
|
|
871
|
+
internal fun authorizedUtf16Range(start: Int, end: Int): Pair<Int, Int> {
|
|
872
|
+
if (start == end) {
|
|
873
|
+
val snapped = PositionBridge.snapToScalarBoundary(
|
|
874
|
+
start,
|
|
875
|
+
lastAuthorizedText,
|
|
876
|
+
biasForward = true
|
|
877
|
+
)
|
|
878
|
+
return snapped to snapped
|
|
879
|
+
}
|
|
880
|
+
return PositionBridge.snapRangeToScalarBoundaries(start, end, lastAuthorizedText)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
internal fun isCurrentTextAuthorizedForEditor(): Boolean =
|
|
884
|
+
(text?.toString() ?: "") == lastAuthorizedText
|
|
885
|
+
|
|
886
|
+
internal fun captureCompositionReplacementRangeIfNeeded() {
|
|
887
|
+
if (didInvalidateCompositionReplacementRange) return
|
|
888
|
+
if (compositionReplacementRange() != null) return
|
|
889
|
+
val (start, end) = normalizedUtf16SelectionRange() ?: return
|
|
890
|
+
setCompositionReplacementRange(start, end)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
internal fun setCompositionReplacementRange(start: Int, end: Int) {
|
|
894
|
+
if (didInvalidateCompositionReplacementRange) return
|
|
895
|
+
val replacementRange = authorizedUtf16Range(start, end)
|
|
896
|
+
composingReplacementStartUtf16 = replacementRange.first
|
|
897
|
+
composingReplacementEndUtf16 = replacementRange.second
|
|
898
|
+
composingReplacementAuthorizedTextRevision = lastAuthorizedTextRevision
|
|
899
|
+
didInvalidateCompositionReplacementRange = false
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
internal fun compositionReplacementRange(): Pair<Int, Int>? {
|
|
903
|
+
val start = composingReplacementStartUtf16 ?: return null
|
|
904
|
+
val end = composingReplacementEndUtf16 ?: return null
|
|
905
|
+
if (composingReplacementAuthorizedTextRevision != lastAuthorizedTextRevision) {
|
|
906
|
+
clearCompositionTrackingForEditor()
|
|
907
|
+
didInvalidateCompositionReplacementRange = true
|
|
908
|
+
return null
|
|
909
|
+
}
|
|
910
|
+
return start to end
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private fun authorizedSelectionForTransientInputRestore(
|
|
914
|
+
currentStart: Int,
|
|
915
|
+
currentEnd: Int
|
|
916
|
+
): Pair<Int, Int>? {
|
|
917
|
+
compositionReplacementRange()?.let { return it }
|
|
918
|
+
return if (
|
|
919
|
+
currentStart >= 0 &&
|
|
920
|
+
currentEnd >= 0 &&
|
|
921
|
+
currentStart <= lastAuthorizedText.length &&
|
|
922
|
+
currentEnd <= lastAuthorizedText.length
|
|
923
|
+
) {
|
|
924
|
+
currentStart to currentEnd
|
|
925
|
+
} else {
|
|
926
|
+
null
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
internal fun consumeInvalidatedCompositionReplacementRangeForEditor(): Boolean {
|
|
931
|
+
val invalidated = didInvalidateCompositionReplacementRange
|
|
932
|
+
didInvalidateCompositionReplacementRange = false
|
|
933
|
+
return invalidated
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
internal fun hasInvalidatedCompositionReplacementRangeForEditor(): Boolean =
|
|
937
|
+
didInvalidateCompositionReplacementRange
|
|
938
|
+
|
|
939
|
+
internal fun setComposingTextForEditor(text: String?) {
|
|
940
|
+
composingText = text
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
internal fun composingTextForEditor(): String? = composingText
|
|
944
|
+
|
|
945
|
+
internal fun composingTextFromVisibleReplacementForEditor(): String? {
|
|
946
|
+
val (start, end) = compositionReplacementRange() ?: return null
|
|
947
|
+
val authorizedText = lastAuthorizedText
|
|
948
|
+
val currentText = text?.toString() ?: return null
|
|
949
|
+
if (start < 0 || end < start || end > authorizedText.length) return null
|
|
950
|
+
|
|
951
|
+
val authorizedPrefix = authorizedText.substring(0, start)
|
|
952
|
+
val authorizedSuffix = authorizedText.substring(end)
|
|
953
|
+
if (!currentText.startsWith(authorizedPrefix)) return null
|
|
954
|
+
if (!currentText.endsWith(authorizedSuffix)) return null
|
|
955
|
+
|
|
956
|
+
val replacementEnd = currentText.length - authorizedSuffix.length
|
|
957
|
+
if (replacementEnd < authorizedPrefix.length) return null
|
|
958
|
+
return currentText.substring(authorizedPrefix.length, replacementEnd)
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
internal fun clearCompositionTrackingForEditor() {
|
|
962
|
+
composingText = null
|
|
963
|
+
composingReplacementStartUtf16 = null
|
|
964
|
+
composingReplacementEndUtf16 = null
|
|
965
|
+
composingReplacementAuthorizedTextRevision = null
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
private fun hasCompositionTrackingForEditor(): Boolean =
|
|
969
|
+
composingText != null ||
|
|
970
|
+
composingReplacementStartUtf16 != null ||
|
|
971
|
+
composingReplacementEndUtf16 != null ||
|
|
972
|
+
composingReplacementAuthorizedTextRevision != null
|
|
973
|
+
|
|
974
|
+
private fun retireInputConnectionForEditor() {
|
|
975
|
+
activeInputConnection?.clearCompositionTrackingForEditor()
|
|
976
|
+
invalidateInputConnectionsForEditor()
|
|
977
|
+
clearCompositionTrackingForEditor()
|
|
978
|
+
clearCompositionInvalidationForEditor()
|
|
979
|
+
clearNativeComposingSpans()
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
internal fun isEditorDestroyedForInput(): Boolean =
|
|
983
|
+
editorId != 0L && NativeEditorViewRegistry.isDestroyed(editorId)
|
|
984
|
+
|
|
985
|
+
private fun hasLiveEditor(): Boolean =
|
|
986
|
+
editorId != 0L && !isEditorDestroyedForInput()
|
|
987
|
+
|
|
988
|
+
private fun discardTransientInputForDestroyedEditorIfNeeded(): Boolean {
|
|
989
|
+
if (!isEditorDestroyedForInput()) return false
|
|
990
|
+
retireInputConnectionForEditor()
|
|
991
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
992
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
993
|
+
return true
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
private fun discardTransientInputAndRestoreAuthorizedTextForEditor() {
|
|
997
|
+
retireInputConnectionForEditor()
|
|
998
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
999
|
+
restoreAuthorizedTextSnapshotForEditor()
|
|
1000
|
+
suppressNativeTextMutationAdoptionForCurrentRevision()
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
private fun restoreAuthorizedTextSnapshotForEditor() {
|
|
1004
|
+
if ((text?.toString() ?: "") == lastAuthorizedText) return
|
|
1005
|
+
val authorizedSnapshot = lastAuthorizedRenderedText ?: lastAuthorizedText
|
|
1006
|
+
val wasApplyingRustState = isApplyingRustState
|
|
1007
|
+
isApplyingRustState = true
|
|
1008
|
+
beginBatchEdit()
|
|
1009
|
+
try {
|
|
1010
|
+
setText(authorizedSnapshot)
|
|
1011
|
+
} finally {
|
|
1012
|
+
endBatchEdit()
|
|
1013
|
+
isApplyingRustState = wasApplyingRustState
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
private fun restartInputAfterCompositionInvalidationIfNeeded(shouldRestart: Boolean) {
|
|
1018
|
+
if (!shouldRestart) return
|
|
1019
|
+
restartInputForEditorIfFocused()
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
private fun restartInputForEditorIfFocused() {
|
|
1023
|
+
if (!hasFocus()) return
|
|
1024
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1025
|
+
imm?.restartInput(this)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private fun restartInputForEditor() {
|
|
1029
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1030
|
+
imm?.restartInput(this)
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
private fun clearCompositionInvalidationForEditor() {
|
|
1034
|
+
didInvalidateCompositionReplacementRange = false
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
private fun nextInputConnectionGenerationForEditor(): Long {
|
|
1038
|
+
inputConnectionGeneration += 1L
|
|
1039
|
+
return inputConnectionGeneration
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
internal fun isInputConnectionCurrentForEditor(
|
|
1043
|
+
boundEditorId: Long,
|
|
1044
|
+
boundGeneration: Long
|
|
1045
|
+
): Boolean =
|
|
1046
|
+
editorId == boundEditorId &&
|
|
1047
|
+
inputConnectionGeneration == boundGeneration &&
|
|
1048
|
+
!isEditorDestroyedForInput()
|
|
1049
|
+
|
|
1050
|
+
private fun invalidateInputConnectionsForEditor() {
|
|
1051
|
+
inputConnectionGeneration += 1L
|
|
1052
|
+
activeInputConnection = null
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private fun clearNativeComposingSpans() {
|
|
1056
|
+
val editable = text ?: return
|
|
1057
|
+
BaseInputConnection.removeComposingSpans(editable)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
internal fun restoreAuthorizedTextIfNeeded() {
|
|
1061
|
+
if (!hasLiveEditor()) return
|
|
1062
|
+
if ((text?.toString() ?: "") == lastAuthorizedText) return
|
|
1063
|
+
recordImeTraceForTesting(
|
|
1064
|
+
"restoreAuthorizedText",
|
|
1065
|
+
"authorizedLength=${lastAuthorizedText.length}"
|
|
1066
|
+
)
|
|
1067
|
+
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
1068
|
+
applyUpdateJSON(stateJSON)
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
fun discardTransientNativeInputForEditorRebind() {
|
|
1072
|
+
retireInputConnectionForEditor()
|
|
1073
|
+
nativeTextMutationAfterBlurWindow = null
|
|
1074
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
1075
|
+
clearImeTraceForTesting()
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
internal fun discardTransientNativeInputForExternalRecovery() {
|
|
1079
|
+
retireInputConnectionForEditor()
|
|
1080
|
+
nativeTextMutationAfterBlurWindow = null
|
|
1081
|
+
restoreAuthorizedTextIfNeeded()
|
|
1082
|
+
suppressNativeTextMutationAdoptionForCurrentRevision()
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
private fun discardTransientNativeInputForReadOnly() {
|
|
1086
|
+
discardTransientNativeInputForExternalRecovery()
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
fun prepareForExternalEditorUpdate(): Boolean {
|
|
1090
|
+
if (blockExternalEditorUpdatePreparationForTesting) return false
|
|
1091
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return false
|
|
1092
|
+
val inputConnection = activeInputConnection
|
|
1093
|
+
if (inputConnection?.flushPendingCompositionForExternalMutation() == false) {
|
|
1094
|
+
return false
|
|
1095
|
+
}
|
|
1096
|
+
return drainNativeTextMutationIfNeeded(allowAfterBlur = true)
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
fun prepareForExternalEditorCommand(): CommandPreparation {
|
|
1100
|
+
if (blockExternalEditorCommandPreparationForTesting) {
|
|
1101
|
+
return CommandPreparation(ready = false, updateJSON = null)
|
|
1102
|
+
}
|
|
1103
|
+
val previousAuthorizedText = lastAuthorizedText
|
|
1104
|
+
if (!prepareForExternalEditorUpdate()) {
|
|
1105
|
+
return CommandPreparation(ready = false, updateJSON = null)
|
|
1106
|
+
}
|
|
1107
|
+
if (!hasLiveEditor() || lastAuthorizedText == previousAuthorizedText) {
|
|
1108
|
+
return CommandPreparation(ready = true, updateJSON = null)
|
|
1109
|
+
}
|
|
1110
|
+
return CommandPreparation(
|
|
1111
|
+
ready = true,
|
|
1112
|
+
updateJSON = editorGetCurrentState(editorId.toULong())
|
|
1113
|
+
)
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
fun handleCompositionCommit(
|
|
1117
|
+
text: String,
|
|
1118
|
+
replacementStartUtf16: Int,
|
|
1119
|
+
replacementEndUtf16: Int,
|
|
1120
|
+
newCursorPosition: Int = 1
|
|
1121
|
+
) {
|
|
732
1122
|
if (!isEditable) return
|
|
733
1123
|
if (isApplyingRustState) return
|
|
734
|
-
if (
|
|
1124
|
+
if (!hasLiveEditor()) return
|
|
1125
|
+
|
|
1126
|
+
val authorizedText = lastAuthorizedText
|
|
1127
|
+
val (startUtf16, endUtf16) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1128
|
+
replacementStartUtf16,
|
|
1129
|
+
replacementEndUtf16,
|
|
1130
|
+
authorizedText
|
|
1131
|
+
)
|
|
1132
|
+
val scalarStart = PositionBridge.utf16ToScalar(startUtf16, authorizedText)
|
|
1133
|
+
val scalarEnd = PositionBridge.utf16ToScalar(endUtf16, authorizedText)
|
|
1134
|
+
|
|
1135
|
+
if (
|
|
1136
|
+
startUtf16 <= endUtf16 &&
|
|
1137
|
+
endUtf16 <= authorizedText.length &&
|
|
1138
|
+
authorizedText.substring(startUtf16, endUtf16) == text
|
|
1139
|
+
) {
|
|
1140
|
+
val requestedCursor = requestedCursorScalar(
|
|
1141
|
+
scalarStart,
|
|
1142
|
+
scalarEnd,
|
|
1143
|
+
authorizedText,
|
|
1144
|
+
text,
|
|
1145
|
+
newCursorPosition
|
|
1146
|
+
) ?: scalarEnd
|
|
1147
|
+
restoreAuthorizedTextIfNeeded()
|
|
1148
|
+
applyRequestedCursorScalar(requestedCursor)
|
|
1149
|
+
return
|
|
1150
|
+
}
|
|
735
1151
|
|
|
736
1152
|
if (text == "\n") {
|
|
737
|
-
|
|
1153
|
+
if (scalarStart != scalarEnd) {
|
|
1154
|
+
deleteAndSplitInRust(scalarStart, scalarEnd)
|
|
1155
|
+
} else {
|
|
1156
|
+
splitBlockInRust(scalarStart)
|
|
1157
|
+
}
|
|
738
1158
|
return
|
|
739
1159
|
}
|
|
740
1160
|
|
|
1161
|
+
insertPlainTextRangeInRust(
|
|
1162
|
+
scalarStart,
|
|
1163
|
+
scalarEnd,
|
|
1164
|
+
text,
|
|
1165
|
+
requestedCursorScalar = requestedCursorScalar(
|
|
1166
|
+
scalarStart,
|
|
1167
|
+
scalarEnd,
|
|
1168
|
+
authorizedText,
|
|
1169
|
+
text,
|
|
1170
|
+
newCursorPosition
|
|
1171
|
+
)
|
|
1172
|
+
)
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
fun handleCorrectionCommit(
|
|
1176
|
+
offsetUtf16: Int,
|
|
1177
|
+
oldText: String,
|
|
1178
|
+
newText: String
|
|
1179
|
+
): Boolean {
|
|
1180
|
+
if (!isEditable) return true
|
|
1181
|
+
if (isApplyingRustState) return true
|
|
1182
|
+
if (!hasLiveEditor()) return false
|
|
1183
|
+
|
|
741
1184
|
val authorizedText = lastAuthorizedText
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1185
|
+
if (offsetUtf16 < 0) {
|
|
1186
|
+
recordImeTraceForTesting(
|
|
1187
|
+
"correctionExplicitNoop",
|
|
1188
|
+
"reason=invalidOffset offset=$offsetUtf16 oldLength=${oldText.length} newLength=${newText.length}"
|
|
1189
|
+
)
|
|
1190
|
+
return false
|
|
1191
|
+
}
|
|
1192
|
+
val endUtf16 = offsetUtf16 + oldText.length
|
|
1193
|
+
if (endUtf16 < offsetUtf16 || endUtf16 > authorizedText.length) {
|
|
1194
|
+
recordImeTraceForTesting(
|
|
1195
|
+
"correctionExplicitNoop",
|
|
1196
|
+
"reason=outOfBounds offset=$offsetUtf16 oldLength=${oldText.length} authorizedLength=${authorizedText.length}"
|
|
1197
|
+
)
|
|
1198
|
+
return false
|
|
1199
|
+
}
|
|
1200
|
+
if (authorizedText.substring(offsetUtf16, endUtf16) != oldText) {
|
|
1201
|
+
recordImeTraceForTesting(
|
|
1202
|
+
"correctionExplicitNoop",
|
|
1203
|
+
"reason=staleText offset=$offsetUtf16 oldLength=${oldText.length} newLength=${newText.length}"
|
|
1204
|
+
)
|
|
1205
|
+
return false
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
val (startUtf16, snappedEndUtf16) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1209
|
+
offsetUtf16,
|
|
1210
|
+
endUtf16,
|
|
1211
|
+
authorizedText
|
|
1212
|
+
)
|
|
1213
|
+
if (
|
|
1214
|
+
startUtf16 != offsetUtf16 ||
|
|
1215
|
+
snappedEndUtf16 != endUtf16 ||
|
|
1216
|
+
startUtf16 > snappedEndUtf16
|
|
1217
|
+
) {
|
|
1218
|
+
recordImeTraceForTesting(
|
|
1219
|
+
"correctionExplicitNoop",
|
|
1220
|
+
"reason=unsnappedScalarBoundary range=$offsetUtf16..$endUtf16 snapped=$startUtf16..$snappedEndUtf16"
|
|
1221
|
+
)
|
|
1222
|
+
return false
|
|
1223
|
+
}
|
|
1224
|
+
|
|
746
1225
|
val scalarStart = PositionBridge.utf16ToScalar(startUtf16, authorizedText)
|
|
747
|
-
val scalarEnd = PositionBridge.utf16ToScalar(
|
|
1226
|
+
val scalarEnd = PositionBridge.utf16ToScalar(snappedEndUtf16, authorizedText)
|
|
1227
|
+
recordImeTraceForTesting(
|
|
1228
|
+
"correctionExplicitApply",
|
|
1229
|
+
"range=$scalarStart..$scalarEnd newLength=${newText.length}"
|
|
1230
|
+
)
|
|
1231
|
+
insertPlainTextRangeInRust(scalarStart, scalarEnd, newText)
|
|
1232
|
+
return true
|
|
1233
|
+
}
|
|
748
1234
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1235
|
+
fun handleMissingOldTextCorrectionCommit(
|
|
1236
|
+
offsetUtf16: Int,
|
|
1237
|
+
newText: String
|
|
1238
|
+
): Boolean {
|
|
1239
|
+
if (!isEditable) return true
|
|
1240
|
+
if (isApplyingRustState) return true
|
|
1241
|
+
if (!hasLiveEditor()) return false
|
|
1242
|
+
|
|
1243
|
+
val authorizedText = lastAuthorizedText
|
|
1244
|
+
val tokenRange = missingOldTextCorrectionTokenRange(authorizedText, offsetUtf16)
|
|
1245
|
+
?: run {
|
|
1246
|
+
recordImeTraceForTesting(
|
|
1247
|
+
"correctionInferredNoop",
|
|
1248
|
+
"reason=noToken offset=$offsetUtf16 newLength=${newText.length}"
|
|
1249
|
+
)
|
|
1250
|
+
return false
|
|
1251
|
+
}
|
|
1252
|
+
val (startUtf16, endUtf16) = tokenRange
|
|
1253
|
+
|
|
1254
|
+
val (snappedStartUtf16, snappedEndUtf16) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1255
|
+
startUtf16,
|
|
1256
|
+
endUtf16,
|
|
1257
|
+
authorizedText
|
|
1258
|
+
)
|
|
1259
|
+
if (snappedStartUtf16 >= snappedEndUtf16) {
|
|
1260
|
+
recordImeTraceForTesting(
|
|
1261
|
+
"correctionInferredNoop",
|
|
1262
|
+
"reason=emptySnappedRange token=$startUtf16..$endUtf16 snapped=$snappedStartUtf16..$snappedEndUtf16"
|
|
1263
|
+
)
|
|
1264
|
+
return false
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
val scalarStart = PositionBridge.utf16ToScalar(snappedStartUtf16, authorizedText)
|
|
1268
|
+
val scalarEnd = PositionBridge.utf16ToScalar(snappedEndUtf16, authorizedText)
|
|
1269
|
+
recordImeTraceForTesting(
|
|
1270
|
+
"correctionInferredApply",
|
|
1271
|
+
"range=$scalarStart..$scalarEnd utf16=$snappedStartUtf16..$snappedEndUtf16 newLength=${newText.length}"
|
|
1272
|
+
)
|
|
1273
|
+
insertPlainTextRangeInRust(scalarStart, scalarEnd, newText)
|
|
1274
|
+
return true
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
private fun missingOldTextCorrectionTokenRange(
|
|
1278
|
+
text: String,
|
|
1279
|
+
offsetUtf16: Int
|
|
1280
|
+
): Pair<Int, Int>? {
|
|
1281
|
+
if (offsetUtf16 < 0 || offsetUtf16 >= text.length) return null
|
|
1282
|
+
|
|
1283
|
+
val tokenOffset = PositionBridge.snapToScalarBoundary(
|
|
1284
|
+
offsetUtf16,
|
|
1285
|
+
text,
|
|
1286
|
+
biasForward = false
|
|
1287
|
+
)
|
|
1288
|
+
if (tokenOffset < 0 || tokenOffset >= text.length) return null
|
|
1289
|
+
if (!isMissingOldTextCorrectionTokenCodePointAt(text, tokenOffset)) return null
|
|
1290
|
+
|
|
1291
|
+
var startUtf16 = tokenOffset
|
|
1292
|
+
while (startUtf16 > 0) {
|
|
1293
|
+
val previousUtf16 = Character.offsetByCodePoints(text, startUtf16, -1)
|
|
1294
|
+
if (!isMissingOldTextCorrectionTokenCodePointAt(text, previousUtf16)) break
|
|
1295
|
+
startUtf16 = previousUtf16
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
var endUtf16 = tokenOffset + Character.charCount(Character.codePointAt(text, tokenOffset))
|
|
1299
|
+
while (endUtf16 < text.length) {
|
|
1300
|
+
if (!isMissingOldTextCorrectionTokenCodePointAt(text, endUtf16)) break
|
|
1301
|
+
endUtf16 += Character.charCount(Character.codePointAt(text, endUtf16))
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
return if (startUtf16 < endUtf16) startUtf16 to endUtf16 else null
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
private fun isMissingOldTextCorrectionTokenCodePointAt(text: String, utf16Offset: Int): Boolean {
|
|
1308
|
+
if (utf16Offset < 0 || utf16Offset >= text.length) return false
|
|
1309
|
+
val codePoint = Character.codePointAt(text, utf16Offset)
|
|
1310
|
+
if (isMissingOldTextCorrectionCoreTokenCodePoint(codePoint)) return true
|
|
1311
|
+
if (!isMissingOldTextCorrectionJoinerCodePoint(codePoint)) return false
|
|
1312
|
+
|
|
1313
|
+
val previousCodePoint = previousCodePointBefore(text, utf16Offset) ?: return false
|
|
1314
|
+
val nextUtf16Offset = utf16Offset + Character.charCount(codePoint)
|
|
1315
|
+
val nextCodePoint = nextCodePointAt(text, nextUtf16Offset) ?: return false
|
|
1316
|
+
return isMissingOldTextCorrectionCoreTokenCodePoint(previousCodePoint) &&
|
|
1317
|
+
isMissingOldTextCorrectionCoreTokenCodePoint(nextCodePoint)
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
private fun isMissingOldTextCorrectionCoreTokenCodePoint(codePoint: Int): Boolean {
|
|
1321
|
+
if (Character.isLetterOrDigit(codePoint)) return true
|
|
1322
|
+
return when (Character.getType(codePoint)) {
|
|
1323
|
+
Character.NON_SPACING_MARK.toInt(),
|
|
1324
|
+
Character.COMBINING_SPACING_MARK.toInt(),
|
|
1325
|
+
Character.ENCLOSING_MARK.toInt(),
|
|
1326
|
+
Character.CONNECTOR_PUNCTUATION.toInt(),
|
|
1327
|
+
Character.MATH_SYMBOL.toInt(),
|
|
1328
|
+
Character.CURRENCY_SYMBOL.toInt(),
|
|
1329
|
+
Character.MODIFIER_SYMBOL.toInt(),
|
|
1330
|
+
Character.OTHER_SYMBOL.toInt(),
|
|
1331
|
+
Character.SURROGATE.toInt() -> true
|
|
1332
|
+
else -> false
|
|
753
1333
|
}
|
|
754
1334
|
}
|
|
755
1335
|
|
|
1336
|
+
private fun isMissingOldTextCorrectionJoinerCodePoint(codePoint: Int): Boolean =
|
|
1337
|
+
codePoint == '\''.code ||
|
|
1338
|
+
codePoint == 0x2018 ||
|
|
1339
|
+
codePoint == 0x2019 ||
|
|
1340
|
+
codePoint == 0x201B ||
|
|
1341
|
+
codePoint == 0xFF07 ||
|
|
1342
|
+
codePoint == '-'.code ||
|
|
1343
|
+
codePoint == 0x2010 ||
|
|
1344
|
+
codePoint == 0x2011 ||
|
|
1345
|
+
codePoint == 0x2012 ||
|
|
1346
|
+
codePoint == 0x2013 ||
|
|
1347
|
+
codePoint == 0x2014 ||
|
|
1348
|
+
codePoint == 0x2212 ||
|
|
1349
|
+
codePoint == 0x200D
|
|
1350
|
+
|
|
1351
|
+
private fun previousCodePointBefore(text: String, utf16Offset: Int): Int? {
|
|
1352
|
+
if (utf16Offset <= 0 || utf16Offset > text.length) return null
|
|
1353
|
+
val previousUtf16 = Character.offsetByCodePoints(text, utf16Offset, -1)
|
|
1354
|
+
return Character.codePointAt(text, previousUtf16)
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
private fun nextCodePointAt(text: String, utf16Offset: Int): Int? {
|
|
1358
|
+
if (utf16Offset < 0 || utf16Offset >= text.length) return null
|
|
1359
|
+
return Character.codePointAt(text, utf16Offset)
|
|
1360
|
+
}
|
|
1361
|
+
|
|
756
1362
|
// ── Input Handling: Deletion ────────────────────────────────────────
|
|
757
1363
|
|
|
758
1364
|
/**
|
|
@@ -766,17 +1372,32 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
766
1372
|
fun handleDelete(beforeLength: Int, afterLength: Int) {
|
|
767
1373
|
if (!isEditable) return
|
|
768
1374
|
if (isApplyingRustState) return
|
|
1375
|
+
val selectionRange = normalizedUtf16SelectionRange()
|
|
769
1376
|
if (editorId == 0L) {
|
|
770
1377
|
// Dev mode: direct editing.
|
|
771
1378
|
val editable = this.text ?: return
|
|
772
|
-
val
|
|
773
|
-
val delStart
|
|
774
|
-
val delEnd
|
|
1379
|
+
val (selectionStart, selectionEnd) = selectionRange ?: return
|
|
1380
|
+
val delStart: Int
|
|
1381
|
+
val delEnd: Int
|
|
1382
|
+
if (selectionStart != selectionEnd) {
|
|
1383
|
+
delStart = selectionStart
|
|
1384
|
+
delEnd = selectionEnd
|
|
1385
|
+
} else {
|
|
1386
|
+
delStart = maxOf(0, selectionStart - beforeLength.coerceAtLeast(0))
|
|
1387
|
+
delEnd = minOf(editable.length, selectionStart + afterLength.coerceAtLeast(0))
|
|
1388
|
+
}
|
|
775
1389
|
editable.delete(delStart, delEnd)
|
|
776
1390
|
return
|
|
777
1391
|
}
|
|
1392
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
778
1393
|
|
|
779
1394
|
val currentText = text?.toString() ?: ""
|
|
1395
|
+
val (selectionStart, selectionEnd) = selectionRange ?: return
|
|
1396
|
+
if (selectionStart != selectionEnd) {
|
|
1397
|
+
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
1398
|
+
deleteRangeInRust(scalarStart, scalarEnd)
|
|
1399
|
+
return
|
|
1400
|
+
}
|
|
780
1401
|
val cursor = selectionStart
|
|
781
1402
|
if (beforeLength > 0 &&
|
|
782
1403
|
afterLength == 0 &&
|
|
@@ -787,8 +1408,13 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
787
1408
|
deleteBackwardAtSelectionScalarInRust(scalarCursor, scalarCursor)
|
|
788
1409
|
return
|
|
789
1410
|
}
|
|
790
|
-
val
|
|
791
|
-
val
|
|
1411
|
+
val rawDelStart = maxOf(0, cursor - beforeLength.coerceAtLeast(0))
|
|
1412
|
+
val rawDelEnd = minOf(currentText.length, cursor + afterLength.coerceAtLeast(0))
|
|
1413
|
+
val (delStart, delEnd) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1414
|
+
rawDelStart,
|
|
1415
|
+
rawDelEnd,
|
|
1416
|
+
currentText
|
|
1417
|
+
)
|
|
792
1418
|
|
|
793
1419
|
val scalarStart = PositionBridge.utf16ToScalar(delStart, currentText)
|
|
794
1420
|
val scalarEnd = PositionBridge.utf16ToScalar(delEnd, currentText)
|
|
@@ -809,11 +1435,11 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
809
1435
|
fun handleBackspace() {
|
|
810
1436
|
if (!isEditable) return
|
|
811
1437
|
if (isApplyingRustState) return
|
|
1438
|
+
val selectionRange = normalizedUtf16SelectionRange() ?: return
|
|
812
1439
|
if (editorId == 0L) {
|
|
813
1440
|
// Dev mode: direct editing.
|
|
814
1441
|
val editable = this.text ?: return
|
|
815
|
-
val start =
|
|
816
|
-
val end = selectionEnd
|
|
1442
|
+
val (start, end) = selectionRange
|
|
817
1443
|
if (start != end) {
|
|
818
1444
|
editable.delete(start, end)
|
|
819
1445
|
} else if (start > 0) {
|
|
@@ -824,15 +1450,14 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
824
1450
|
}
|
|
825
1451
|
return
|
|
826
1452
|
}
|
|
1453
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
827
1454
|
|
|
828
1455
|
val currentText = text?.toString() ?: ""
|
|
829
|
-
val start =
|
|
830
|
-
val end = selectionEnd
|
|
1456
|
+
val (start, end) = selectionRange
|
|
831
1457
|
|
|
832
1458
|
if (start != end) {
|
|
833
1459
|
// Range selection: delete the range.
|
|
834
|
-
val scalarStart =
|
|
835
|
-
val scalarEnd = PositionBridge.utf16ToScalar(end, currentText)
|
|
1460
|
+
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
836
1461
|
deleteRangeInRust(scalarStart, scalarEnd)
|
|
837
1462
|
} else if (start > 0) {
|
|
838
1463
|
if (currentText.getOrNull(start - 1) == EMPTY_BLOCK_PLACEHOLDER) {
|
|
@@ -859,6 +1484,57 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
859
1484
|
}
|
|
860
1485
|
}
|
|
861
1486
|
|
|
1487
|
+
fun handleForwardDelete() {
|
|
1488
|
+
if (!isEditable) return
|
|
1489
|
+
if (isApplyingRustState) return
|
|
1490
|
+
val selectionRange = normalizedUtf16SelectionRange() ?: return
|
|
1491
|
+
if (editorId == 0L) {
|
|
1492
|
+
val editable = this.text ?: return
|
|
1493
|
+
val (start, end) = selectionRange
|
|
1494
|
+
if (start != end) {
|
|
1495
|
+
editable.delete(start, end)
|
|
1496
|
+
} else if (start < editable.length) {
|
|
1497
|
+
val breakIter = java.text.BreakIterator.getCharacterInstance()
|
|
1498
|
+
breakIter.setText(editable.toString())
|
|
1499
|
+
val nextBoundary = breakIter.following(start)
|
|
1500
|
+
val nextUtf16 = if (nextBoundary == java.text.BreakIterator.DONE) {
|
|
1501
|
+
editable.length
|
|
1502
|
+
} else {
|
|
1503
|
+
nextBoundary
|
|
1504
|
+
}
|
|
1505
|
+
editable.delete(start, nextUtf16.coerceIn(start, editable.length))
|
|
1506
|
+
}
|
|
1507
|
+
return
|
|
1508
|
+
}
|
|
1509
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
1510
|
+
|
|
1511
|
+
val currentText = text?.toString() ?: ""
|
|
1512
|
+
val (start, end) = selectionRange
|
|
1513
|
+
if (start != end) {
|
|
1514
|
+
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
1515
|
+
deleteRangeInRust(scalarStart, scalarEnd)
|
|
1516
|
+
} else if (start < currentText.length) {
|
|
1517
|
+
val breakIter = java.text.BreakIterator.getCharacterInstance()
|
|
1518
|
+
breakIter.setText(currentText)
|
|
1519
|
+
val nextBoundary = breakIter.following(start)
|
|
1520
|
+
val nextUtf16 = if (nextBoundary == java.text.BreakIterator.DONE) {
|
|
1521
|
+
currentText.length
|
|
1522
|
+
} else {
|
|
1523
|
+
nextBoundary
|
|
1524
|
+
}
|
|
1525
|
+
val (utf16Start, utf16End) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1526
|
+
start,
|
|
1527
|
+
nextUtf16.coerceIn(start, currentText.length),
|
|
1528
|
+
currentText
|
|
1529
|
+
)
|
|
1530
|
+
val scalarStart = PositionBridge.utf16ToScalar(utf16Start, currentText)
|
|
1531
|
+
val scalarEnd = PositionBridge.utf16ToScalar(utf16End, currentText)
|
|
1532
|
+
if (scalarStart < scalarEnd) {
|
|
1533
|
+
deleteRangeInRust(scalarStart, scalarEnd)
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
862
1538
|
// ── Input Handling: Return Key ──────────────────────────────────────
|
|
863
1539
|
|
|
864
1540
|
/**
|
|
@@ -869,8 +1545,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
869
1545
|
if (isApplyingRustState) return
|
|
870
1546
|
|
|
871
1547
|
val currentText = text?.toString() ?: ""
|
|
872
|
-
val start =
|
|
873
|
-
val end = selectionEnd
|
|
1548
|
+
val (start, end) = normalizedUtf16SelectionRange() ?: return
|
|
874
1549
|
|
|
875
1550
|
if (editorId == 0L) {
|
|
876
1551
|
// Dev mode: insert newline directly.
|
|
@@ -878,15 +1553,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
878
1553
|
editable.replace(start, end, "\n")
|
|
879
1554
|
return
|
|
880
1555
|
}
|
|
1556
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
881
1557
|
|
|
882
1558
|
if (start != end) {
|
|
883
1559
|
// Range selection: atomic delete-and-split via Rust.
|
|
884
|
-
val scalarStart =
|
|
885
|
-
|
|
886
|
-
val updateJSON = editorDeleteAndSplitScalar(
|
|
887
|
-
editorId.toULong(), scalarStart.toUInt(), scalarEnd.toUInt()
|
|
888
|
-
)
|
|
889
|
-
applyUpdateJSON(updateJSON)
|
|
1560
|
+
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
1561
|
+
deleteAndSplitInRust(scalarStart, scalarEnd)
|
|
890
1562
|
} else {
|
|
891
1563
|
val scalarPos = PositionBridge.utf16ToScalar(start, currentText)
|
|
892
1564
|
splitBlockInRust(scalarPos)
|
|
@@ -907,6 +1579,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
907
1579
|
editable.replace(start, end, "\n")
|
|
908
1580
|
return
|
|
909
1581
|
}
|
|
1582
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
910
1583
|
|
|
911
1584
|
val selection = currentScalarSelection() ?: return
|
|
912
1585
|
val updateJSON = editorInsertNodeAtSelectionScalar(
|
|
@@ -924,7 +1597,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
924
1597
|
fun handleTab(shiftPressed: Boolean): Boolean {
|
|
925
1598
|
if (!isEditable) return false
|
|
926
1599
|
if (isApplyingRustState) return false
|
|
927
|
-
if (
|
|
1600
|
+
if (!hasLiveEditor()) return false
|
|
928
1601
|
if (!isSelectionInsideList()) return false
|
|
929
1602
|
val selection = currentScalarSelection() ?: return false
|
|
930
1603
|
|
|
@@ -952,6 +1625,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
952
1625
|
handleBackspace()
|
|
953
1626
|
true
|
|
954
1627
|
}
|
|
1628
|
+
KeyEvent.KEYCODE_FORWARD_DEL -> {
|
|
1629
|
+
handleForwardDelete()
|
|
1630
|
+
true
|
|
1631
|
+
}
|
|
955
1632
|
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> {
|
|
956
1633
|
if (shiftPressed) {
|
|
957
1634
|
handleHardBreak()
|
|
@@ -965,28 +1642,55 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
965
1642
|
}
|
|
966
1643
|
}
|
|
967
1644
|
|
|
1645
|
+
private fun isSupportedHardwareMutationKey(keyCode: Int): Boolean =
|
|
1646
|
+
when (keyCode) {
|
|
1647
|
+
KeyEvent.KEYCODE_DEL,
|
|
1648
|
+
KeyEvent.KEYCODE_FORWARD_DEL,
|
|
1649
|
+
KeyEvent.KEYCODE_ENTER,
|
|
1650
|
+
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
|
1651
|
+
KeyEvent.KEYCODE_TAB -> true
|
|
1652
|
+
else -> false
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
internal fun isReadOnlyTextMutationKeyEvent(event: KeyEvent): Boolean {
|
|
1656
|
+
if (isSupportedHardwareMutationKey(event.keyCode) ||
|
|
1657
|
+
event.keyCode == KeyEvent.KEYCODE_FORWARD_DEL
|
|
1658
|
+
) {
|
|
1659
|
+
return true
|
|
1660
|
+
}
|
|
1661
|
+
if (event.keyCode == KeyEvent.KEYCODE_INSERT && event.isShiftPressed) {
|
|
1662
|
+
return true
|
|
1663
|
+
}
|
|
1664
|
+
if (event.isCtrlPressed || event.isMetaPressed) {
|
|
1665
|
+
return when (event.keyCode) {
|
|
1666
|
+
KeyEvent.KEYCODE_V,
|
|
1667
|
+
KeyEvent.KEYCODE_X,
|
|
1668
|
+
KeyEvent.KEYCODE_Z,
|
|
1669
|
+
KeyEvent.KEYCODE_Y -> true
|
|
1670
|
+
else -> false
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
if (!keyEventCharacters(event).isNullOrEmpty()) return true
|
|
1674
|
+
return event.unicodeChar != 0
|
|
1675
|
+
}
|
|
1676
|
+
|
|
968
1677
|
fun handleHardwareKeyEvent(event: KeyEvent?): Boolean {
|
|
969
1678
|
if (event == null || !isEditable || isApplyingRustState) return false
|
|
970
1679
|
|
|
971
1680
|
return when (event.action) {
|
|
972
1681
|
KeyEvent.ACTION_DOWN -> {
|
|
973
|
-
|
|
974
|
-
KeyEvent.KEYCODE_DEL,
|
|
975
|
-
KeyEvent.KEYCODE_ENTER,
|
|
976
|
-
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
|
977
|
-
KeyEvent.KEYCODE_TAB -> true
|
|
978
|
-
else -> false
|
|
979
|
-
}
|
|
980
|
-
if (!supported) return false
|
|
1682
|
+
if (!isSupportedHardwareMutationKey(event.keyCode)) return false
|
|
981
1683
|
|
|
982
|
-
|
|
983
|
-
|
|
1684
|
+
val signature = hardwareKeyEventSignature(event)
|
|
1685
|
+
if (
|
|
1686
|
+
lastHandledHardwareKeySignature == signature ||
|
|
1687
|
+
didRecentlyHandleHardwareKeyDown(signature)
|
|
1688
|
+
) {
|
|
984
1689
|
return true
|
|
985
1690
|
}
|
|
986
1691
|
|
|
987
1692
|
if (handleHardwareKeyDown(event.keyCode, event.isShiftPressed)) {
|
|
988
|
-
|
|
989
|
-
lastHandledHardwareKeyDownTime = event.downTime
|
|
1693
|
+
markHandledHardwareKeyDown(signature)
|
|
990
1694
|
true
|
|
991
1695
|
} else {
|
|
992
1696
|
false
|
|
@@ -994,10 +1698,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
994
1698
|
}
|
|
995
1699
|
|
|
996
1700
|
KeyEvent.ACTION_UP -> {
|
|
997
|
-
if (
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1701
|
+
if (lastHandledHardwareKeySignature?.let {
|
|
1702
|
+
it.keyCode == event.keyCode && it.downTime == event.downTime
|
|
1703
|
+
} == true) {
|
|
1704
|
+
lastHandledHardwareKeySignature = null
|
|
1001
1705
|
true
|
|
1002
1706
|
} else {
|
|
1003
1707
|
false
|
|
@@ -1008,8 +1712,119 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1008
1712
|
}
|
|
1009
1713
|
}
|
|
1010
1714
|
|
|
1715
|
+
internal fun handlePrintableHardwareKeyEvent(
|
|
1716
|
+
event: KeyEvent,
|
|
1717
|
+
applyBaseEvent: () -> Boolean
|
|
1718
|
+
): Boolean {
|
|
1719
|
+
if (!isEditable || isApplyingRustState || !isPrintableHardwareMutationKey(event)) {
|
|
1720
|
+
return false
|
|
1721
|
+
}
|
|
1722
|
+
val signature = hardwareKeyEventSignature(event)
|
|
1723
|
+
return when (event.action) {
|
|
1724
|
+
KeyEvent.ACTION_DOWN -> {
|
|
1725
|
+
if (
|
|
1726
|
+
lastHandledHardwareKeySignature == signature ||
|
|
1727
|
+
didRecentlyHandleHardwareKeyDown(signature)
|
|
1728
|
+
) {
|
|
1729
|
+
true
|
|
1730
|
+
} else {
|
|
1731
|
+
val inputConnection = activeInputConnection?.takeIf {
|
|
1732
|
+
it.hasPendingComposition()
|
|
1733
|
+
}
|
|
1734
|
+
if (inputConnection != null) {
|
|
1735
|
+
var didMutate = false
|
|
1736
|
+
runWithTransientInputMutationGuard {
|
|
1737
|
+
didMutate = insertTransientHardwareText(keyEventText(event))
|
|
1738
|
+
didMutate
|
|
1739
|
+
}
|
|
1740
|
+
if (!didMutate) return false
|
|
1741
|
+
inputConnection.refreshComposingTextFromEditableForEditor()
|
|
1742
|
+
} else {
|
|
1743
|
+
applyBaseEvent()
|
|
1744
|
+
}
|
|
1745
|
+
markHandledHardwareKeyDown(signature)
|
|
1746
|
+
true
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
KeyEvent.ACTION_UP -> {
|
|
1750
|
+
if (lastHandledHardwareKeySignature?.let {
|
|
1751
|
+
it.keyCode == event.keyCode && it.downTime == event.downTime
|
|
1752
|
+
} == true) {
|
|
1753
|
+
lastHandledHardwareKeySignature = null
|
|
1754
|
+
}
|
|
1755
|
+
false
|
|
1756
|
+
}
|
|
1757
|
+
else -> false
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
private fun isPrintableHardwareMutationKey(event: KeyEvent): Boolean {
|
|
1762
|
+
if (isSupportedHardwareMutationKey(event.keyCode)) return false
|
|
1763
|
+
if (event.isCtrlPressed || event.isMetaPressed) return false
|
|
1764
|
+
return !keyEventText(event).isNullOrEmpty()
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
private fun hardwareKeyEventSignature(event: KeyEvent): HardwareKeyEventSignature =
|
|
1768
|
+
HardwareKeyEventSignature(
|
|
1769
|
+
keyCode = event.keyCode,
|
|
1770
|
+
downTime = event.downTime,
|
|
1771
|
+
repeatCount = event.repeatCount
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
@Suppress("DEPRECATION")
|
|
1775
|
+
private fun keyEventCharacters(event: KeyEvent): String? = event.characters
|
|
1776
|
+
|
|
1777
|
+
private fun keyEventText(event: KeyEvent): String? {
|
|
1778
|
+
val characters = keyEventCharacters(event)
|
|
1779
|
+
if (!characters.isNullOrEmpty()) return characters
|
|
1780
|
+
val unicodeChar = event.unicodeChar
|
|
1781
|
+
if (unicodeChar == 0) return null
|
|
1782
|
+
return runCatching {
|
|
1783
|
+
String(Character.toChars(unicodeChar))
|
|
1784
|
+
}.getOrNull()
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
private fun insertTransientHardwareText(insertedText: String?): Boolean {
|
|
1788
|
+
if (insertedText.isNullOrEmpty()) return false
|
|
1789
|
+
val editable = text ?: return false
|
|
1790
|
+
val currentText = editable.toString()
|
|
1791
|
+
val rawStart = selectionStart
|
|
1792
|
+
val rawEnd = selectionEnd
|
|
1793
|
+
if (rawStart < 0 || rawEnd < 0) return false
|
|
1794
|
+
val start = rawStart.coerceIn(0, editable.length)
|
|
1795
|
+
val end = rawEnd.coerceIn(0, editable.length)
|
|
1796
|
+
val normalizedStart = minOf(start, end)
|
|
1797
|
+
val normalizedEnd = maxOf(start, end)
|
|
1798
|
+
val (replaceStart, replaceEnd) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1799
|
+
normalizedStart,
|
|
1800
|
+
normalizedEnd,
|
|
1801
|
+
currentText
|
|
1802
|
+
)
|
|
1803
|
+
editable.replace(replaceStart, replaceEnd, insertedText)
|
|
1804
|
+
val cursor = (replaceStart + insertedText.length).coerceIn(0, editable.length)
|
|
1805
|
+
setSelection(cursor)
|
|
1806
|
+
return true
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
private fun markHandledHardwareKeyDown(signature: HardwareKeyEventSignature) {
|
|
1810
|
+
lastHandledHardwareKeySignature = signature
|
|
1811
|
+
recentHandledHardwareKeyDownSignature = signature
|
|
1812
|
+
recentHandledHardwareKeyDownUptimeMs = SystemClock.uptimeMillis()
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
private fun didRecentlyHandleHardwareKeyDown(signature: HardwareKeyEventSignature): Boolean {
|
|
1816
|
+
val recentSignature = recentHandledHardwareKeyDownSignature ?: return false
|
|
1817
|
+
val elapsedMs = SystemClock.uptimeMillis() - recentHandledHardwareKeyDownUptimeMs
|
|
1818
|
+
if (elapsedMs > RECENT_HANDLED_HARDWARE_KEY_DOWN_WINDOW_MS) {
|
|
1819
|
+
recentHandledHardwareKeyDownSignature = null
|
|
1820
|
+
recentHandledHardwareKeyDownUptimeMs = 0L
|
|
1821
|
+
return false
|
|
1822
|
+
}
|
|
1823
|
+
return recentSignature == signature
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1011
1826
|
fun performToolbarToggleMark(markName: String) {
|
|
1012
|
-
if (!isEditable || isApplyingRustState ||
|
|
1827
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1013
1828
|
val selection = currentScalarSelection() ?: return
|
|
1014
1829
|
val updateJSON = editorToggleMarkAtSelectionScalar(
|
|
1015
1830
|
editorId.toULong(),
|
|
@@ -1021,7 +1836,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1021
1836
|
}
|
|
1022
1837
|
|
|
1023
1838
|
fun performToolbarToggleList(listType: String, isActive: Boolean) {
|
|
1024
|
-
if (!isEditable || isApplyingRustState ||
|
|
1839
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1025
1840
|
val selection = currentScalarSelection() ?: return
|
|
1026
1841
|
val updateJSON = if (isActive) {
|
|
1027
1842
|
editorUnwrapFromListAtSelectionScalar(
|
|
@@ -1041,7 +1856,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1041
1856
|
}
|
|
1042
1857
|
|
|
1043
1858
|
fun performToolbarToggleBlockquote() {
|
|
1044
|
-
if (!isEditable || isApplyingRustState ||
|
|
1859
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1045
1860
|
val selection = currentScalarSelection() ?: return
|
|
1046
1861
|
val updateJSON = editorToggleBlockquoteAtSelectionScalar(
|
|
1047
1862
|
editorId.toULong(),
|
|
@@ -1052,7 +1867,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1052
1867
|
}
|
|
1053
1868
|
|
|
1054
1869
|
fun performToolbarToggleHeading(level: Int) {
|
|
1055
|
-
if (!isEditable || isApplyingRustState ||
|
|
1870
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1056
1871
|
if (level !in 1..6) return
|
|
1057
1872
|
val selection = currentScalarSelection() ?: return
|
|
1058
1873
|
val updateJSON = editorToggleHeadingAtSelectionScalar(
|
|
@@ -1065,7 +1880,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1065
1880
|
}
|
|
1066
1881
|
|
|
1067
1882
|
fun performToolbarIndentListItem() {
|
|
1068
|
-
if (!isEditable || isApplyingRustState ||
|
|
1883
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1069
1884
|
val selection = currentScalarSelection() ?: return
|
|
1070
1885
|
val updateJSON = editorIndentListItemAtSelectionScalar(
|
|
1071
1886
|
editorId.toULong(),
|
|
@@ -1076,7 +1891,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1076
1891
|
}
|
|
1077
1892
|
|
|
1078
1893
|
fun performToolbarOutdentListItem() {
|
|
1079
|
-
if (!isEditable || isApplyingRustState ||
|
|
1894
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1080
1895
|
val selection = currentScalarSelection() ?: return
|
|
1081
1896
|
val updateJSON = editorOutdentListItemAtSelectionScalar(
|
|
1082
1897
|
editorId.toULong(),
|
|
@@ -1087,7 +1902,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1087
1902
|
}
|
|
1088
1903
|
|
|
1089
1904
|
fun performToolbarInsertNode(nodeType: String) {
|
|
1090
|
-
if (!isEditable || isApplyingRustState ||
|
|
1905
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1091
1906
|
val selection = currentScalarSelection() ?: return
|
|
1092
1907
|
val updateJSON = editorInsertNodeAtSelectionScalar(
|
|
1093
1908
|
editorId.toULong(),
|
|
@@ -1099,12 +1914,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1099
1914
|
}
|
|
1100
1915
|
|
|
1101
1916
|
fun performToolbarUndo() {
|
|
1102
|
-
if (!isEditable || isApplyingRustState ||
|
|
1917
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1103
1918
|
applyUpdateJSON(editorUndo(editorId.toULong()))
|
|
1104
1919
|
}
|
|
1105
1920
|
|
|
1106
1921
|
fun performToolbarRedo() {
|
|
1107
|
-
if (!isEditable || isApplyingRustState ||
|
|
1922
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1108
1923
|
applyUpdateJSON(editorRedo(editorId.toULong()))
|
|
1109
1924
|
}
|
|
1110
1925
|
|
|
@@ -1117,32 +1932,52 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1117
1932
|
* falling back to plain text.
|
|
1118
1933
|
*/
|
|
1119
1934
|
override fun onTextContextMenuItem(id: Int): Boolean {
|
|
1120
|
-
if (!isEditable && id
|
|
1121
|
-
if (id == android.R.id.
|
|
1122
|
-
|
|
1935
|
+
if (!isEditable && isMutatingContextMenuItem(id)) return true
|
|
1936
|
+
if (id == android.R.id.cut) {
|
|
1937
|
+
handleCut()
|
|
1938
|
+
return true
|
|
1939
|
+
}
|
|
1940
|
+
if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) {
|
|
1941
|
+
handlePaste(plainTextOnly = id == android.R.id.pasteAsPlainText)
|
|
1123
1942
|
return true
|
|
1124
1943
|
}
|
|
1125
1944
|
return super.onTextContextMenuItem(id)
|
|
1126
1945
|
}
|
|
1127
1946
|
|
|
1947
|
+
private fun isMutatingContextMenuItem(id: Int): Boolean =
|
|
1948
|
+
id == android.R.id.paste ||
|
|
1949
|
+
id == android.R.id.pasteAsPlainText ||
|
|
1950
|
+
id == android.R.id.cut
|
|
1951
|
+
|
|
1128
1952
|
/**
|
|
1129
|
-
* Block accessibility-initiated text mutations (paste, set text) when not editable.
|
|
1953
|
+
* Block accessibility-initiated text mutations (paste, cut, set text) when not editable.
|
|
1130
1954
|
* Selection and copy actions remain available.
|
|
1131
1955
|
*/
|
|
1132
1956
|
override fun performAccessibilityAction(action: Int, arguments: android.os.Bundle?): Boolean {
|
|
1133
|
-
if (!isEditable && (
|
|
1134
|
-
|
|
1957
|
+
if (!isEditable && (
|
|
1958
|
+
action == android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT ||
|
|
1959
|
+
action == android.view.accessibility.AccessibilityNodeInfo.ACTION_PASTE ||
|
|
1960
|
+
action == android.view.accessibility.AccessibilityNodeInfo.ACTION_CUT
|
|
1961
|
+
)
|
|
1962
|
+
) {
|
|
1135
1963
|
return false
|
|
1136
1964
|
}
|
|
1965
|
+
if (action == android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT) {
|
|
1966
|
+
return handleAccessibilitySetText(arguments)
|
|
1967
|
+
}
|
|
1137
1968
|
return super.performAccessibilityAction(action, arguments)
|
|
1138
1969
|
}
|
|
1139
1970
|
|
|
1140
|
-
private fun handlePaste() {
|
|
1971
|
+
private fun handlePaste(plainTextOnly: Boolean) {
|
|
1141
1972
|
if (editorId == 0L) {
|
|
1142
1973
|
// Dev mode: default paste behavior.
|
|
1143
|
-
super.onTextContextMenuItem(
|
|
1974
|
+
super.onTextContextMenuItem(
|
|
1975
|
+
if (plainTextOnly) android.R.id.pasteAsPlainText else android.R.id.paste
|
|
1976
|
+
)
|
|
1144
1977
|
return
|
|
1145
1978
|
}
|
|
1979
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
1980
|
+
if (!prepareForExternalEditorUpdate()) return
|
|
1146
1981
|
|
|
1147
1982
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
1148
1983
|
?: return
|
|
@@ -1153,18 +1988,69 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1153
1988
|
|
|
1154
1989
|
// Try HTML first for rich paste.
|
|
1155
1990
|
val htmlText = item.htmlText
|
|
1156
|
-
if (htmlText != null) {
|
|
1991
|
+
if (!plainTextOnly && htmlText != null) {
|
|
1157
1992
|
pasteHTML(htmlText)
|
|
1158
1993
|
return
|
|
1159
1994
|
}
|
|
1160
1995
|
|
|
1161
1996
|
// Fallback to plain text.
|
|
1162
|
-
val plainText = item.text?.toString()
|
|
1997
|
+
val plainText = item.text?.toString() ?: item.coerceToText(context)?.toString()
|
|
1163
1998
|
if (plainText != null) {
|
|
1164
1999
|
pastePlainText(plainText)
|
|
1165
2000
|
}
|
|
1166
2001
|
}
|
|
1167
2002
|
|
|
2003
|
+
private fun handleCut() {
|
|
2004
|
+
if (editorId == 0L) {
|
|
2005
|
+
super.onTextContextMenuItem(android.R.id.cut)
|
|
2006
|
+
return
|
|
2007
|
+
}
|
|
2008
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
2009
|
+
if (!prepareForExternalEditorUpdate()) return
|
|
2010
|
+
|
|
2011
|
+
val currentText = text?.toString() ?: return
|
|
2012
|
+
val (selectionStart, selectionEnd) = normalizedUtf16SelectionRange(currentText) ?: return
|
|
2013
|
+
if (selectionStart == selectionEnd) return
|
|
2014
|
+
|
|
2015
|
+
val (utf16Start, utf16End) = PositionBridge.snapRangeToScalarBoundaries(
|
|
2016
|
+
selectionStart,
|
|
2017
|
+
selectionEnd,
|
|
2018
|
+
currentText
|
|
2019
|
+
)
|
|
2020
|
+
if (utf16Start >= utf16End) return
|
|
2021
|
+
|
|
2022
|
+
val selectedText = currentText.substring(utf16Start, utf16End)
|
|
2023
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
2024
|
+
clipboard?.setPrimaryClip(ClipData.newPlainText(null, selectedText))
|
|
2025
|
+
|
|
2026
|
+
val scalarStart = PositionBridge.utf16ToScalar(utf16Start, currentText)
|
|
2027
|
+
val scalarEnd = PositionBridge.utf16ToScalar(utf16End, currentText)
|
|
2028
|
+
deleteRangeInRust(scalarStart, scalarEnd)
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
private fun handleAccessibilitySetText(arguments: android.os.Bundle?): Boolean {
|
|
2032
|
+
val replacement = arguments
|
|
2033
|
+
?.getCharSequence(
|
|
2034
|
+
android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE
|
|
2035
|
+
)
|
|
2036
|
+
?.toString()
|
|
2037
|
+
?: return false
|
|
2038
|
+
if (editorId == 0L) {
|
|
2039
|
+
return super.performAccessibilityAction(
|
|
2040
|
+
android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT,
|
|
2041
|
+
arguments
|
|
2042
|
+
)
|
|
2043
|
+
}
|
|
2044
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return false
|
|
2045
|
+
if (!prepareForExternalEditorUpdate()) return false
|
|
2046
|
+
|
|
2047
|
+
val currentText = text?.toString() ?: return false
|
|
2048
|
+
val scalarStart = 0
|
|
2049
|
+
val scalarEnd = currentText.codePointCount(0, currentText.length)
|
|
2050
|
+
insertPlainTextRangeInRust(scalarStart, scalarEnd, replacement)
|
|
2051
|
+
return true
|
|
2052
|
+
}
|
|
2053
|
+
|
|
1168
2054
|
// ── Selection Change ────────────────────────────────────────────────
|
|
1169
2055
|
|
|
1170
2056
|
/**
|
|
@@ -1183,23 +2069,35 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1183
2069
|
ensureSelectionVisible()
|
|
1184
2070
|
onSelectionOrContentMayChange?.invoke()
|
|
1185
2071
|
|
|
1186
|
-
|
|
2072
|
+
syncCurrentSelectionToRust()
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
private fun syncCurrentSelectionToRust() {
|
|
2076
|
+
if (!hasLiveEditor()) return
|
|
1187
2077
|
|
|
1188
2078
|
val currentText = text?.toString() ?: ""
|
|
1189
2079
|
if (currentText != lastAuthorizedText) return
|
|
1190
|
-
val scalarAnchor =
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
scalarAnchor
|
|
1197
|
-
|
|
1198
|
-
|
|
2080
|
+
val (scalarAnchor, scalarHead) = rawScalarSelection(currentText) ?: return
|
|
2081
|
+
|
|
2082
|
+
val selectionHook = onSetSelectionScalarInRustForTesting
|
|
2083
|
+
val docAnchor: Int
|
|
2084
|
+
val docHead: Int
|
|
2085
|
+
if (selectionHook != null) {
|
|
2086
|
+
selectionHook(scalarAnchor, scalarHead)
|
|
2087
|
+
docAnchor = scalarAnchor
|
|
2088
|
+
docHead = scalarHead
|
|
2089
|
+
} else {
|
|
2090
|
+
// Sync selection to Rust (converts scalar→doc internally).
|
|
2091
|
+
editorSetSelectionScalar(
|
|
2092
|
+
editorId.toULong(),
|
|
2093
|
+
scalarAnchor.toUInt(),
|
|
2094
|
+
scalarHead.toUInt()
|
|
2095
|
+
)
|
|
1199
2096
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
2097
|
+
// Emit doc positions (not scalar offsets) to match the Selection contract.
|
|
2098
|
+
docAnchor = editorScalarToDoc(editorId.toULong(), scalarAnchor.toUInt()).toInt()
|
|
2099
|
+
docHead = editorScalarToDoc(editorId.toULong(), scalarHead.toUInt()).toInt()
|
|
2100
|
+
}
|
|
1203
2101
|
editorListener?.onSelectionChanged(docAnchor, docHead)
|
|
1204
2102
|
}
|
|
1205
2103
|
|
|
@@ -1209,6 +2107,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1209
2107
|
* Insert text at a scalar position via the Rust editor.
|
|
1210
2108
|
*/
|
|
1211
2109
|
private fun insertTextInRust(text: String, atScalarPos: Int) {
|
|
2110
|
+
if (!hasLiveEditor()) return
|
|
1212
2111
|
onInsertTextInRustForTesting?.let { callback ->
|
|
1213
2112
|
callback(text, atScalarPos)
|
|
1214
2113
|
return
|
|
@@ -1218,6 +2117,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1218
2117
|
}
|
|
1219
2118
|
|
|
1220
2119
|
private fun replaceTextRangeInRust(scalarFrom: Int, scalarTo: Int, text: String) {
|
|
2120
|
+
if (!hasLiveEditor()) return
|
|
1221
2121
|
onReplaceTextInRustForTesting?.let { callback ->
|
|
1222
2122
|
callback(scalarFrom, scalarTo, text)
|
|
1223
2123
|
return
|
|
@@ -1231,6 +2131,109 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1231
2131
|
applyUpdateJSON(updateJSON)
|
|
1232
2132
|
}
|
|
1233
2133
|
|
|
2134
|
+
private fun insertPlainTextRangeInRust(
|
|
2135
|
+
scalarFrom: Int,
|
|
2136
|
+
scalarTo: Int,
|
|
2137
|
+
text: String,
|
|
2138
|
+
requestedCursorScalar: Int? = null
|
|
2139
|
+
) {
|
|
2140
|
+
if (!hasLiveEditor()) return
|
|
2141
|
+
if (text.isEmpty()) {
|
|
2142
|
+
if (scalarFrom != scalarTo) {
|
|
2143
|
+
deleteRangeInRust(scalarFrom, scalarTo)
|
|
2144
|
+
}
|
|
2145
|
+
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2146
|
+
return
|
|
2147
|
+
}
|
|
2148
|
+
if (text.indexOf('\n') >= 0 || text.indexOf('\r') >= 0) {
|
|
2149
|
+
val docJson = plainTextDocumentFragmentJson(text)
|
|
2150
|
+
onInsertContentJsonAtSelectionScalarForTesting?.let { callback ->
|
|
2151
|
+
callback(scalarFrom, scalarTo, docJson)
|
|
2152
|
+
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2153
|
+
return
|
|
2154
|
+
}
|
|
2155
|
+
val updateJSON = editorInsertContentJsonAtSelectionScalar(
|
|
2156
|
+
editorId.toULong(),
|
|
2157
|
+
scalarFrom.toUInt(),
|
|
2158
|
+
scalarTo.toUInt(),
|
|
2159
|
+
docJson
|
|
2160
|
+
)
|
|
2161
|
+
applyUpdateJSON(updateJSON)
|
|
2162
|
+
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2163
|
+
return
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
if (scalarFrom != scalarTo) {
|
|
2167
|
+
replaceTextRangeInRust(scalarFrom, scalarTo, text)
|
|
2168
|
+
} else {
|
|
2169
|
+
insertTextInRust(text, scalarFrom)
|
|
2170
|
+
}
|
|
2171
|
+
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
private fun requestedCursorScalar(
|
|
2175
|
+
scalarFrom: Int,
|
|
2176
|
+
scalarTo: Int,
|
|
2177
|
+
currentText: String,
|
|
2178
|
+
insertedText: String,
|
|
2179
|
+
newCursorPosition: Int
|
|
2180
|
+
): Int? {
|
|
2181
|
+
if (newCursorPosition == 1) return null
|
|
2182
|
+
val insertedScalarLength = insertedText.codePointCount(0, insertedText.length)
|
|
2183
|
+
val currentScalarLength = currentText.codePointCount(0, currentText.length)
|
|
2184
|
+
val nextScalarLength =
|
|
2185
|
+
(currentScalarLength - (scalarTo - scalarFrom) + insertedScalarLength).coerceAtLeast(0)
|
|
2186
|
+
val requested = if (newCursorPosition > 0) {
|
|
2187
|
+
scalarFrom + insertedScalarLength + newCursorPosition - 1
|
|
2188
|
+
} else {
|
|
2189
|
+
scalarFrom + newCursorPosition
|
|
2190
|
+
}
|
|
2191
|
+
return requested.coerceIn(0, nextScalarLength)
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
private fun applyRequestedCursorScalar(requestedCursorScalar: Int?) {
|
|
2195
|
+
val requested = requestedCursorScalar ?: return
|
|
2196
|
+
if (!hasLiveEditor()) return
|
|
2197
|
+
val currentText = text?.toString().orEmpty()
|
|
2198
|
+
val safeScalar = requested.coerceAtLeast(0)
|
|
2199
|
+
onSetSelectionScalarInRustForTesting?.let { callback ->
|
|
2200
|
+
callback(safeScalar, safeScalar)
|
|
2201
|
+
} ?: editorSetSelectionScalar(
|
|
2202
|
+
editorId.toULong(),
|
|
2203
|
+
safeScalar.toUInt(),
|
|
2204
|
+
safeScalar.toUInt()
|
|
2205
|
+
)
|
|
2206
|
+
val localScalar = safeScalar.coerceIn(0, currentText.codePointCount(0, currentText.length))
|
|
2207
|
+
val safeUtf16 = PositionBridge.scalarToUtf16(localScalar, currentText)
|
|
2208
|
+
.coerceIn(0, currentText.length)
|
|
2209
|
+
if (selectionStart != safeUtf16 || selectionEnd != safeUtf16) {
|
|
2210
|
+
setSelection(safeUtf16, safeUtf16)
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
private fun plainTextDocumentFragmentJson(text: String): String {
|
|
2215
|
+
val normalizedText = text.replace("\r\n", "\n").replace('\r', '\n')
|
|
2216
|
+
val content = org.json.JSONArray()
|
|
2217
|
+
for (line in normalizedText.split('\n')) {
|
|
2218
|
+
val paragraph = org.json.JSONObject().put("type", "paragraph")
|
|
2219
|
+
if (line.isNotEmpty()) {
|
|
2220
|
+
paragraph.put(
|
|
2221
|
+
"content",
|
|
2222
|
+
org.json.JSONArray().put(
|
|
2223
|
+
org.json.JSONObject()
|
|
2224
|
+
.put("type", "text")
|
|
2225
|
+
.put("text", line)
|
|
2226
|
+
)
|
|
2227
|
+
)
|
|
2228
|
+
}
|
|
2229
|
+
content.put(paragraph)
|
|
2230
|
+
}
|
|
2231
|
+
return org.json.JSONObject()
|
|
2232
|
+
.put("type", "doc")
|
|
2233
|
+
.put("content", content)
|
|
2234
|
+
.toString()
|
|
2235
|
+
}
|
|
2236
|
+
|
|
1234
2237
|
private fun nativeTextMutationFromAuthorizedDiff(currentText: String): NativeTextMutation? {
|
|
1235
2238
|
val authorizedText = lastAuthorizedText
|
|
1236
2239
|
if (currentText == authorizedText) return null
|
|
@@ -1243,6 +2246,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1243
2246
|
) {
|
|
1244
2247
|
prefix++
|
|
1245
2248
|
}
|
|
2249
|
+
prefix = minOf(
|
|
2250
|
+
PositionBridge.snapToScalarBoundary(prefix, authorizedText, biasForward = false),
|
|
2251
|
+
PositionBridge.snapToScalarBoundary(prefix, currentText, biasForward = false)
|
|
2252
|
+
)
|
|
1246
2253
|
|
|
1247
2254
|
var authorizedEnd = authorizedText.length
|
|
1248
2255
|
var currentEnd = currentText.length
|
|
@@ -1254,41 +2261,190 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1254
2261
|
authorizedEnd--
|
|
1255
2262
|
currentEnd--
|
|
1256
2263
|
}
|
|
2264
|
+
authorizedEnd = PositionBridge.snapToScalarBoundary(
|
|
2265
|
+
authorizedEnd,
|
|
2266
|
+
authorizedText,
|
|
2267
|
+
biasForward = true
|
|
2268
|
+
)
|
|
2269
|
+
currentEnd = PositionBridge.snapToScalarBoundary(
|
|
2270
|
+
currentEnd,
|
|
2271
|
+
currentText,
|
|
2272
|
+
biasForward = true
|
|
2273
|
+
)
|
|
1257
2274
|
|
|
1258
2275
|
val replacementText = currentText.substring(prefix, currentEnd)
|
|
2276
|
+
val rawSelectionStart = selectionStart
|
|
2277
|
+
val rawSelectionEnd = selectionEnd
|
|
2278
|
+
val selectionAnchorUtf16 = rawSelectionStart
|
|
2279
|
+
.takeIf { it >= 0 }
|
|
2280
|
+
?.let { PositionBridge.snapToScalarBoundary(it, currentText, biasForward = true) }
|
|
2281
|
+
val selectionHeadUtf16 = rawSelectionEnd
|
|
2282
|
+
.takeIf { it >= 0 }
|
|
2283
|
+
?.let { PositionBridge.snapToScalarBoundary(it, currentText, biasForward = true) }
|
|
1259
2284
|
return NativeTextMutation(
|
|
1260
2285
|
scalarFrom = PositionBridge.utf16ToScalar(prefix, authorizedText),
|
|
1261
2286
|
scalarTo = PositionBridge.utf16ToScalar(authorizedEnd, authorizedText),
|
|
1262
2287
|
replacementText = replacementText,
|
|
1263
|
-
resultingText = currentText
|
|
2288
|
+
resultingText = currentText,
|
|
2289
|
+
selectionScalarAnchor = selectionAnchorUtf16?.let {
|
|
2290
|
+
PositionBridge.utf16ToScalar(it, currentText)
|
|
2291
|
+
},
|
|
2292
|
+
selectionScalarHead = selectionHeadUtf16?.let {
|
|
2293
|
+
PositionBridge.utf16ToScalar(it, currentText)
|
|
2294
|
+
}
|
|
2295
|
+
)
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
private fun shouldAdoptNativeTextMutation(
|
|
2299
|
+
mutation: NativeTextMutation,
|
|
2300
|
+
allowAfterBlur: Boolean = false
|
|
2301
|
+
): Boolean {
|
|
2302
|
+
if (!isEditable) return false
|
|
2303
|
+
if (isNativeTextMutationAdoptionSuppressedForCurrentRevision()) return false
|
|
2304
|
+
if (!hasFocus()) {
|
|
2305
|
+
return allowAfterBlur &&
|
|
2306
|
+
canAdoptNativeTextMutationAfterBlur() &&
|
|
2307
|
+
shouldAdoptFinalNativeTextMutation(mutation)
|
|
2308
|
+
}
|
|
2309
|
+
return shouldAdoptFinalNativeTextMutation(mutation)
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
private fun shouldAdoptFinalNativeTextMutation(mutation: NativeTextMutation): Boolean {
|
|
2313
|
+
if (composingTextForEditor() != null) return false
|
|
2314
|
+
val trackedRange = compositionReplacementRange() ?: return true
|
|
2315
|
+
val authorizedText = lastAuthorizedText
|
|
2316
|
+
val trackedStart = PositionBridge.utf16ToScalar(trackedRange.first, authorizedText)
|
|
2317
|
+
val trackedEnd = PositionBridge.utf16ToScalar(trackedRange.second, authorizedText)
|
|
2318
|
+
if (trackedStart == trackedEnd) {
|
|
2319
|
+
return mutation.scalarFrom == trackedStart &&
|
|
2320
|
+
mutation.scalarTo == trackedStart &&
|
|
2321
|
+
mutation.replacementText.isNotEmpty()
|
|
2322
|
+
}
|
|
2323
|
+
if (mutation.scalarFrom == mutation.scalarTo) {
|
|
2324
|
+
return mutation.replacementText.isNotEmpty() &&
|
|
2325
|
+
mutation.scalarFrom >= trackedStart &&
|
|
2326
|
+
mutation.scalarFrom <= trackedEnd
|
|
2327
|
+
}
|
|
2328
|
+
return mutation.scalarFrom < trackedEnd && mutation.scalarTo > trackedStart
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
private fun drainNativeTextMutationIfNeeded(allowAfterBlur: Boolean): Boolean {
|
|
2332
|
+
if (editorId == 0L) return true
|
|
2333
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return false
|
|
2334
|
+
val editable = text
|
|
2335
|
+
val currentText = editable?.toString() ?: ""
|
|
2336
|
+
if (currentText == lastAuthorizedText) return true
|
|
2337
|
+
|
|
2338
|
+
val mutation = nativeTextMutationFromAuthorizedDiff(currentText)
|
|
2339
|
+
if (mutation != null && shouldAdoptNativeTextMutation(mutation, allowAfterBlur)) {
|
|
2340
|
+
commitNativeTextMutation(mutation)
|
|
2341
|
+
return true
|
|
2342
|
+
}
|
|
2343
|
+
recordImeTraceForTesting(
|
|
2344
|
+
"nativeMutationNoop",
|
|
2345
|
+
"reason=${if (mutation == null) "noDiffRange" else "notAdoptable"} allowAfterBlur=$allowAfterBlur currentLength=${currentText.length} authorizedLength=${lastAuthorizedText.length}"
|
|
2346
|
+
)
|
|
2347
|
+
return false
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
private fun beginNativeTextMutationAfterBlurWindow() {
|
|
2351
|
+
if (!hasLiveEditor()) {
|
|
2352
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
2353
|
+
return
|
|
2354
|
+
}
|
|
2355
|
+
nativeTextMutationAfterBlurWindow = NativeTextMutationAfterBlurWindow(
|
|
2356
|
+
editorId = editorId,
|
|
2357
|
+
authorizedTextRevision = lastAuthorizedTextRevision,
|
|
2358
|
+
deadlineMs = SystemClock.uptimeMillis() + NATIVE_TEXT_MUTATION_AFTER_BLUR_WINDOW_MS
|
|
1264
2359
|
)
|
|
1265
2360
|
}
|
|
1266
2361
|
|
|
1267
|
-
private fun
|
|
1268
|
-
|
|
1269
|
-
|
|
2362
|
+
private fun clearNativeTextMutationAfterBlurWindow() {
|
|
2363
|
+
nativeTextMutationAfterBlurWindow = null
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
private fun suppressNativeTextMutationAdoptionForCurrentRevision() {
|
|
2367
|
+
if (!hasLiveEditor()) {
|
|
2368
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
2369
|
+
return
|
|
2370
|
+
}
|
|
2371
|
+
nativeTextMutationAdoptionSuppression = NativeTextMutationAdoptionSuppression(
|
|
2372
|
+
editorId = editorId,
|
|
2373
|
+
authorizedTextRevision = lastAuthorizedTextRevision
|
|
2374
|
+
)
|
|
2375
|
+
}
|
|
1270
2376
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
2377
|
+
private fun clearNativeTextMutationAdoptionSuppression() {
|
|
2378
|
+
nativeTextMutationAdoptionSuppression = null
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
private fun isNativeTextMutationAdoptionSuppressedForCurrentRevision(): Boolean {
|
|
2382
|
+
val suppression = nativeTextMutationAdoptionSuppression ?: return false
|
|
2383
|
+
if (
|
|
2384
|
+
suppression.editorId != editorId ||
|
|
2385
|
+
suppression.authorizedTextRevision != lastAuthorizedTextRevision
|
|
2386
|
+
) {
|
|
2387
|
+
nativeTextMutationAdoptionSuppression = null
|
|
2388
|
+
return false
|
|
2389
|
+
}
|
|
2390
|
+
return true
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
private fun canAdoptNativeTextMutationAfterBlur(): Boolean {
|
|
2394
|
+
val window = nativeTextMutationAfterBlurWindow ?: return false
|
|
2395
|
+
val now = SystemClock.uptimeMillis()
|
|
2396
|
+
if (now > window.deadlineMs ||
|
|
2397
|
+
window.editorId != editorId ||
|
|
2398
|
+
window.authorizedTextRevision != lastAuthorizedTextRevision ||
|
|
2399
|
+
window.didAdoptMutation
|
|
2400
|
+
) {
|
|
2401
|
+
nativeTextMutationAfterBlurWindow = null
|
|
2402
|
+
return false
|
|
2403
|
+
}
|
|
2404
|
+
return true
|
|
1274
2405
|
}
|
|
1275
2406
|
|
|
1276
2407
|
private fun commitNativeTextMutation(mutation: NativeTextMutation) {
|
|
1277
|
-
if ((
|
|
2408
|
+
if (!hasLiveEditor()) return
|
|
2409
|
+
if ((text?.toString() ?: "") != mutation.resultingText) {
|
|
2410
|
+
recordImeTraceForTesting(
|
|
2411
|
+
"nativeMutationNoop",
|
|
2412
|
+
"reason=staleResult range=${mutation.scalarFrom}..${mutation.scalarTo} replacementLength=${mutation.replacementText.length}"
|
|
2413
|
+
)
|
|
2414
|
+
return
|
|
2415
|
+
}
|
|
2416
|
+
val shouldRestartInput = hasFocus()
|
|
2417
|
+
retireInputConnectionForEditor()
|
|
2418
|
+
nativeTextMutationAfterBlurWindow?.didAdoptMutation = true
|
|
2419
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1278
2420
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
2421
|
+
recordImeTraceForTesting(
|
|
2422
|
+
"nativeMutationApply",
|
|
2423
|
+
"range=${mutation.scalarFrom}..${mutation.scalarTo} replacementLength=${mutation.replacementText.length} restartInput=$shouldRestartInput"
|
|
2424
|
+
)
|
|
2425
|
+
if (mutation.replacementText.isEmpty()) {
|
|
1284
2426
|
deleteRangeInRust(mutation.scalarFrom, mutation.scalarTo)
|
|
1285
2427
|
} else {
|
|
1286
|
-
|
|
2428
|
+
insertPlainTextRangeInRust(
|
|
1287
2429
|
mutation.scalarFrom,
|
|
1288
2430
|
mutation.scalarTo,
|
|
1289
2431
|
mutation.replacementText
|
|
1290
2432
|
)
|
|
1291
2433
|
}
|
|
2434
|
+
restoreSelectionAfterNativeTextMutation(mutation)
|
|
2435
|
+
if (shouldRestartInput) {
|
|
2436
|
+
restartInputForEditor()
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
private fun restoreSelectionAfterNativeTextMutation(mutation: NativeTextMutation) {
|
|
2441
|
+
val selectionScalarAnchor = mutation.selectionScalarAnchor ?: return
|
|
2442
|
+
val selectionScalarHead = mutation.selectionScalarHead ?: return
|
|
2443
|
+
val currentText = text?.toString() ?: return
|
|
2444
|
+
val anchorUtf16 = PositionBridge.scalarToUtf16(selectionScalarAnchor, currentText)
|
|
2445
|
+
val headUtf16 = PositionBridge.scalarToUtf16(selectionScalarHead, currentText)
|
|
2446
|
+
val length = currentText.length
|
|
2447
|
+
setSelection(anchorUtf16.coerceIn(0, length), headUtf16.coerceIn(0, length))
|
|
1292
2448
|
}
|
|
1293
2449
|
|
|
1294
2450
|
/**
|
|
@@ -1298,6 +2454,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1298
2454
|
* @param scalarTo End scalar offset (exclusive).
|
|
1299
2455
|
*/
|
|
1300
2456
|
private fun deleteRangeInRust(scalarFrom: Int, scalarTo: Int) {
|
|
2457
|
+
if (!hasLiveEditor()) return
|
|
1301
2458
|
if (scalarFrom >= scalarTo) return
|
|
1302
2459
|
onDeleteRangeInRustForTesting?.let { callback ->
|
|
1303
2460
|
callback(scalarFrom, scalarTo)
|
|
@@ -1308,6 +2465,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1308
2465
|
}
|
|
1309
2466
|
|
|
1310
2467
|
private fun deleteBackwardAtSelectionScalarInRust(scalarAnchor: Int, scalarHead: Int) {
|
|
2468
|
+
if (!hasLiveEditor()) return
|
|
1311
2469
|
onDeleteBackwardAtSelectionScalarInRustForTesting?.let { callback ->
|
|
1312
2470
|
callback(scalarAnchor, scalarHead)
|
|
1313
2471
|
return
|
|
@@ -1324,16 +2482,84 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1324
2482
|
* Split a block at a scalar position via the Rust editor.
|
|
1325
2483
|
*/
|
|
1326
2484
|
private fun splitBlockInRust(atScalarPos: Int) {
|
|
2485
|
+
if (!hasLiveEditor()) return
|
|
1327
2486
|
val updateJSON = editorSplitBlockScalar(editorId.toULong(), atScalarPos.toUInt())
|
|
1328
2487
|
applyUpdateJSON(updateJSON)
|
|
1329
2488
|
}
|
|
1330
2489
|
|
|
1331
|
-
private fun
|
|
2490
|
+
private fun deleteAndSplitInRust(scalarFrom: Int, scalarTo: Int) {
|
|
2491
|
+
if (!hasLiveEditor()) return
|
|
2492
|
+
onDeleteAndSplitScalarInRustForTesting?.let { callback ->
|
|
2493
|
+
callback(scalarFrom, scalarTo)
|
|
2494
|
+
return
|
|
2495
|
+
}
|
|
2496
|
+
val updateJSON = editorDeleteAndSplitScalar(
|
|
2497
|
+
editorId.toULong(),
|
|
2498
|
+
scalarFrom.toUInt(),
|
|
2499
|
+
scalarTo.toUInt()
|
|
2500
|
+
)
|
|
2501
|
+
applyUpdateJSON(updateJSON)
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
internal fun currentScalarSelection(): Pair<Int, Int>? {
|
|
2505
|
+
val currentText = text?.toString() ?: return null
|
|
2506
|
+
return normalizedScalarSelectionRange(currentText)
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
private fun normalizedUtf16SelectionRange(currentText: String): Pair<Int, Int>? {
|
|
2510
|
+
val start = selectionStart
|
|
2511
|
+
val end = selectionEnd
|
|
2512
|
+
if (start < 0 || end < 0) return null
|
|
2513
|
+
val clampedStart = start.coerceIn(0, currentText.length)
|
|
2514
|
+
val clampedEnd = end.coerceIn(0, currentText.length)
|
|
2515
|
+
return minOf(clampedStart, clampedEnd) to maxOf(clampedStart, clampedEnd)
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
private fun normalizedUtf16SelectionRange(): Pair<Int, Int>? {
|
|
1332
2519
|
val currentText = text?.toString() ?: return null
|
|
1333
|
-
return
|
|
1334
|
-
|
|
1335
|
-
|
|
2520
|
+
return normalizedUtf16SelectionRange(currentText)
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
private fun normalizedScalarSelectionRange(currentText: String): Pair<Int, Int>? {
|
|
2524
|
+
val (start, end) = normalizedUtf16SelectionRange(currentText) ?: return null
|
|
2525
|
+
val (snappedStart, snappedEnd) = if (start == end) {
|
|
2526
|
+
val snapped = PositionBridge.snapToScalarBoundary(
|
|
2527
|
+
start,
|
|
2528
|
+
currentText,
|
|
2529
|
+
biasForward = true
|
|
2530
|
+
)
|
|
2531
|
+
snapped to snapped
|
|
2532
|
+
} else {
|
|
2533
|
+
PositionBridge.snapRangeToScalarBoundaries(start, end, currentText)
|
|
2534
|
+
}
|
|
2535
|
+
return PositionBridge.utf16ToScalar(snappedStart, currentText) to
|
|
2536
|
+
PositionBridge.utf16ToScalar(snappedEnd, currentText)
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
private fun rawScalarSelection(currentText: String): Pair<Int, Int>? {
|
|
2540
|
+
val anchor = selectionStart
|
|
2541
|
+
val head = selectionEnd
|
|
2542
|
+
if (anchor < 0 || head < 0) return null
|
|
2543
|
+
val clampedAnchor = anchor.coerceIn(0, currentText.length)
|
|
2544
|
+
val clampedHead = head.coerceIn(0, currentText.length)
|
|
2545
|
+
if (clampedAnchor == clampedHead) {
|
|
2546
|
+
val snapped = PositionBridge.snapToScalarBoundary(
|
|
2547
|
+
clampedAnchor,
|
|
2548
|
+
currentText,
|
|
2549
|
+
biasForward = true
|
|
2550
|
+
)
|
|
2551
|
+
val scalar = PositionBridge.utf16ToScalar(snapped, currentText)
|
|
2552
|
+
return scalar to scalar
|
|
2553
|
+
}
|
|
2554
|
+
val (rangeStart, rangeEnd) = PositionBridge.snapRangeToScalarBoundaries(
|
|
2555
|
+
minOf(clampedAnchor, clampedHead),
|
|
2556
|
+
maxOf(clampedAnchor, clampedHead),
|
|
2557
|
+
currentText
|
|
1336
2558
|
)
|
|
2559
|
+
val snappedAnchor = if (clampedAnchor < clampedHead) rangeStart else rangeEnd
|
|
2560
|
+
val snappedHead = if (clampedAnchor < clampedHead) rangeEnd else rangeStart
|
|
2561
|
+
return PositionBridge.utf16ToScalar(snappedAnchor, currentText) to
|
|
2562
|
+
PositionBridge.utf16ToScalar(snappedHead, currentText)
|
|
1337
2563
|
}
|
|
1338
2564
|
|
|
1339
2565
|
fun selectedImageGeometry(): SelectedImageGeometry? {
|
|
@@ -1352,7 +2578,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1352
2578
|
val textLayout = layout ?: return null
|
|
1353
2579
|
val currentText = text?.toString() ?: return null
|
|
1354
2580
|
val scalarPos = PositionBridge.utf16ToScalar(spanStart, currentText)
|
|
1355
|
-
val docPos = if (
|
|
2581
|
+
val docPos = if (hasLiveEditor()) {
|
|
1356
2582
|
editorScalarToDoc(editorId.toULong(), scalarPos.toUInt()).toInt()
|
|
1357
2583
|
} else {
|
|
1358
2584
|
0
|
|
@@ -1366,7 +2592,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1366
2592
|
}
|
|
1367
2593
|
|
|
1368
2594
|
fun resizeImageAtDocPos(docPos: Int, widthPx: Float, heightPx: Float) {
|
|
1369
|
-
if (
|
|
2595
|
+
if (!hasLiveEditor()) return
|
|
1370
2596
|
val density = resources.displayMetrics.density
|
|
1371
2597
|
val widthDp = maxOf(48, (widthPx / density).roundToInt())
|
|
1372
2598
|
val heightDp = maxOf(48, (heightPx / density).roundToInt())
|
|
@@ -1380,7 +2606,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1380
2606
|
}
|
|
1381
2607
|
|
|
1382
2608
|
private fun isSelectionInsideList(): Boolean {
|
|
1383
|
-
if (
|
|
2609
|
+
if (!hasLiveEditor()) return false
|
|
1384
2610
|
|
|
1385
2611
|
return try {
|
|
1386
2612
|
val state = org.json.JSONObject(editorGetCurrentState(editorId.toULong()))
|
|
@@ -1396,6 +2622,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1396
2622
|
* Paste HTML content through Rust.
|
|
1397
2623
|
*/
|
|
1398
2624
|
private fun pasteHTML(html: String) {
|
|
2625
|
+
if (!hasLiveEditor()) return
|
|
2626
|
+
syncCurrentSelectionToRust()
|
|
2627
|
+
onInsertContentHtmlInRustForTesting?.let { callback ->
|
|
2628
|
+
callback(html)
|
|
2629
|
+
return
|
|
2630
|
+
}
|
|
1399
2631
|
val updateJSON = editorInsertContentHtml(editorId.toULong(), html)
|
|
1400
2632
|
applyUpdateJSON(updateJSON)
|
|
1401
2633
|
}
|
|
@@ -1404,9 +2636,8 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1404
2636
|
* Paste plain text through Rust.
|
|
1405
2637
|
*/
|
|
1406
2638
|
private fun pastePlainText(text: String) {
|
|
1407
|
-
val
|
|
1408
|
-
|
|
1409
|
-
insertTextInRust(text, scalarPos)
|
|
2639
|
+
val (scalarStart, scalarEnd) = currentScalarSelection() ?: return
|
|
2640
|
+
insertPlainTextRangeInRust(scalarStart, scalarEnd, text)
|
|
1410
2641
|
}
|
|
1411
2642
|
|
|
1412
2643
|
// ── Applying Rust State ─────────────────────────────────────────────
|
|
@@ -1477,6 +2708,8 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1477
2708
|
replaceRange: RenderReplaceRange? = null,
|
|
1478
2709
|
usedPatch: Boolean
|
|
1479
2710
|
) {
|
|
2711
|
+
val hadCompositionTracking = hasCompositionTrackingForEditor()
|
|
2712
|
+
var shouldRestartInput = false
|
|
1480
2713
|
isApplyingRustState = true
|
|
1481
2714
|
beginBatchEdit()
|
|
1482
2715
|
try {
|
|
@@ -1486,11 +2719,22 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1486
2719
|
setText(spannable)
|
|
1487
2720
|
}
|
|
1488
2721
|
lastAuthorizedText = text?.toString().orEmpty()
|
|
2722
|
+
lastAuthorizedRenderedText = text?.let { SpannableStringBuilder(it) }
|
|
2723
|
+
lastAuthorizedTextRevision += 1L
|
|
2724
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
2725
|
+
if (hadCompositionTracking) {
|
|
2726
|
+
retireInputConnectionForEditor()
|
|
2727
|
+
shouldRestartInput = true
|
|
2728
|
+
} else {
|
|
2729
|
+
clearCompositionTrackingForEditor()
|
|
2730
|
+
}
|
|
1489
2731
|
lastRenderAppliedPatchForTesting = usedPatch
|
|
2732
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1490
2733
|
} finally {
|
|
1491
2734
|
endBatchEdit()
|
|
1492
2735
|
isApplyingRustState = false
|
|
1493
2736
|
}
|
|
2737
|
+
restartInputAfterCompositionInvalidationIfNeeded(shouldRestartInput)
|
|
1494
2738
|
}
|
|
1495
2739
|
|
|
1496
2740
|
private fun buildPatchedSpannable(patch: ParsedRenderPatch): android.text.SpannableStringBuilder =
|
|
@@ -1691,6 +2935,8 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1691
2935
|
if (shouldSkipRender) {
|
|
1692
2936
|
lastRenderAppliedPatchForTesting = false
|
|
1693
2937
|
currentRenderBlocksJson = resolvedRenderBlocks?.let(::cloneJsonArray)
|
|
2938
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
2939
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1694
2940
|
buildRenderNanos = 0L
|
|
1695
2941
|
applyRenderNanos = 0L
|
|
1696
2942
|
} else {
|
|
@@ -1911,7 +3157,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1911
3157
|
|
|
1912
3158
|
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
|
|
1913
3159
|
super.onFocusChanged(focused, direction, previouslyFocusedRect)
|
|
1914
|
-
if (
|
|
3160
|
+
if (focused) {
|
|
3161
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3162
|
+
} else {
|
|
3163
|
+
beginNativeTextMutationAfterBlurWindow()
|
|
1915
3164
|
clearExplicitSelectedImageRange()
|
|
1916
3165
|
}
|
|
1917
3166
|
}
|
|
@@ -2018,6 +3267,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2018
3267
|
*/
|
|
2019
3268
|
private fun applySelectionFromJSON(selection: org.json.JSONObject) {
|
|
2020
3269
|
val type = selection.optString("type", "") ?: return
|
|
3270
|
+
if (isEditorDestroyedForInput()) return
|
|
2021
3271
|
|
|
2022
3272
|
isApplyingRustState = true
|
|
2023
3273
|
try {
|
|
@@ -2029,12 +3279,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2029
3279
|
// Convert doc positions to scalar offsets.
|
|
2030
3280
|
val scalarAnchor = editorDocToScalar(editorId.toULong(), docAnchor.toUInt()).toInt()
|
|
2031
3281
|
val scalarHead = editorDocToScalar(editorId.toULong(), docHead.toUInt()).toInt()
|
|
2032
|
-
val
|
|
2033
|
-
val
|
|
3282
|
+
val anchorUtf16 = PositionBridge.scalarToUtf16(scalarAnchor, currentText)
|
|
3283
|
+
val headUtf16 = PositionBridge.scalarToUtf16(scalarHead, currentText)
|
|
2034
3284
|
val len = text?.length ?: 0
|
|
2035
3285
|
setSelection(
|
|
2036
|
-
|
|
2037
|
-
|
|
3286
|
+
anchorUtf16.coerceIn(0, len),
|
|
3287
|
+
headUtf16.coerceIn(0, len)
|
|
2038
3288
|
)
|
|
2039
3289
|
}
|
|
2040
3290
|
"node" -> {
|
|
@@ -2081,13 +3331,13 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2081
3331
|
|
|
2082
3332
|
override fun afterTextChanged(s: Editable?) {
|
|
2083
3333
|
if (isApplyingRustState) return
|
|
2084
|
-
if (
|
|
3334
|
+
if (!hasLiveEditor()) return
|
|
2085
3335
|
|
|
2086
3336
|
val currentText = s?.toString() ?: ""
|
|
2087
3337
|
if (currentText == lastAuthorizedText) return
|
|
2088
3338
|
|
|
2089
3339
|
val mutation = nativeTextMutationFromAuthorizedDiff(currentText)
|
|
2090
|
-
if (mutation != null && shouldAdoptNativeTextMutation(
|
|
3340
|
+
if (mutation != null && shouldAdoptNativeTextMutation(mutation, allowAfterBlur = true)) {
|
|
2091
3341
|
commitNativeTextMutation(mutation)
|
|
2092
3342
|
return
|
|
2093
3343
|
}
|
|
@@ -2113,6 +3363,9 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2113
3363
|
private const val DEFAULT_AUTO_CORRECT = true
|
|
2114
3364
|
private const val DEFAULT_KEYBOARD_TYPE = "default"
|
|
2115
3365
|
private const val EMPTY_BLOCK_PLACEHOLDER = '\u200B'
|
|
3366
|
+
private const val IME_TRACE_LIMIT_FOR_TESTING = 80
|
|
3367
|
+
private const val NATIVE_TEXT_MUTATION_AFTER_BLUR_WINDOW_MS = 750L
|
|
3368
|
+
private const val RECENT_HANDLED_HARDWARE_KEY_DOWN_WINDOW_MS = 750L
|
|
2116
3369
|
private const val LOG_TAG = "NativeEditor"
|
|
2117
3370
|
}
|
|
2118
3371
|
}
|