@apollohg/react-native-prose-editor 0.5.15 → 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 +1454 -127
- 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/Info.plist +5 -5
- 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 +787 -51
- 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
|
|
@@ -18,6 +21,7 @@ import android.util.Log
|
|
|
18
21
|
import android.util.TypedValue
|
|
19
22
|
import android.view.KeyEvent
|
|
20
23
|
import android.view.MotionEvent
|
|
24
|
+
import android.view.inputmethod.BaseInputConnection
|
|
21
25
|
import android.view.inputmethod.EditorInfo
|
|
22
26
|
import android.view.inputmethod.InputConnection
|
|
23
27
|
import android.view.inputmethod.InputMethodManager
|
|
@@ -79,6 +83,17 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
79
83
|
val label: String
|
|
80
84
|
)
|
|
81
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
|
+
|
|
82
97
|
data class LinkHit(
|
|
83
98
|
val href: String,
|
|
84
99
|
val text: String
|
|
@@ -107,6 +122,27 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
107
122
|
val end: Int
|
|
108
123
|
)
|
|
109
124
|
|
|
125
|
+
private data class NativeTextMutation(
|
|
126
|
+
val scalarFrom: Int,
|
|
127
|
+
val scalarTo: Int,
|
|
128
|
+
val replacementText: 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
|
|
144
|
+
)
|
|
145
|
+
|
|
110
146
|
/**
|
|
111
147
|
* Listener interface for editor events, parallel to iOS's EditorTextViewDelegate.
|
|
112
148
|
*/
|
|
@@ -129,6 +165,13 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
129
165
|
* focus, text selection, and copy capability.
|
|
130
166
|
*/
|
|
131
167
|
var isEditable: Boolean = true
|
|
168
|
+
set(value) {
|
|
169
|
+
if (field == value) return
|
|
170
|
+
if (!value) {
|
|
171
|
+
discardTransientNativeInputForReadOnly()
|
|
172
|
+
}
|
|
173
|
+
field = value
|
|
174
|
+
}
|
|
132
175
|
|
|
133
176
|
/**
|
|
134
177
|
* Guard flag to prevent re-entrant input interception while we're
|
|
@@ -184,12 +227,25 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
184
227
|
var reconciliationCount: Int = 0
|
|
185
228
|
private set
|
|
186
229
|
|
|
187
|
-
private var
|
|
188
|
-
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
|
|
189
244
|
private var explicitSelectedImageRange: ImageSelectionRange? = null
|
|
190
245
|
private var lastRenderAppliedPatchForTesting: Boolean = false
|
|
191
246
|
internal var captureApplyUpdateTraceForTesting: Boolean = false
|
|
192
247
|
private var lastApplyUpdateTraceForTesting: ApplyUpdateTrace? = null
|
|
248
|
+
private val imeTraceForTesting = java.util.ArrayDeque<String>()
|
|
193
249
|
private var currentRenderBlocksJson: org.json.JSONArray? = null
|
|
194
250
|
private var renderAppearanceRevision: Long = 1L
|
|
195
251
|
private var lastAppliedRenderAppearanceRevision: Long = 0L
|
|
@@ -197,10 +253,32 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
197
253
|
internal var onDeleteBackwardAtSelectionScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
198
254
|
internal var onInsertTextInRustForTesting: ((String, Int) -> Unit)? = null
|
|
199
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
|
|
200
262
|
|
|
201
263
|
fun lastRenderAppliedPatch(): Boolean = lastRenderAppliedPatchForTesting
|
|
202
264
|
fun lastApplyUpdateTrace(): ApplyUpdateTrace? = lastApplyUpdateTraceForTesting
|
|
203
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
|
+
|
|
204
282
|
init {
|
|
205
283
|
// Configure for rich text editing.
|
|
206
284
|
inputType = resolvedInputType()
|
|
@@ -275,22 +353,23 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
275
353
|
|
|
276
354
|
val currentStart = selectionStart
|
|
277
355
|
val currentEnd = selectionEnd
|
|
356
|
+
val authorizedSelection = authorizedSelectionForTransientInputRestore(
|
|
357
|
+
currentStart,
|
|
358
|
+
currentEnd
|
|
359
|
+
)
|
|
360
|
+
discardTransientInputAndRestoreAuthorizedTextForEditor()
|
|
278
361
|
setRawInputType(nextInputType)
|
|
279
362
|
|
|
280
363
|
val editable = text
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
currentEnd <= editable.length
|
|
287
|
-
) {
|
|
288
|
-
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
|
+
)
|
|
289
369
|
}
|
|
290
370
|
|
|
291
371
|
if (hasFocus()) {
|
|
292
|
-
|
|
293
|
-
imm?.restartInput(this)
|
|
372
|
+
restartInputForEditor()
|
|
294
373
|
}
|
|
295
374
|
}
|
|
296
375
|
|
|
@@ -338,16 +417,72 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
338
417
|
*/
|
|
339
418
|
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
|
340
419
|
val baseConnection = super.onCreateInputConnection(outAttrs) ?: return null
|
|
341
|
-
|
|
420
|
+
val generation = nextInputConnectionGenerationForEditor()
|
|
421
|
+
return EditorInputConnection(this, baseConnection, editorId, generation).also {
|
|
422
|
+
activeInputConnection = it
|
|
423
|
+
}
|
|
342
424
|
}
|
|
343
425
|
|
|
344
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
|
+
}
|
|
345
433
|
if (handleHardwareKeyEvent(event)) {
|
|
346
434
|
return true
|
|
347
435
|
}
|
|
436
|
+
if (handlePrintableHardwareKeyEvent(event) { super.dispatchKeyEvent(event) }) {
|
|
437
|
+
return true
|
|
438
|
+
}
|
|
348
439
|
return super.dispatchKeyEvent(event)
|
|
349
440
|
}
|
|
350
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
|
+
|
|
351
486
|
override fun onDraw(canvas: android.graphics.Canvas) {
|
|
352
487
|
super.onDraw(canvas)
|
|
353
488
|
|
|
@@ -484,7 +619,15 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
484
619
|
* @param id The editor ID from `editor_create()`.
|
|
485
620
|
* @param initialHTML Optional HTML to set as initial content.
|
|
486
621
|
*/
|
|
487
|
-
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
|
+
}
|
|
488
631
|
editorId = id
|
|
489
632
|
|
|
490
633
|
if (!initialHTML.isNullOrEmpty()) {
|
|
@@ -494,7 +637,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
494
637
|
} else {
|
|
495
638
|
// Pull current state from Rust (content may already be loaded via bridge).
|
|
496
639
|
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
497
|
-
applyUpdateJSON(stateJSON)
|
|
640
|
+
applyUpdateJSON(stateJSON, notifyListener = notifyListener)
|
|
498
641
|
}
|
|
499
642
|
}
|
|
500
643
|
|
|
@@ -502,6 +645,9 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
502
645
|
* Unbind from the current editor instance.
|
|
503
646
|
*/
|
|
504
647
|
fun unbindEditor() {
|
|
648
|
+
if (editorId != 0L) {
|
|
649
|
+
discardTransientNativeInputForEditorRebind()
|
|
650
|
+
}
|
|
505
651
|
editorId = 0
|
|
506
652
|
}
|
|
507
653
|
|
|
@@ -522,7 +668,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
522
668
|
renderAppearanceRevision += 1L
|
|
523
669
|
setBackgroundColor(theme?.backgroundColor ?: baseBackgroundColor)
|
|
524
670
|
applyContentInsets(theme?.contentInsets)
|
|
525
|
-
if (
|
|
671
|
+
if (hasLiveEditor()) {
|
|
526
672
|
val previousScrollX = scrollX
|
|
527
673
|
val previousScrollY = scrollY
|
|
528
674
|
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
@@ -677,17 +823,18 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
677
823
|
* Called by [EditorInputConnection.commitText]. Routes the text through
|
|
678
824
|
* the Rust editor instead of directly inserting into the EditText.
|
|
679
825
|
*/
|
|
680
|
-
fun handleTextCommit(text: String) {
|
|
826
|
+
fun handleTextCommit(text: String, newCursorPosition: Int = 1) {
|
|
681
827
|
if (!isEditable) return
|
|
682
828
|
if (isApplyingRustState) return
|
|
829
|
+
val selectionRange = normalizedUtf16SelectionRange() ?: return
|
|
683
830
|
if (editorId == 0L) {
|
|
684
831
|
// No Rust editor bound — fall through to direct editing (dev mode).
|
|
685
832
|
val editable = this.text ?: return
|
|
686
|
-
val start =
|
|
687
|
-
val end = selectionEnd
|
|
833
|
+
val (start, end) = selectionRange
|
|
688
834
|
editable.replace(start, end, text)
|
|
689
835
|
return
|
|
690
836
|
}
|
|
837
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
691
838
|
|
|
692
839
|
// Handle Enter/Return as a block split operation.
|
|
693
840
|
if (text == "\n") {
|
|
@@ -696,18 +843,19 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
696
843
|
}
|
|
697
844
|
|
|
698
845
|
val currentText = this.text?.toString() ?: ""
|
|
699
|
-
val
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
+
)
|
|
711
859
|
}
|
|
712
860
|
|
|
713
861
|
internal fun runWithTransientInputMutationGuard(block: () -> Boolean): Boolean {
|
|
@@ -720,31 +868,497 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
720
868
|
}
|
|
721
869
|
}
|
|
722
870
|
|
|
723
|
-
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
|
+
) {
|
|
724
1122
|
if (!isEditable) return
|
|
725
1123
|
if (isApplyingRustState) return
|
|
726
|
-
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
|
+
}
|
|
727
1151
|
|
|
728
1152
|
if (text == "\n") {
|
|
729
|
-
|
|
1153
|
+
if (scalarStart != scalarEnd) {
|
|
1154
|
+
deleteAndSplitInRust(scalarStart, scalarEnd)
|
|
1155
|
+
} else {
|
|
1156
|
+
splitBlockInRust(scalarStart)
|
|
1157
|
+
}
|
|
730
1158
|
return
|
|
731
1159
|
}
|
|
732
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
|
+
|
|
733
1184
|
val authorizedText = lastAuthorizedText
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
+
|
|
738
1225
|
val scalarStart = PositionBridge.utf16ToScalar(startUtf16, authorizedText)
|
|
739
|
-
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
|
+
}
|
|
740
1234
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
|
745
1333
|
}
|
|
746
1334
|
}
|
|
747
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
|
+
|
|
748
1362
|
// ── Input Handling: Deletion ────────────────────────────────────────
|
|
749
1363
|
|
|
750
1364
|
/**
|
|
@@ -758,17 +1372,32 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
758
1372
|
fun handleDelete(beforeLength: Int, afterLength: Int) {
|
|
759
1373
|
if (!isEditable) return
|
|
760
1374
|
if (isApplyingRustState) return
|
|
1375
|
+
val selectionRange = normalizedUtf16SelectionRange()
|
|
761
1376
|
if (editorId == 0L) {
|
|
762
1377
|
// Dev mode: direct editing.
|
|
763
1378
|
val editable = this.text ?: return
|
|
764
|
-
val
|
|
765
|
-
val delStart
|
|
766
|
-
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
|
+
}
|
|
767
1389
|
editable.delete(delStart, delEnd)
|
|
768
1390
|
return
|
|
769
1391
|
}
|
|
1392
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
770
1393
|
|
|
771
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
|
+
}
|
|
772
1401
|
val cursor = selectionStart
|
|
773
1402
|
if (beforeLength > 0 &&
|
|
774
1403
|
afterLength == 0 &&
|
|
@@ -779,8 +1408,13 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
779
1408
|
deleteBackwardAtSelectionScalarInRust(scalarCursor, scalarCursor)
|
|
780
1409
|
return
|
|
781
1410
|
}
|
|
782
|
-
val
|
|
783
|
-
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
|
+
)
|
|
784
1418
|
|
|
785
1419
|
val scalarStart = PositionBridge.utf16ToScalar(delStart, currentText)
|
|
786
1420
|
val scalarEnd = PositionBridge.utf16ToScalar(delEnd, currentText)
|
|
@@ -801,11 +1435,11 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
801
1435
|
fun handleBackspace() {
|
|
802
1436
|
if (!isEditable) return
|
|
803
1437
|
if (isApplyingRustState) return
|
|
1438
|
+
val selectionRange = normalizedUtf16SelectionRange() ?: return
|
|
804
1439
|
if (editorId == 0L) {
|
|
805
1440
|
// Dev mode: direct editing.
|
|
806
1441
|
val editable = this.text ?: return
|
|
807
|
-
val start =
|
|
808
|
-
val end = selectionEnd
|
|
1442
|
+
val (start, end) = selectionRange
|
|
809
1443
|
if (start != end) {
|
|
810
1444
|
editable.delete(start, end)
|
|
811
1445
|
} else if (start > 0) {
|
|
@@ -816,15 +1450,14 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
816
1450
|
}
|
|
817
1451
|
return
|
|
818
1452
|
}
|
|
1453
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
819
1454
|
|
|
820
1455
|
val currentText = text?.toString() ?: ""
|
|
821
|
-
val start =
|
|
822
|
-
val end = selectionEnd
|
|
1456
|
+
val (start, end) = selectionRange
|
|
823
1457
|
|
|
824
1458
|
if (start != end) {
|
|
825
1459
|
// Range selection: delete the range.
|
|
826
|
-
val scalarStart =
|
|
827
|
-
val scalarEnd = PositionBridge.utf16ToScalar(end, currentText)
|
|
1460
|
+
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
828
1461
|
deleteRangeInRust(scalarStart, scalarEnd)
|
|
829
1462
|
} else if (start > 0) {
|
|
830
1463
|
if (currentText.getOrNull(start - 1) == EMPTY_BLOCK_PLACEHOLDER) {
|
|
@@ -851,6 +1484,57 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
851
1484
|
}
|
|
852
1485
|
}
|
|
853
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
|
+
|
|
854
1538
|
// ── Input Handling: Return Key ──────────────────────────────────────
|
|
855
1539
|
|
|
856
1540
|
/**
|
|
@@ -861,8 +1545,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
861
1545
|
if (isApplyingRustState) return
|
|
862
1546
|
|
|
863
1547
|
val currentText = text?.toString() ?: ""
|
|
864
|
-
val start =
|
|
865
|
-
val end = selectionEnd
|
|
1548
|
+
val (start, end) = normalizedUtf16SelectionRange() ?: return
|
|
866
1549
|
|
|
867
1550
|
if (editorId == 0L) {
|
|
868
1551
|
// Dev mode: insert newline directly.
|
|
@@ -870,15 +1553,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
870
1553
|
editable.replace(start, end, "\n")
|
|
871
1554
|
return
|
|
872
1555
|
}
|
|
1556
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
873
1557
|
|
|
874
1558
|
if (start != end) {
|
|
875
1559
|
// Range selection: atomic delete-and-split via Rust.
|
|
876
|
-
val scalarStart =
|
|
877
|
-
|
|
878
|
-
val updateJSON = editorDeleteAndSplitScalar(
|
|
879
|
-
editorId.toULong(), scalarStart.toUInt(), scalarEnd.toUInt()
|
|
880
|
-
)
|
|
881
|
-
applyUpdateJSON(updateJSON)
|
|
1560
|
+
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
1561
|
+
deleteAndSplitInRust(scalarStart, scalarEnd)
|
|
882
1562
|
} else {
|
|
883
1563
|
val scalarPos = PositionBridge.utf16ToScalar(start, currentText)
|
|
884
1564
|
splitBlockInRust(scalarPos)
|
|
@@ -899,6 +1579,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
899
1579
|
editable.replace(start, end, "\n")
|
|
900
1580
|
return
|
|
901
1581
|
}
|
|
1582
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
902
1583
|
|
|
903
1584
|
val selection = currentScalarSelection() ?: return
|
|
904
1585
|
val updateJSON = editorInsertNodeAtSelectionScalar(
|
|
@@ -916,7 +1597,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
916
1597
|
fun handleTab(shiftPressed: Boolean): Boolean {
|
|
917
1598
|
if (!isEditable) return false
|
|
918
1599
|
if (isApplyingRustState) return false
|
|
919
|
-
if (
|
|
1600
|
+
if (!hasLiveEditor()) return false
|
|
920
1601
|
if (!isSelectionInsideList()) return false
|
|
921
1602
|
val selection = currentScalarSelection() ?: return false
|
|
922
1603
|
|
|
@@ -944,6 +1625,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
944
1625
|
handleBackspace()
|
|
945
1626
|
true
|
|
946
1627
|
}
|
|
1628
|
+
KeyEvent.KEYCODE_FORWARD_DEL -> {
|
|
1629
|
+
handleForwardDelete()
|
|
1630
|
+
true
|
|
1631
|
+
}
|
|
947
1632
|
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> {
|
|
948
1633
|
if (shiftPressed) {
|
|
949
1634
|
handleHardBreak()
|
|
@@ -957,28 +1642,55 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
957
1642
|
}
|
|
958
1643
|
}
|
|
959
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
|
+
|
|
960
1677
|
fun handleHardwareKeyEvent(event: KeyEvent?): Boolean {
|
|
961
1678
|
if (event == null || !isEditable || isApplyingRustState) return false
|
|
962
1679
|
|
|
963
1680
|
return when (event.action) {
|
|
964
1681
|
KeyEvent.ACTION_DOWN -> {
|
|
965
|
-
|
|
966
|
-
KeyEvent.KEYCODE_DEL,
|
|
967
|
-
KeyEvent.KEYCODE_ENTER,
|
|
968
|
-
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
|
969
|
-
KeyEvent.KEYCODE_TAB -> true
|
|
970
|
-
else -> false
|
|
971
|
-
}
|
|
972
|
-
if (!supported) return false
|
|
1682
|
+
if (!isSupportedHardwareMutationKey(event.keyCode)) return false
|
|
973
1683
|
|
|
974
|
-
|
|
975
|
-
|
|
1684
|
+
val signature = hardwareKeyEventSignature(event)
|
|
1685
|
+
if (
|
|
1686
|
+
lastHandledHardwareKeySignature == signature ||
|
|
1687
|
+
didRecentlyHandleHardwareKeyDown(signature)
|
|
1688
|
+
) {
|
|
976
1689
|
return true
|
|
977
1690
|
}
|
|
978
1691
|
|
|
979
1692
|
if (handleHardwareKeyDown(event.keyCode, event.isShiftPressed)) {
|
|
980
|
-
|
|
981
|
-
lastHandledHardwareKeyDownTime = event.downTime
|
|
1693
|
+
markHandledHardwareKeyDown(signature)
|
|
982
1694
|
true
|
|
983
1695
|
} else {
|
|
984
1696
|
false
|
|
@@ -986,10 +1698,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
986
1698
|
}
|
|
987
1699
|
|
|
988
1700
|
KeyEvent.ACTION_UP -> {
|
|
989
|
-
if (
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1701
|
+
if (lastHandledHardwareKeySignature?.let {
|
|
1702
|
+
it.keyCode == event.keyCode && it.downTime == event.downTime
|
|
1703
|
+
} == true) {
|
|
1704
|
+
lastHandledHardwareKeySignature = null
|
|
993
1705
|
true
|
|
994
1706
|
} else {
|
|
995
1707
|
false
|
|
@@ -1000,8 +1712,119 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1000
1712
|
}
|
|
1001
1713
|
}
|
|
1002
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
|
+
|
|
1003
1826
|
fun performToolbarToggleMark(markName: String) {
|
|
1004
|
-
if (!isEditable || isApplyingRustState ||
|
|
1827
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1005
1828
|
val selection = currentScalarSelection() ?: return
|
|
1006
1829
|
val updateJSON = editorToggleMarkAtSelectionScalar(
|
|
1007
1830
|
editorId.toULong(),
|
|
@@ -1013,7 +1836,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1013
1836
|
}
|
|
1014
1837
|
|
|
1015
1838
|
fun performToolbarToggleList(listType: String, isActive: Boolean) {
|
|
1016
|
-
if (!isEditable || isApplyingRustState ||
|
|
1839
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1017
1840
|
val selection = currentScalarSelection() ?: return
|
|
1018
1841
|
val updateJSON = if (isActive) {
|
|
1019
1842
|
editorUnwrapFromListAtSelectionScalar(
|
|
@@ -1033,7 +1856,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1033
1856
|
}
|
|
1034
1857
|
|
|
1035
1858
|
fun performToolbarToggleBlockquote() {
|
|
1036
|
-
if (!isEditable || isApplyingRustState ||
|
|
1859
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1037
1860
|
val selection = currentScalarSelection() ?: return
|
|
1038
1861
|
val updateJSON = editorToggleBlockquoteAtSelectionScalar(
|
|
1039
1862
|
editorId.toULong(),
|
|
@@ -1044,7 +1867,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1044
1867
|
}
|
|
1045
1868
|
|
|
1046
1869
|
fun performToolbarToggleHeading(level: Int) {
|
|
1047
|
-
if (!isEditable || isApplyingRustState ||
|
|
1870
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1048
1871
|
if (level !in 1..6) return
|
|
1049
1872
|
val selection = currentScalarSelection() ?: return
|
|
1050
1873
|
val updateJSON = editorToggleHeadingAtSelectionScalar(
|
|
@@ -1057,7 +1880,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1057
1880
|
}
|
|
1058
1881
|
|
|
1059
1882
|
fun performToolbarIndentListItem() {
|
|
1060
|
-
if (!isEditable || isApplyingRustState ||
|
|
1883
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1061
1884
|
val selection = currentScalarSelection() ?: return
|
|
1062
1885
|
val updateJSON = editorIndentListItemAtSelectionScalar(
|
|
1063
1886
|
editorId.toULong(),
|
|
@@ -1068,7 +1891,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1068
1891
|
}
|
|
1069
1892
|
|
|
1070
1893
|
fun performToolbarOutdentListItem() {
|
|
1071
|
-
if (!isEditable || isApplyingRustState ||
|
|
1894
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1072
1895
|
val selection = currentScalarSelection() ?: return
|
|
1073
1896
|
val updateJSON = editorOutdentListItemAtSelectionScalar(
|
|
1074
1897
|
editorId.toULong(),
|
|
@@ -1079,7 +1902,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1079
1902
|
}
|
|
1080
1903
|
|
|
1081
1904
|
fun performToolbarInsertNode(nodeType: String) {
|
|
1082
|
-
if (!isEditable || isApplyingRustState ||
|
|
1905
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1083
1906
|
val selection = currentScalarSelection() ?: return
|
|
1084
1907
|
val updateJSON = editorInsertNodeAtSelectionScalar(
|
|
1085
1908
|
editorId.toULong(),
|
|
@@ -1091,12 +1914,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1091
1914
|
}
|
|
1092
1915
|
|
|
1093
1916
|
fun performToolbarUndo() {
|
|
1094
|
-
if (!isEditable || isApplyingRustState ||
|
|
1917
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1095
1918
|
applyUpdateJSON(editorUndo(editorId.toULong()))
|
|
1096
1919
|
}
|
|
1097
1920
|
|
|
1098
1921
|
fun performToolbarRedo() {
|
|
1099
|
-
if (!isEditable || isApplyingRustState ||
|
|
1922
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1100
1923
|
applyUpdateJSON(editorRedo(editorId.toULong()))
|
|
1101
1924
|
}
|
|
1102
1925
|
|
|
@@ -1109,32 +1932,52 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1109
1932
|
* falling back to plain text.
|
|
1110
1933
|
*/
|
|
1111
1934
|
override fun onTextContextMenuItem(id: Int): Boolean {
|
|
1112
|
-
if (!isEditable && id
|
|
1113
|
-
if (id == android.R.id.
|
|
1114
|
-
|
|
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)
|
|
1115
1942
|
return true
|
|
1116
1943
|
}
|
|
1117
1944
|
return super.onTextContextMenuItem(id)
|
|
1118
1945
|
}
|
|
1119
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
|
+
|
|
1120
1952
|
/**
|
|
1121
|
-
* Block accessibility-initiated text mutations (paste, set text) when not editable.
|
|
1953
|
+
* Block accessibility-initiated text mutations (paste, cut, set text) when not editable.
|
|
1122
1954
|
* Selection and copy actions remain available.
|
|
1123
1955
|
*/
|
|
1124
1956
|
override fun performAccessibilityAction(action: Int, arguments: android.os.Bundle?): Boolean {
|
|
1125
|
-
if (!isEditable && (
|
|
1126
|
-
|
|
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
|
+
) {
|
|
1127
1963
|
return false
|
|
1128
1964
|
}
|
|
1965
|
+
if (action == android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT) {
|
|
1966
|
+
return handleAccessibilitySetText(arguments)
|
|
1967
|
+
}
|
|
1129
1968
|
return super.performAccessibilityAction(action, arguments)
|
|
1130
1969
|
}
|
|
1131
1970
|
|
|
1132
|
-
private fun handlePaste() {
|
|
1971
|
+
private fun handlePaste(plainTextOnly: Boolean) {
|
|
1133
1972
|
if (editorId == 0L) {
|
|
1134
1973
|
// Dev mode: default paste behavior.
|
|
1135
|
-
super.onTextContextMenuItem(
|
|
1974
|
+
super.onTextContextMenuItem(
|
|
1975
|
+
if (plainTextOnly) android.R.id.pasteAsPlainText else android.R.id.paste
|
|
1976
|
+
)
|
|
1136
1977
|
return
|
|
1137
1978
|
}
|
|
1979
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
1980
|
+
if (!prepareForExternalEditorUpdate()) return
|
|
1138
1981
|
|
|
1139
1982
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
1140
1983
|
?: return
|
|
@@ -1145,18 +1988,69 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1145
1988
|
|
|
1146
1989
|
// Try HTML first for rich paste.
|
|
1147
1990
|
val htmlText = item.htmlText
|
|
1148
|
-
if (htmlText != null) {
|
|
1991
|
+
if (!plainTextOnly && htmlText != null) {
|
|
1149
1992
|
pasteHTML(htmlText)
|
|
1150
1993
|
return
|
|
1151
1994
|
}
|
|
1152
1995
|
|
|
1153
1996
|
// Fallback to plain text.
|
|
1154
|
-
val plainText = item.text?.toString()
|
|
1997
|
+
val plainText = item.text?.toString() ?: item.coerceToText(context)?.toString()
|
|
1155
1998
|
if (plainText != null) {
|
|
1156
1999
|
pastePlainText(plainText)
|
|
1157
2000
|
}
|
|
1158
2001
|
}
|
|
1159
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
|
+
|
|
1160
2054
|
// ── Selection Change ────────────────────────────────────────────────
|
|
1161
2055
|
|
|
1162
2056
|
/**
|
|
@@ -1175,23 +2069,35 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1175
2069
|
ensureSelectionVisible()
|
|
1176
2070
|
onSelectionOrContentMayChange?.invoke()
|
|
1177
2071
|
|
|
1178
|
-
|
|
2072
|
+
syncCurrentSelectionToRust()
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
private fun syncCurrentSelectionToRust() {
|
|
2076
|
+
if (!hasLiveEditor()) return
|
|
1179
2077
|
|
|
1180
2078
|
val currentText = text?.toString() ?: ""
|
|
1181
2079
|
if (currentText != lastAuthorizedText) return
|
|
1182
|
-
val scalarAnchor =
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
scalarAnchor
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
+
)
|
|
1191
2096
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
+
}
|
|
1195
2101
|
editorListener?.onSelectionChanged(docAnchor, docHead)
|
|
1196
2102
|
}
|
|
1197
2103
|
|
|
@@ -1201,6 +2107,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1201
2107
|
* Insert text at a scalar position via the Rust editor.
|
|
1202
2108
|
*/
|
|
1203
2109
|
private fun insertTextInRust(text: String, atScalarPos: Int) {
|
|
2110
|
+
if (!hasLiveEditor()) return
|
|
1204
2111
|
onInsertTextInRustForTesting?.let { callback ->
|
|
1205
2112
|
callback(text, atScalarPos)
|
|
1206
2113
|
return
|
|
@@ -1210,6 +2117,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1210
2117
|
}
|
|
1211
2118
|
|
|
1212
2119
|
private fun replaceTextRangeInRust(scalarFrom: Int, scalarTo: Int, text: String) {
|
|
2120
|
+
if (!hasLiveEditor()) return
|
|
1213
2121
|
onReplaceTextInRustForTesting?.let { callback ->
|
|
1214
2122
|
callback(scalarFrom, scalarTo, text)
|
|
1215
2123
|
return
|
|
@@ -1223,6 +2131,322 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1223
2131
|
applyUpdateJSON(updateJSON)
|
|
1224
2132
|
}
|
|
1225
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
|
+
|
|
2237
|
+
private fun nativeTextMutationFromAuthorizedDiff(currentText: String): NativeTextMutation? {
|
|
2238
|
+
val authorizedText = lastAuthorizedText
|
|
2239
|
+
if (currentText == authorizedText) return null
|
|
2240
|
+
|
|
2241
|
+
var prefix = 0
|
|
2242
|
+
val sharedLength = minOf(authorizedText.length, currentText.length)
|
|
2243
|
+
while (
|
|
2244
|
+
prefix < sharedLength &&
|
|
2245
|
+
authorizedText[prefix] == currentText[prefix]
|
|
2246
|
+
) {
|
|
2247
|
+
prefix++
|
|
2248
|
+
}
|
|
2249
|
+
prefix = minOf(
|
|
2250
|
+
PositionBridge.snapToScalarBoundary(prefix, authorizedText, biasForward = false),
|
|
2251
|
+
PositionBridge.snapToScalarBoundary(prefix, currentText, biasForward = false)
|
|
2252
|
+
)
|
|
2253
|
+
|
|
2254
|
+
var authorizedEnd = authorizedText.length
|
|
2255
|
+
var currentEnd = currentText.length
|
|
2256
|
+
while (
|
|
2257
|
+
authorizedEnd > prefix &&
|
|
2258
|
+
currentEnd > prefix &&
|
|
2259
|
+
authorizedText[authorizedEnd - 1] == currentText[currentEnd - 1]
|
|
2260
|
+
) {
|
|
2261
|
+
authorizedEnd--
|
|
2262
|
+
currentEnd--
|
|
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
|
+
)
|
|
2274
|
+
|
|
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) }
|
|
2284
|
+
return NativeTextMutation(
|
|
2285
|
+
scalarFrom = PositionBridge.utf16ToScalar(prefix, authorizedText),
|
|
2286
|
+
scalarTo = PositionBridge.utf16ToScalar(authorizedEnd, authorizedText),
|
|
2287
|
+
replacementText = replacementText,
|
|
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
|
|
2359
|
+
)
|
|
2360
|
+
}
|
|
2361
|
+
|
|
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
|
+
}
|
|
2376
|
+
|
|
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
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
private fun commitNativeTextMutation(mutation: NativeTextMutation) {
|
|
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()
|
|
2420
|
+
|
|
2421
|
+
recordImeTraceForTesting(
|
|
2422
|
+
"nativeMutationApply",
|
|
2423
|
+
"range=${mutation.scalarFrom}..${mutation.scalarTo} replacementLength=${mutation.replacementText.length} restartInput=$shouldRestartInput"
|
|
2424
|
+
)
|
|
2425
|
+
if (mutation.replacementText.isEmpty()) {
|
|
2426
|
+
deleteRangeInRust(mutation.scalarFrom, mutation.scalarTo)
|
|
2427
|
+
} else {
|
|
2428
|
+
insertPlainTextRangeInRust(
|
|
2429
|
+
mutation.scalarFrom,
|
|
2430
|
+
mutation.scalarTo,
|
|
2431
|
+
mutation.replacementText
|
|
2432
|
+
)
|
|
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))
|
|
2448
|
+
}
|
|
2449
|
+
|
|
1226
2450
|
/**
|
|
1227
2451
|
* Delete a scalar range via the Rust editor.
|
|
1228
2452
|
*
|
|
@@ -1230,6 +2454,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1230
2454
|
* @param scalarTo End scalar offset (exclusive).
|
|
1231
2455
|
*/
|
|
1232
2456
|
private fun deleteRangeInRust(scalarFrom: Int, scalarTo: Int) {
|
|
2457
|
+
if (!hasLiveEditor()) return
|
|
1233
2458
|
if (scalarFrom >= scalarTo) return
|
|
1234
2459
|
onDeleteRangeInRustForTesting?.let { callback ->
|
|
1235
2460
|
callback(scalarFrom, scalarTo)
|
|
@@ -1240,6 +2465,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1240
2465
|
}
|
|
1241
2466
|
|
|
1242
2467
|
private fun deleteBackwardAtSelectionScalarInRust(scalarAnchor: Int, scalarHead: Int) {
|
|
2468
|
+
if (!hasLiveEditor()) return
|
|
1243
2469
|
onDeleteBackwardAtSelectionScalarInRustForTesting?.let { callback ->
|
|
1244
2470
|
callback(scalarAnchor, scalarHead)
|
|
1245
2471
|
return
|
|
@@ -1256,16 +2482,84 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1256
2482
|
* Split a block at a scalar position via the Rust editor.
|
|
1257
2483
|
*/
|
|
1258
2484
|
private fun splitBlockInRust(atScalarPos: Int) {
|
|
2485
|
+
if (!hasLiveEditor()) return
|
|
1259
2486
|
val updateJSON = editorSplitBlockScalar(editorId.toULong(), atScalarPos.toUInt())
|
|
1260
2487
|
applyUpdateJSON(updateJSON)
|
|
1261
2488
|
}
|
|
1262
2489
|
|
|
1263
|
-
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>? {
|
|
1264
2505
|
val currentText = text?.toString() ?: return null
|
|
1265
|
-
return
|
|
1266
|
-
|
|
1267
|
-
|
|
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>? {
|
|
2519
|
+
val currentText = text?.toString() ?: return null
|
|
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
|
|
1268
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)
|
|
1269
2563
|
}
|
|
1270
2564
|
|
|
1271
2565
|
fun selectedImageGeometry(): SelectedImageGeometry? {
|
|
@@ -1284,7 +2578,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1284
2578
|
val textLayout = layout ?: return null
|
|
1285
2579
|
val currentText = text?.toString() ?: return null
|
|
1286
2580
|
val scalarPos = PositionBridge.utf16ToScalar(spanStart, currentText)
|
|
1287
|
-
val docPos = if (
|
|
2581
|
+
val docPos = if (hasLiveEditor()) {
|
|
1288
2582
|
editorScalarToDoc(editorId.toULong(), scalarPos.toUInt()).toInt()
|
|
1289
2583
|
} else {
|
|
1290
2584
|
0
|
|
@@ -1298,7 +2592,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1298
2592
|
}
|
|
1299
2593
|
|
|
1300
2594
|
fun resizeImageAtDocPos(docPos: Int, widthPx: Float, heightPx: Float) {
|
|
1301
|
-
if (
|
|
2595
|
+
if (!hasLiveEditor()) return
|
|
1302
2596
|
val density = resources.displayMetrics.density
|
|
1303
2597
|
val widthDp = maxOf(48, (widthPx / density).roundToInt())
|
|
1304
2598
|
val heightDp = maxOf(48, (heightPx / density).roundToInt())
|
|
@@ -1312,7 +2606,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1312
2606
|
}
|
|
1313
2607
|
|
|
1314
2608
|
private fun isSelectionInsideList(): Boolean {
|
|
1315
|
-
if (
|
|
2609
|
+
if (!hasLiveEditor()) return false
|
|
1316
2610
|
|
|
1317
2611
|
return try {
|
|
1318
2612
|
val state = org.json.JSONObject(editorGetCurrentState(editorId.toULong()))
|
|
@@ -1328,6 +2622,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1328
2622
|
* Paste HTML content through Rust.
|
|
1329
2623
|
*/
|
|
1330
2624
|
private fun pasteHTML(html: String) {
|
|
2625
|
+
if (!hasLiveEditor()) return
|
|
2626
|
+
syncCurrentSelectionToRust()
|
|
2627
|
+
onInsertContentHtmlInRustForTesting?.let { callback ->
|
|
2628
|
+
callback(html)
|
|
2629
|
+
return
|
|
2630
|
+
}
|
|
1331
2631
|
val updateJSON = editorInsertContentHtml(editorId.toULong(), html)
|
|
1332
2632
|
applyUpdateJSON(updateJSON)
|
|
1333
2633
|
}
|
|
@@ -1336,9 +2636,8 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1336
2636
|
* Paste plain text through Rust.
|
|
1337
2637
|
*/
|
|
1338
2638
|
private fun pastePlainText(text: String) {
|
|
1339
|
-
val
|
|
1340
|
-
|
|
1341
|
-
insertTextInRust(text, scalarPos)
|
|
2639
|
+
val (scalarStart, scalarEnd) = currentScalarSelection() ?: return
|
|
2640
|
+
insertPlainTextRangeInRust(scalarStart, scalarEnd, text)
|
|
1342
2641
|
}
|
|
1343
2642
|
|
|
1344
2643
|
// ── Applying Rust State ─────────────────────────────────────────────
|
|
@@ -1409,6 +2708,8 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1409
2708
|
replaceRange: RenderReplaceRange? = null,
|
|
1410
2709
|
usedPatch: Boolean
|
|
1411
2710
|
) {
|
|
2711
|
+
val hadCompositionTracking = hasCompositionTrackingForEditor()
|
|
2712
|
+
var shouldRestartInput = false
|
|
1412
2713
|
isApplyingRustState = true
|
|
1413
2714
|
beginBatchEdit()
|
|
1414
2715
|
try {
|
|
@@ -1418,11 +2719,22 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1418
2719
|
setText(spannable)
|
|
1419
2720
|
}
|
|
1420
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
|
+
}
|
|
1421
2731
|
lastRenderAppliedPatchForTesting = usedPatch
|
|
2732
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1422
2733
|
} finally {
|
|
1423
2734
|
endBatchEdit()
|
|
1424
2735
|
isApplyingRustState = false
|
|
1425
2736
|
}
|
|
2737
|
+
restartInputAfterCompositionInvalidationIfNeeded(shouldRestartInput)
|
|
1426
2738
|
}
|
|
1427
2739
|
|
|
1428
2740
|
private fun buildPatchedSpannable(patch: ParsedRenderPatch): android.text.SpannableStringBuilder =
|
|
@@ -1623,6 +2935,8 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1623
2935
|
if (shouldSkipRender) {
|
|
1624
2936
|
lastRenderAppliedPatchForTesting = false
|
|
1625
2937
|
currentRenderBlocksJson = resolvedRenderBlocks?.let(::cloneJsonArray)
|
|
2938
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
2939
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1626
2940
|
buildRenderNanos = 0L
|
|
1627
2941
|
applyRenderNanos = 0L
|
|
1628
2942
|
} else {
|
|
@@ -1843,7 +3157,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1843
3157
|
|
|
1844
3158
|
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
|
|
1845
3159
|
super.onFocusChanged(focused, direction, previouslyFocusedRect)
|
|
1846
|
-
if (
|
|
3160
|
+
if (focused) {
|
|
3161
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3162
|
+
} else {
|
|
3163
|
+
beginNativeTextMutationAfterBlurWindow()
|
|
1847
3164
|
clearExplicitSelectedImageRange()
|
|
1848
3165
|
}
|
|
1849
3166
|
}
|
|
@@ -1950,6 +3267,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1950
3267
|
*/
|
|
1951
3268
|
private fun applySelectionFromJSON(selection: org.json.JSONObject) {
|
|
1952
3269
|
val type = selection.optString("type", "") ?: return
|
|
3270
|
+
if (isEditorDestroyedForInput()) return
|
|
1953
3271
|
|
|
1954
3272
|
isApplyingRustState = true
|
|
1955
3273
|
try {
|
|
@@ -1961,12 +3279,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1961
3279
|
// Convert doc positions to scalar offsets.
|
|
1962
3280
|
val scalarAnchor = editorDocToScalar(editorId.toULong(), docAnchor.toUInt()).toInt()
|
|
1963
3281
|
val scalarHead = editorDocToScalar(editorId.toULong(), docHead.toUInt()).toInt()
|
|
1964
|
-
val
|
|
1965
|
-
val
|
|
3282
|
+
val anchorUtf16 = PositionBridge.scalarToUtf16(scalarAnchor, currentText)
|
|
3283
|
+
val headUtf16 = PositionBridge.scalarToUtf16(scalarHead, currentText)
|
|
1966
3284
|
val len = text?.length ?: 0
|
|
1967
3285
|
setSelection(
|
|
1968
|
-
|
|
1969
|
-
|
|
3286
|
+
anchorUtf16.coerceIn(0, len),
|
|
3287
|
+
headUtf16.coerceIn(0, len)
|
|
1970
3288
|
)
|
|
1971
3289
|
}
|
|
1972
3290
|
"node" -> {
|
|
@@ -2013,11 +3331,17 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2013
3331
|
|
|
2014
3332
|
override fun afterTextChanged(s: Editable?) {
|
|
2015
3333
|
if (isApplyingRustState) return
|
|
2016
|
-
if (
|
|
3334
|
+
if (!hasLiveEditor()) return
|
|
2017
3335
|
|
|
2018
3336
|
val currentText = s?.toString() ?: ""
|
|
2019
3337
|
if (currentText == lastAuthorizedText) return
|
|
2020
3338
|
|
|
3339
|
+
val mutation = nativeTextMutationFromAuthorizedDiff(currentText)
|
|
3340
|
+
if (mutation != null && shouldAdoptNativeTextMutation(mutation, allowAfterBlur = true)) {
|
|
3341
|
+
commitNativeTextMutation(mutation)
|
|
3342
|
+
return
|
|
3343
|
+
}
|
|
3344
|
+
|
|
2021
3345
|
// Text has diverged from Rust's authorized state.
|
|
2022
3346
|
reconciliationCount++
|
|
2023
3347
|
Log.w(
|
|
@@ -2039,6 +3363,9 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2039
3363
|
private const val DEFAULT_AUTO_CORRECT = true
|
|
2040
3364
|
private const val DEFAULT_KEYBOARD_TYPE = "default"
|
|
2041
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
|
|
2042
3369
|
private const val LOG_TAG = "NativeEditor"
|
|
2043
3370
|
}
|
|
2044
3371
|
}
|