@apollohg/react-native-prose-editor 0.5.16 → 0.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +2440 -275
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +783 -64
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1767 -81
- 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 +912 -89
- package/package.json +2 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
|
@@ -1,18 +1,33 @@
|
|
|
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.Build
|
|
10
|
+
import android.os.Handler
|
|
11
|
+
import android.os.Looper
|
|
12
|
+
import android.os.SystemClock
|
|
13
|
+
import android.provider.Settings
|
|
8
14
|
import android.text.Annotation
|
|
9
15
|
import android.text.Editable
|
|
10
16
|
import android.text.InputType
|
|
11
17
|
import android.text.Layout
|
|
18
|
+
import android.text.Selection
|
|
12
19
|
import android.text.Spanned
|
|
20
|
+
import android.text.SpannableStringBuilder
|
|
13
21
|
import android.text.StaticLayout
|
|
14
22
|
import android.text.TextPaint
|
|
15
23
|
import android.text.TextWatcher
|
|
24
|
+
import android.text.style.AbsoluteSizeSpan
|
|
25
|
+
import android.text.style.BackgroundColorSpan
|
|
26
|
+
import android.text.style.ForegroundColorSpan
|
|
27
|
+
import android.text.style.StrikethroughSpan
|
|
28
|
+
import android.text.style.StyleSpan
|
|
29
|
+
import android.text.style.TypefaceSpan
|
|
30
|
+
import android.text.style.UnderlineSpan
|
|
16
31
|
import android.util.AttributeSet
|
|
17
32
|
import android.util.Log
|
|
18
33
|
import android.util.TypedValue
|
|
@@ -70,6 +85,15 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
70
85
|
val totalNanos: Long
|
|
71
86
|
)
|
|
72
87
|
|
|
88
|
+
internal data class ImeInitialSurroundingText(
|
|
89
|
+
val text: String,
|
|
90
|
+
val selectionStart: Int,
|
|
91
|
+
val selectionEnd: Int,
|
|
92
|
+
val originalSelectionStart: Int,
|
|
93
|
+
val originalSelectionEnd: Int,
|
|
94
|
+
val removedPlaceholderCount: Int
|
|
95
|
+
)
|
|
96
|
+
|
|
73
97
|
data class SelectedImageGeometry(
|
|
74
98
|
val docPos: Int,
|
|
75
99
|
val rect: RectF
|
|
@@ -80,6 +104,17 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
80
104
|
val label: String
|
|
81
105
|
)
|
|
82
106
|
|
|
107
|
+
data class CommandPreparation(
|
|
108
|
+
val ready: Boolean,
|
|
109
|
+
val updateJSON: String?
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
private data class HardwareKeyEventSignature(
|
|
113
|
+
val keyCode: Int,
|
|
114
|
+
val downTime: Long,
|
|
115
|
+
val repeatCount: Int
|
|
116
|
+
)
|
|
117
|
+
|
|
83
118
|
data class LinkHit(
|
|
84
119
|
val href: String,
|
|
85
120
|
val text: String
|
|
@@ -112,9 +147,43 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
112
147
|
val scalarFrom: Int,
|
|
113
148
|
val scalarTo: Int,
|
|
114
149
|
val replacementText: String,
|
|
115
|
-
val resultingText: String
|
|
150
|
+
val resultingText: String,
|
|
151
|
+
val replacementStartUtf16: Int,
|
|
152
|
+
val replacementEndUtf16: Int,
|
|
153
|
+
val selectionScalarAnchor: Int?,
|
|
154
|
+
val selectionScalarHead: Int?
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
private data class NativeTextMutationAfterBlurWindow(
|
|
158
|
+
val editorId: Long,
|
|
159
|
+
val authorizedTextRevision: Long,
|
|
160
|
+
val deadlineMs: Long,
|
|
161
|
+
var didAdoptMutation: Boolean = false
|
|
116
162
|
)
|
|
117
163
|
|
|
164
|
+
private data class NativeTextMutationAdoptionSuppression(
|
|
165
|
+
val editorId: Long,
|
|
166
|
+
val authorizedTextRevision: Long
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
private interface TransientComposingTextStyleSpan
|
|
170
|
+
|
|
171
|
+
private class TransientComposingSizeSpan(sizePx: Int) :
|
|
172
|
+
AbsoluteSizeSpan(sizePx, false),
|
|
173
|
+
TransientComposingTextStyleSpan
|
|
174
|
+
|
|
175
|
+
private class TransientComposingColorSpan(color: Int) :
|
|
176
|
+
ForegroundColorSpan(color),
|
|
177
|
+
TransientComposingTextStyleSpan
|
|
178
|
+
|
|
179
|
+
private class TransientComposingTypefaceSpan(family: String) :
|
|
180
|
+
TypefaceSpan(family),
|
|
181
|
+
TransientComposingTextStyleSpan
|
|
182
|
+
|
|
183
|
+
private class TransientComposingStyleSpan(style: Int) :
|
|
184
|
+
StyleSpan(style),
|
|
185
|
+
TransientComposingTextStyleSpan
|
|
186
|
+
|
|
118
187
|
/**
|
|
119
188
|
* Listener interface for editor events, parallel to iOS's EditorTextViewDelegate.
|
|
120
189
|
*/
|
|
@@ -137,6 +206,13 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
137
206
|
* focus, text selection, and copy capability.
|
|
138
207
|
*/
|
|
139
208
|
var isEditable: Boolean = true
|
|
209
|
+
set(value) {
|
|
210
|
+
if (field == value) return
|
|
211
|
+
if (!value) {
|
|
212
|
+
discardTransientNativeInputForReadOnly()
|
|
213
|
+
}
|
|
214
|
+
field = value
|
|
215
|
+
}
|
|
140
216
|
|
|
141
217
|
/**
|
|
142
218
|
* Guard flag to prevent re-entrant input interception while we're
|
|
@@ -192,22 +268,110 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
192
268
|
var reconciliationCount: Int = 0
|
|
193
269
|
private set
|
|
194
270
|
|
|
195
|
-
private var
|
|
196
|
-
private var
|
|
271
|
+
private var lastHandledHardwareKeySignature: HardwareKeyEventSignature? = null
|
|
272
|
+
private var recentHandledHardwareKeyDownSignature: HardwareKeyEventSignature? = null
|
|
273
|
+
private var recentHandledHardwareKeyDownUptimeMs: Long = 0L
|
|
274
|
+
private var activeInputConnection: EditorInputConnection? = null
|
|
275
|
+
private var inputConnectionGeneration: Long = 0L
|
|
276
|
+
private var composingText: String? = null
|
|
277
|
+
private var composingReplacementStartUtf16: Int? = null
|
|
278
|
+
private var composingReplacementEndUtf16: Int? = null
|
|
279
|
+
private var composingReplacementAuthorizedTextRevision: Long? = null
|
|
280
|
+
private var didInvalidateCompositionReplacementRange = false
|
|
281
|
+
private var nativeTextMutationAfterBlurWindow: NativeTextMutationAfterBlurWindow? = null
|
|
282
|
+
private var nativeTextMutationAdoptionSuppression: NativeTextMutationAdoptionSuppression? = null
|
|
283
|
+
private var lastAuthorizedTextRevision: Long = 0L
|
|
284
|
+
private var lastAuthorizedRenderedText: CharSequence? = null
|
|
197
285
|
private var explicitSelectedImageRange: ImageSelectionRange? = null
|
|
198
286
|
private var lastRenderAppliedPatchForTesting: Boolean = false
|
|
199
287
|
internal var captureApplyUpdateTraceForTesting: Boolean = false
|
|
200
288
|
private var lastApplyUpdateTraceForTesting: ApplyUpdateTrace? = null
|
|
289
|
+
private val imeTraceForTesting = java.util.ArrayDeque<String>()
|
|
290
|
+
private var imeTraceSequence: Long = 0L
|
|
291
|
+
private var lastImeTraceUptimeMs: Long = 0L
|
|
201
292
|
private var currentRenderBlocksJson: org.json.JSONArray? = null
|
|
202
293
|
private var renderAppearanceRevision: Long = 1L
|
|
203
294
|
private var lastAppliedRenderAppearanceRevision: Long = 0L
|
|
295
|
+
private var pendingOptimisticRenderText: String? = null
|
|
296
|
+
private var deferredRustUpdateApplicationDepth: Int = 0
|
|
297
|
+
private var deferredRustUpdateJSON: String? = null
|
|
298
|
+
private var deferredRustUpdateGeneration: Long = 0L
|
|
299
|
+
private var lineBoundaryInputRefreshGeneration: Long = 0L
|
|
300
|
+
private var restartInputSelectionUpdateGeneration: Long = 0L
|
|
204
301
|
internal var onDeleteRangeInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
205
302
|
internal var onDeleteBackwardAtSelectionScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
206
303
|
internal var onInsertTextInRustForTesting: ((String, Int) -> Unit)? = null
|
|
207
304
|
internal var onReplaceTextInRustForTesting: ((Int, Int, String) -> Unit)? = null
|
|
305
|
+
internal var onSetSelectionScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
306
|
+
internal var onDeleteAndSplitScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
307
|
+
internal var onInsertContentHtmlInRustForTesting: ((String) -> Unit)? = null
|
|
308
|
+
internal var onInsertContentJsonAtSelectionScalarForTesting: ((Int, Int, String) -> Unit)? = null
|
|
309
|
+
internal var blockExternalEditorUpdatePreparationForTesting = false
|
|
310
|
+
internal var blockExternalEditorCommandPreparationForTesting = false
|
|
208
311
|
|
|
209
312
|
fun lastRenderAppliedPatch(): Boolean = lastRenderAppliedPatchForTesting
|
|
210
313
|
fun lastApplyUpdateTrace(): ApplyUpdateTrace? = lastApplyUpdateTraceForTesting
|
|
314
|
+
internal fun hasDeferredRustUpdateApplicationForTesting(): Boolean = deferredRustUpdateJSON != null
|
|
315
|
+
|
|
316
|
+
internal fun applyRustUpdateJSONForTesting(updateJSON: String) {
|
|
317
|
+
applyRustUpdateJSON(updateJSON)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
internal fun recordImeTraceForTesting(event: String, details: String = "") {
|
|
321
|
+
if (imeTraceForTesting.size >= IME_TRACE_LIMIT_FOR_TESTING) {
|
|
322
|
+
imeTraceForTesting.removeFirst()
|
|
323
|
+
}
|
|
324
|
+
imeTraceForTesting.addLast(
|
|
325
|
+
if (details.isEmpty()) event else "$event:$details"
|
|
326
|
+
)
|
|
327
|
+
if (Log.isLoggable(IME_TRACE_LOG_TAG, Log.VERBOSE)) {
|
|
328
|
+
val now = SystemClock.uptimeMillis()
|
|
329
|
+
val deltaMs = if (lastImeTraceUptimeMs == 0L) 0L else now - lastImeTraceUptimeMs
|
|
330
|
+
lastImeTraceUptimeMs = now
|
|
331
|
+
imeTraceSequence += 1L
|
|
332
|
+
val textLength = text?.length ?: -1
|
|
333
|
+
val selection = "${selectionStart}..${selectionEnd}"
|
|
334
|
+
val composingRange = "${composingReplacementStartUtf16 ?: -1}.." +
|
|
335
|
+
"${composingReplacementEndUtf16 ?: -1}"
|
|
336
|
+
val composingRevisionMatches =
|
|
337
|
+
composingReplacementAuthorizedTextRevision == lastAuthorizedTextRevision
|
|
338
|
+
val message = buildString {
|
|
339
|
+
append("#").append(imeTraceSequence)
|
|
340
|
+
append(" +").append(deltaMs).append("ms ")
|
|
341
|
+
append(event)
|
|
342
|
+
if (details.isNotEmpty()) {
|
|
343
|
+
append(" ").append(details)
|
|
344
|
+
}
|
|
345
|
+
append(" editor=").append(editorId)
|
|
346
|
+
append(" gen=").append(inputConnectionGeneration)
|
|
347
|
+
append(" activeIc=").append(activeInputConnection != null)
|
|
348
|
+
append(" focus=").append(hasFocus())
|
|
349
|
+
append(" applying=").append(isApplyingRustState)
|
|
350
|
+
append(" editable=").append(isEditable)
|
|
351
|
+
append(" textLen=").append(textLength)
|
|
352
|
+
append(" authLen=").append(lastAuthorizedText.length)
|
|
353
|
+
append(" sel=").append(selection)
|
|
354
|
+
append(" composingTextLen=").append(composingText?.length ?: -1)
|
|
355
|
+
append(" composingRange=").append(composingRange)
|
|
356
|
+
append(" composingRevOk=").append(composingRevisionMatches)
|
|
357
|
+
append(" invalidComp=").append(didInvalidateCompositionReplacementRange)
|
|
358
|
+
append(" deferredRustUpdate=").append(deferredRustUpdateJSON != null)
|
|
359
|
+
append(" scroll=").append(scrollX).append(",").append(scrollY)
|
|
360
|
+
}
|
|
361
|
+
Log.v(IME_TRACE_LOG_TAG, message)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
internal fun clearImeTraceForTesting() {
|
|
366
|
+
imeTraceForTesting.clear()
|
|
367
|
+
imeTraceSequence = 0L
|
|
368
|
+
lastImeTraceUptimeMs = 0L
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
internal fun imeTraceSnapshotForTesting(): List<String> =
|
|
372
|
+
imeTraceForTesting.toList()
|
|
373
|
+
|
|
374
|
+
private fun nanosToMicros(nanos: Long): Long = nanos / 1_000L
|
|
211
375
|
|
|
212
376
|
init {
|
|
213
377
|
// Configure for rich text editing.
|
|
@@ -283,22 +447,23 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
283
447
|
|
|
284
448
|
val currentStart = selectionStart
|
|
285
449
|
val currentEnd = selectionEnd
|
|
450
|
+
val authorizedSelection = authorizedSelectionForTransientInputRestore(
|
|
451
|
+
currentStart,
|
|
452
|
+
currentEnd
|
|
453
|
+
)
|
|
454
|
+
discardTransientInputAndRestoreAuthorizedTextForEditor()
|
|
286
455
|
setRawInputType(nextInputType)
|
|
287
456
|
|
|
288
457
|
val editable = text
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
currentEnd <= editable.length
|
|
295
|
-
) {
|
|
296
|
-
setSelection(currentStart, currentEnd)
|
|
458
|
+
if (editable != null && authorizedSelection != null) {
|
|
459
|
+
setSelection(
|
|
460
|
+
authorizedSelection.first.coerceIn(0, editable.length),
|
|
461
|
+
authorizedSelection.second.coerceIn(0, editable.length)
|
|
462
|
+
)
|
|
297
463
|
}
|
|
298
464
|
|
|
299
465
|
if (hasFocus()) {
|
|
300
|
-
|
|
301
|
-
imm?.restartInput(this)
|
|
466
|
+
restartInputForEditor()
|
|
302
467
|
}
|
|
303
468
|
}
|
|
304
469
|
|
|
@@ -346,16 +511,124 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
346
511
|
*/
|
|
347
512
|
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
|
348
513
|
val baseConnection = super.onCreateInputConnection(outAttrs) ?: return null
|
|
349
|
-
|
|
514
|
+
val originalInitialCapsMode = outAttrs.initialCapsMode
|
|
515
|
+
outAttrs.initialCapsMode = cursorCapsModeForEditor(
|
|
516
|
+
reqModes = outAttrs.inputType,
|
|
517
|
+
baseCapsMode = outAttrs.initialCapsMode
|
|
518
|
+
)
|
|
519
|
+
val initialSurroundingText = applyInitialSurroundingTextForIme(outAttrs)
|
|
520
|
+
val generation = nextInputConnectionGenerationForEditor()
|
|
521
|
+
recordImeTraceForTesting(
|
|
522
|
+
"createInputConnection",
|
|
523
|
+
"boundEditor=$editorId boundGen=$generation inputType=$inputType initialCaps=$originalInitialCapsMode->${outAttrs.initialCapsMode} " +
|
|
524
|
+
"imeContextPlaceholdersRemoved=${initialSurroundingText?.removedPlaceholderCount ?: 0} " +
|
|
525
|
+
"imeContextSel=${initialSurroundingText?.selectionStart ?: outAttrs.initialSelStart}..${initialSurroundingText?.selectionEnd ?: outAttrs.initialSelEnd} " +
|
|
526
|
+
"imeContextRawSel=${initialSurroundingText?.originalSelectionStart ?: selectionStart}..${initialSurroundingText?.originalSelectionEnd ?: selectionEnd} " +
|
|
527
|
+
"imeContextBeforeTail=\"${initialSurroundingText?.textBeforeSelectionTailForImeLog() ?: ""}\""
|
|
528
|
+
)
|
|
529
|
+
return EditorInputConnection(this, baseConnection, editorId, generation).also {
|
|
530
|
+
activeInputConnection = it
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private fun applyInitialSurroundingTextForIme(outAttrs: EditorInfo): ImeInitialSurroundingText? {
|
|
535
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return null
|
|
536
|
+
val initialText = initialSurroundingTextForImeForEditor() ?: return null
|
|
537
|
+
|
|
538
|
+
outAttrs.initialSelStart = initialText.selectionStart
|
|
539
|
+
outAttrs.initialSelEnd = initialText.selectionEnd
|
|
540
|
+
outAttrs.setInitialSurroundingText(initialText.text)
|
|
541
|
+
return initialText
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private fun ImeInitialSurroundingText.textBeforeSelectionTailForImeLog(limit: Int = 24): String {
|
|
545
|
+
val end = selectionStart.coerceIn(0, text.length)
|
|
546
|
+
val start = maxOf(0, end - limit)
|
|
547
|
+
return text.substring(start, end).toImeTraceSnippet()
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private fun String.toImeTraceSnippet(): String {
|
|
551
|
+
val builder = StringBuilder(length)
|
|
552
|
+
forEach { ch ->
|
|
553
|
+
when (ch) {
|
|
554
|
+
'\n' -> builder.append("\\n")
|
|
555
|
+
'\r' -> builder.append("\\r")
|
|
556
|
+
'\t' -> builder.append("\\t")
|
|
557
|
+
'\\' -> builder.append("\\\\")
|
|
558
|
+
'"' -> builder.append("\\\"")
|
|
559
|
+
else -> {
|
|
560
|
+
if (ch.code < 0x20 || ch == LayoutConstants.SYNTHETIC_PLACEHOLDER_CHARACTER[0]) {
|
|
561
|
+
builder.append("\\u")
|
|
562
|
+
builder.append(ch.code.toString(16).padStart(4, '0'))
|
|
563
|
+
} else {
|
|
564
|
+
builder.append(ch)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return builder.toString()
|
|
350
570
|
}
|
|
351
571
|
|
|
352
572
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
|
573
|
+
if (!isEditable && isReadOnlyTextMutationKeyEvent(event)) {
|
|
574
|
+
return true
|
|
575
|
+
}
|
|
576
|
+
if (handleCompositionKeyEvent(event) { super.dispatchKeyEvent(event) }) {
|
|
577
|
+
return true
|
|
578
|
+
}
|
|
353
579
|
if (handleHardwareKeyEvent(event)) {
|
|
354
580
|
return true
|
|
355
581
|
}
|
|
582
|
+
if (handlePrintableHardwareKeyEvent(event) { super.dispatchKeyEvent(event) }) {
|
|
583
|
+
return true
|
|
584
|
+
}
|
|
356
585
|
return super.dispatchKeyEvent(event)
|
|
357
586
|
}
|
|
358
587
|
|
|
588
|
+
internal fun handleCompositionKeyEvent(event: KeyEvent, applyBaseEvent: () -> Boolean): Boolean {
|
|
589
|
+
val inputConnection = activeInputConnection ?: return false
|
|
590
|
+
if (!inputConnection.hasPendingComposition()) return false
|
|
591
|
+
if (!isCompositionKeyCode(event.keyCode)) return false
|
|
592
|
+
if (event.action == KeyEvent.ACTION_DOWN) {
|
|
593
|
+
val signature = hardwareKeyEventSignature(event)
|
|
594
|
+
if (
|
|
595
|
+
lastHandledHardwareKeySignature == signature ||
|
|
596
|
+
didRecentlyHandleHardwareKeyDown(signature)
|
|
597
|
+
) {
|
|
598
|
+
return true
|
|
599
|
+
}
|
|
600
|
+
markHandledHardwareKeyDown(signature)
|
|
601
|
+
runWithTransientInputMutationGuard {
|
|
602
|
+
when (event.keyCode) {
|
|
603
|
+
KeyEvent.KEYCODE_DEL,
|
|
604
|
+
KeyEvent.KEYCODE_FORWARD_DEL -> inputConnection.deleteTransientTextForHardwareKeyEvent(event)
|
|
605
|
+
else -> applyBaseEvent()
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
inputConnection.refreshComposingTextFromEditableForEditor()
|
|
609
|
+
return true
|
|
610
|
+
}
|
|
611
|
+
if (event.action == KeyEvent.ACTION_UP) {
|
|
612
|
+
if (lastHandledHardwareKeySignature?.let {
|
|
613
|
+
it.keyCode == event.keyCode && it.downTime == event.downTime
|
|
614
|
+
} == true) {
|
|
615
|
+
lastHandledHardwareKeySignature = null
|
|
616
|
+
}
|
|
617
|
+
return true
|
|
618
|
+
}
|
|
619
|
+
return false
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private fun isCompositionKeyCode(keyCode: Int): Boolean =
|
|
623
|
+
when (keyCode) {
|
|
624
|
+
KeyEvent.KEYCODE_DEL,
|
|
625
|
+
KeyEvent.KEYCODE_FORWARD_DEL,
|
|
626
|
+
KeyEvent.KEYCODE_ENTER,
|
|
627
|
+
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
|
628
|
+
KeyEvent.KEYCODE_TAB -> true
|
|
629
|
+
else -> false
|
|
630
|
+
}
|
|
631
|
+
|
|
359
632
|
override fun onDraw(canvas: android.graphics.Canvas) {
|
|
360
633
|
super.onDraw(canvas)
|
|
361
634
|
|
|
@@ -492,7 +765,15 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
492
765
|
* @param id The editor ID from `editor_create()`.
|
|
493
766
|
* @param initialHTML Optional HTML to set as initial content.
|
|
494
767
|
*/
|
|
495
|
-
fun bindEditor(id: Long, initialHTML: String? = null) {
|
|
768
|
+
fun bindEditor(id: Long, initialHTML: String? = null, notifyListener: Boolean = true) {
|
|
769
|
+
if (id != 0L && NativeEditorViewRegistry.isDestroyed(id)) {
|
|
770
|
+
discardTransientNativeInputForEditorRebind()
|
|
771
|
+
editorId = 0L
|
|
772
|
+
return
|
|
773
|
+
}
|
|
774
|
+
if (editorId != id) {
|
|
775
|
+
discardTransientNativeInputForEditorRebind()
|
|
776
|
+
}
|
|
496
777
|
editorId = id
|
|
497
778
|
|
|
498
779
|
if (!initialHTML.isNullOrEmpty()) {
|
|
@@ -502,7 +783,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
502
783
|
} else {
|
|
503
784
|
// Pull current state from Rust (content may already be loaded via bridge).
|
|
504
785
|
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
505
|
-
applyUpdateJSON(stateJSON)
|
|
786
|
+
applyUpdateJSON(stateJSON, notifyListener = notifyListener)
|
|
506
787
|
}
|
|
507
788
|
}
|
|
508
789
|
|
|
@@ -510,6 +791,9 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
510
791
|
* Unbind from the current editor instance.
|
|
511
792
|
*/
|
|
512
793
|
fun unbindEditor() {
|
|
794
|
+
if (editorId != 0L) {
|
|
795
|
+
discardTransientNativeInputForEditorRebind()
|
|
796
|
+
}
|
|
513
797
|
editorId = 0
|
|
514
798
|
}
|
|
515
799
|
|
|
@@ -530,7 +814,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
530
814
|
renderAppearanceRevision += 1L
|
|
531
815
|
setBackgroundColor(theme?.backgroundColor ?: baseBackgroundColor)
|
|
532
816
|
applyContentInsets(theme?.contentInsets)
|
|
533
|
-
if (
|
|
817
|
+
if (hasLiveEditor()) {
|
|
534
818
|
val previousScrollX = scrollX
|
|
535
819
|
val previousScrollY = scrollY
|
|
536
820
|
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
@@ -685,36 +969,201 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
685
969
|
* Called by [EditorInputConnection.commitText]. Routes the text through
|
|
686
970
|
* the Rust editor instead of directly inserting into the EditText.
|
|
687
971
|
*/
|
|
688
|
-
fun handleTextCommit(text: String) {
|
|
689
|
-
|
|
690
|
-
if (
|
|
972
|
+
fun handleTextCommit(text: String, newCursorPosition: Int = 1) {
|
|
973
|
+
val startedAt = System.nanoTime()
|
|
974
|
+
if (!isEditable) {
|
|
975
|
+
recordImeTraceForTesting("handleTextCommitNoop", "reason=notEditable textLength=${text.length}")
|
|
976
|
+
return
|
|
977
|
+
}
|
|
978
|
+
if (isApplyingRustState) {
|
|
979
|
+
recordImeTraceForTesting("handleTextCommitNoop", "reason=applyingRust textLength=${text.length}")
|
|
980
|
+
return
|
|
981
|
+
}
|
|
982
|
+
val selectionRange = normalizedUtf16SelectionRange()
|
|
983
|
+
if (selectionRange == null) {
|
|
984
|
+
recordImeTraceForTesting("handleTextCommitNoop", "reason=noSelection textLength=${text.length}")
|
|
985
|
+
return
|
|
986
|
+
}
|
|
691
987
|
if (editorId == 0L) {
|
|
692
988
|
// No Rust editor bound — fall through to direct editing (dev mode).
|
|
693
989
|
val editable = this.text ?: return
|
|
694
|
-
val start =
|
|
695
|
-
val end = selectionEnd
|
|
990
|
+
val (start, end) = selectionRange
|
|
696
991
|
editable.replace(start, end, text)
|
|
992
|
+
recordImeTraceForTesting(
|
|
993
|
+
"handleTextCommitDirect",
|
|
994
|
+
"textLength=${text.length} utf16Sel=$start..$end totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
995
|
+
)
|
|
996
|
+
return
|
|
997
|
+
}
|
|
998
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) {
|
|
999
|
+
recordImeTraceForTesting("handleTextCommitNoop", "reason=destroyedEditor textLength=${text.length}")
|
|
697
1000
|
return
|
|
698
1001
|
}
|
|
699
1002
|
|
|
700
1003
|
// Handle Enter/Return as a block split operation.
|
|
701
1004
|
if (text == "\n") {
|
|
1005
|
+
recordImeTraceForTesting(
|
|
1006
|
+
"handleTextCommit",
|
|
1007
|
+
"route=return utf16Sel=${selectionRange.first}..${selectionRange.second}"
|
|
1008
|
+
)
|
|
702
1009
|
handleReturnKey()
|
|
1010
|
+
recordImeTraceForTesting(
|
|
1011
|
+
"handleTextCommitDone",
|
|
1012
|
+
"route=return totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1013
|
+
)
|
|
703
1014
|
return
|
|
704
1015
|
}
|
|
705
1016
|
|
|
706
1017
|
val currentText = this.text?.toString() ?: ""
|
|
707
|
-
val
|
|
708
|
-
|
|
1018
|
+
val scalarSelectionRange = normalizedScalarSelectionRange(currentText)
|
|
1019
|
+
if (scalarSelectionRange == null) {
|
|
1020
|
+
recordImeTraceForTesting("handleTextCommitNoop", "reason=noScalarSelection textLength=${text.length}")
|
|
1021
|
+
return
|
|
1022
|
+
}
|
|
1023
|
+
val (scalarStart, scalarEnd) = scalarSelectionRange
|
|
1024
|
+
val requestedCursor = requestedCursorScalar(
|
|
1025
|
+
scalarStart,
|
|
1026
|
+
scalarEnd,
|
|
1027
|
+
currentText,
|
|
1028
|
+
text,
|
|
1029
|
+
newCursorPosition
|
|
1030
|
+
)
|
|
1031
|
+
recordImeTraceForTesting(
|
|
1032
|
+
"handleTextCommit",
|
|
1033
|
+
"textLength=${text.length} cursor=$newCursorPosition utf16Sel=${selectionRange.first}..${selectionRange.second} scalarSel=$scalarStart..$scalarEnd requestedCursor=$requestedCursor"
|
|
1034
|
+
)
|
|
1035
|
+
val didApplyOptimisticVisibleText = applyOptimisticPlainTextCommitIfPossible(
|
|
1036
|
+
startUtf16 = selectionRange.first,
|
|
1037
|
+
endUtf16 = selectionRange.second,
|
|
1038
|
+
committedText = text,
|
|
1039
|
+
newCursorPosition = newCursorPosition
|
|
1040
|
+
)
|
|
1041
|
+
if (didApplyOptimisticVisibleText) {
|
|
1042
|
+
recordImeTraceForTesting(
|
|
1043
|
+
"optimisticVisibleTextCommit",
|
|
1044
|
+
"textLength=${text.length} utf16Sel=${selectionRange.first}..${selectionRange.second}"
|
|
1045
|
+
)
|
|
1046
|
+
}
|
|
1047
|
+
insertPlainTextRangeInRust(
|
|
1048
|
+
scalarStart,
|
|
1049
|
+
scalarEnd,
|
|
1050
|
+
text,
|
|
1051
|
+
requestedCursorScalar = requestedCursor
|
|
1052
|
+
)
|
|
1053
|
+
recordImeTraceForTesting(
|
|
1054
|
+
"handleTextCommitDone",
|
|
1055
|
+
"textLength=${text.length} totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1056
|
+
)
|
|
1057
|
+
}
|
|
709
1058
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1059
|
+
private data class OptimisticInlineSpan(
|
|
1060
|
+
val span: Any,
|
|
1061
|
+
val flags: Int
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
private fun applyOptimisticPlainTextCommitIfPossible(
|
|
1065
|
+
startUtf16: Int,
|
|
1066
|
+
endUtf16: Int,
|
|
1067
|
+
committedText: String,
|
|
1068
|
+
newCursorPosition: Int
|
|
1069
|
+
): Boolean {
|
|
1070
|
+
if (newCursorPosition != 1) return false
|
|
1071
|
+
if (startUtf16 != endUtf16) return false
|
|
1072
|
+
if (committedText.isEmpty()) return false
|
|
1073
|
+
if (committedText.codePointCount(0, committedText.length) != 1) return false
|
|
1074
|
+
if (committedText.indexOf('\n') >= 0 || committedText.indexOf('\r') >= 0) return false
|
|
1075
|
+
if (hasCompositionTrackingForEditor()) return false
|
|
1076
|
+
val editable = text ?: return false
|
|
1077
|
+
val currentText = editable.toString()
|
|
1078
|
+
if (currentText != lastAuthorizedText) return false
|
|
1079
|
+
if (startUtf16 < 0 || endUtf16 < startUtf16 || endUtf16 > editable.length) return false
|
|
1080
|
+
val spanned = editable as? Spanned
|
|
1081
|
+
if (spanned != null && spannedRangeContainsImageSpan(spanned, startUtf16, endUtf16)) return false
|
|
1082
|
+
|
|
1083
|
+
val inlineSpans = spanned?.let {
|
|
1084
|
+
optimisticInlineSpansForInsertion(it, startUtf16)
|
|
1085
|
+
}.orEmpty()
|
|
1086
|
+
var didApply = false
|
|
1087
|
+
runWithTransientInputMutationGuard {
|
|
1088
|
+
editable.replace(startUtf16, endUtf16, committedText)
|
|
1089
|
+
val insertedEnd = startUtf16 + committedText.length
|
|
1090
|
+
applyOptimisticInlineSpans(editable, startUtf16, insertedEnd, inlineSpans)
|
|
1091
|
+
Selection.setSelection(editable, insertedEnd, insertedEnd)
|
|
1092
|
+
didApply = true
|
|
1093
|
+
true
|
|
1094
|
+
}
|
|
1095
|
+
if (didApply) {
|
|
1096
|
+
pendingOptimisticRenderText = editable.toString()
|
|
1097
|
+
}
|
|
1098
|
+
return didApply
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
private fun optimisticInlineSpansForInsertion(
|
|
1102
|
+
spanned: Spanned,
|
|
1103
|
+
insertionStart: Int
|
|
1104
|
+
): List<OptimisticInlineSpan> {
|
|
1105
|
+
if (spanned.isEmpty()) return emptyList()
|
|
1106
|
+
val sourceIndex = when {
|
|
1107
|
+
insertionStart > 0 -> insertionStart - 1
|
|
1108
|
+
insertionStart < spanned.length -> insertionStart
|
|
1109
|
+
else -> return emptyList()
|
|
1110
|
+
}
|
|
1111
|
+
val queryStart = sourceIndex.coerceIn(0, spanned.length - 1)
|
|
1112
|
+
val queryEnd = (queryStart + 1).coerceAtMost(spanned.length)
|
|
1113
|
+
val spans = mutableListOf<OptimisticInlineSpan>()
|
|
1114
|
+
spanned.getSpans(queryStart, queryEnd, Any::class.java).forEach { span ->
|
|
1115
|
+
if (spanned.getSpanStart(span) > queryStart || spanned.getSpanEnd(span) <= queryStart) {
|
|
1116
|
+
return@forEach
|
|
1117
|
+
}
|
|
1118
|
+
cloneOptimisticInlineSpan(span)?.let { clone ->
|
|
1119
|
+
spans.add(OptimisticInlineSpan(clone, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE))
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return spans
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
private fun cloneOptimisticInlineSpan(span: Any): Any? =
|
|
1126
|
+
when (span) {
|
|
1127
|
+
is ForegroundColorSpan -> ForegroundColorSpan(span.foregroundColor)
|
|
1128
|
+
is BackgroundColorSpan -> BackgroundColorSpan(span.backgroundColor)
|
|
1129
|
+
is AbsoluteSizeSpan -> AbsoluteSizeSpan(span.size, span.dip)
|
|
1130
|
+
is StyleSpan -> StyleSpan(span.style)
|
|
1131
|
+
is UnderlineSpan -> UnderlineSpan()
|
|
1132
|
+
is StrikethroughSpan -> StrikethroughSpan()
|
|
1133
|
+
else -> null
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private fun applyOptimisticInlineSpans(
|
|
1137
|
+
editable: Editable,
|
|
1138
|
+
start: Int,
|
|
1139
|
+
end: Int,
|
|
1140
|
+
inlineSpans: List<OptimisticInlineSpan>
|
|
1141
|
+
) {
|
|
1142
|
+
if (start >= end || end > editable.length) return
|
|
1143
|
+
var hasColor = false
|
|
1144
|
+
var hasSize = false
|
|
1145
|
+
inlineSpans.forEach { spec ->
|
|
1146
|
+
hasColor = hasColor || spec.span is ForegroundColorSpan
|
|
1147
|
+
hasSize = hasSize || spec.span is AbsoluteSizeSpan
|
|
1148
|
+
editable.setSpan(spec.span, start, end, spec.flags)
|
|
1149
|
+
}
|
|
1150
|
+
val textStyle = theme?.effectiveTextStyle("paragraph")
|
|
1151
|
+
if (!hasColor) {
|
|
1152
|
+
editable.setSpan(
|
|
1153
|
+
ForegroundColorSpan(textStyle?.color ?: baseTextColor),
|
|
1154
|
+
start,
|
|
1155
|
+
end,
|
|
1156
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1157
|
+
)
|
|
1158
|
+
}
|
|
1159
|
+
if (!hasSize) {
|
|
1160
|
+
val resolvedTextSize = textStyle?.fontSize?.times(resources.displayMetrics.density) ?: baseFontSize
|
|
1161
|
+
editable.setSpan(
|
|
1162
|
+
AbsoluteSizeSpan(resolvedTextSize.toInt(), false),
|
|
1163
|
+
start,
|
|
1164
|
+
end,
|
|
1165
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1166
|
+
)
|
|
718
1167
|
}
|
|
719
1168
|
}
|
|
720
1169
|
|
|
@@ -728,165 +1177,852 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
728
1177
|
}
|
|
729
1178
|
}
|
|
730
1179
|
|
|
731
|
-
fun
|
|
732
|
-
if (
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
return
|
|
1180
|
+
internal fun authorizedUtf16Range(start: Int, end: Int): Pair<Int, Int> {
|
|
1181
|
+
if (start == end) {
|
|
1182
|
+
val snapped = PositionBridge.snapToScalarBoundary(
|
|
1183
|
+
start,
|
|
1184
|
+
lastAuthorizedText,
|
|
1185
|
+
biasForward = true
|
|
1186
|
+
)
|
|
1187
|
+
return snapped to snapped
|
|
739
1188
|
}
|
|
1189
|
+
return PositionBridge.snapRangeToScalarBoundaries(start, end, lastAuthorizedText)
|
|
1190
|
+
}
|
|
740
1191
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
.coerceIn(0, authorizedText.length)
|
|
744
|
-
val endUtf16 = maxOf(replacementStartUtf16, replacementEndUtf16)
|
|
745
|
-
.coerceIn(0, authorizedText.length)
|
|
746
|
-
val scalarStart = PositionBridge.utf16ToScalar(startUtf16, authorizedText)
|
|
747
|
-
val scalarEnd = PositionBridge.utf16ToScalar(endUtf16, authorizedText)
|
|
1192
|
+
internal fun isCurrentTextAuthorizedForEditor(): Boolean =
|
|
1193
|
+
(text?.toString() ?: "") == lastAuthorizedText
|
|
748
1194
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1195
|
+
internal fun captureCompositionReplacementRangeIfNeeded() {
|
|
1196
|
+
if (didInvalidateCompositionReplacementRange) return
|
|
1197
|
+
if (compositionReplacementRange() != null) return
|
|
1198
|
+
val (start, end) = normalizedUtf16SelectionRange() ?: return
|
|
1199
|
+
setCompositionReplacementRange(start, end)
|
|
754
1200
|
}
|
|
755
1201
|
|
|
756
|
-
|
|
1202
|
+
internal fun setCompositionReplacementRange(start: Int, end: Int) {
|
|
1203
|
+
if (didInvalidateCompositionReplacementRange) return
|
|
1204
|
+
val replacementRange = authorizedUtf16Range(start, end)
|
|
1205
|
+
composingReplacementStartUtf16 = replacementRange.first
|
|
1206
|
+
composingReplacementEndUtf16 = replacementRange.second
|
|
1207
|
+
composingReplacementAuthorizedTextRevision = lastAuthorizedTextRevision
|
|
1208
|
+
didInvalidateCompositionReplacementRange = false
|
|
1209
|
+
}
|
|
757
1210
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
*/
|
|
766
|
-
fun handleDelete(beforeLength: Int, afterLength: Int) {
|
|
767
|
-
if (!isEditable) return
|
|
768
|
-
if (isApplyingRustState) return
|
|
769
|
-
if (editorId == 0L) {
|
|
770
|
-
// Dev mode: direct editing.
|
|
771
|
-
val editable = this.text ?: return
|
|
772
|
-
val cursor = selectionStart
|
|
773
|
-
val delStart = maxOf(0, cursor - beforeLength)
|
|
774
|
-
val delEnd = minOf(editable.length, cursor + afterLength)
|
|
775
|
-
editable.delete(delStart, delEnd)
|
|
776
|
-
return
|
|
1211
|
+
internal fun compositionReplacementRange(): Pair<Int, Int>? {
|
|
1212
|
+
val start = composingReplacementStartUtf16 ?: return null
|
|
1213
|
+
val end = composingReplacementEndUtf16 ?: return null
|
|
1214
|
+
if (composingReplacementAuthorizedTextRevision != lastAuthorizedTextRevision) {
|
|
1215
|
+
clearCompositionTrackingForEditor()
|
|
1216
|
+
didInvalidateCompositionReplacementRange = true
|
|
1217
|
+
return null
|
|
777
1218
|
}
|
|
1219
|
+
return start to end
|
|
1220
|
+
}
|
|
778
1221
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1222
|
+
private fun authorizedSelectionForTransientInputRestore(
|
|
1223
|
+
currentStart: Int,
|
|
1224
|
+
currentEnd: Int
|
|
1225
|
+
): Pair<Int, Int>? {
|
|
1226
|
+
compositionReplacementRange()?.let { return it }
|
|
1227
|
+
return if (
|
|
1228
|
+
currentStart >= 0 &&
|
|
1229
|
+
currentEnd >= 0 &&
|
|
1230
|
+
currentStart <= lastAuthorizedText.length &&
|
|
1231
|
+
currentEnd <= lastAuthorizedText.length
|
|
785
1232
|
) {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1233
|
+
currentStart to currentEnd
|
|
1234
|
+
} else {
|
|
1235
|
+
null
|
|
789
1236
|
}
|
|
790
|
-
|
|
791
|
-
val delEnd = minOf(currentText.length, cursor + afterLength)
|
|
792
|
-
|
|
793
|
-
val scalarStart = PositionBridge.utf16ToScalar(delStart, currentText)
|
|
794
|
-
val scalarEnd = PositionBridge.utf16ToScalar(delEnd, currentText)
|
|
1237
|
+
}
|
|
795
1238
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}
|
|
1239
|
+
internal fun consumeInvalidatedCompositionReplacementRangeForEditor(): Boolean {
|
|
1240
|
+
val invalidated = didInvalidateCompositionReplacementRange
|
|
1241
|
+
didInvalidateCompositionReplacementRange = false
|
|
1242
|
+
return invalidated
|
|
801
1243
|
}
|
|
802
1244
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
*
|
|
806
|
-
* If there's a range selection, deletes the range. Otherwise deletes
|
|
807
|
-
* the grapheme cluster before the cursor.
|
|
808
|
-
*/
|
|
809
|
-
fun handleBackspace() {
|
|
810
|
-
if (!isEditable) return
|
|
811
|
-
if (isApplyingRustState) return
|
|
812
|
-
if (editorId == 0L) {
|
|
813
|
-
// Dev mode: direct editing.
|
|
814
|
-
val editable = this.text ?: return
|
|
815
|
-
val start = selectionStart
|
|
816
|
-
val end = selectionEnd
|
|
817
|
-
if (start != end) {
|
|
818
|
-
editable.delete(start, end)
|
|
819
|
-
} else if (start > 0) {
|
|
820
|
-
// Delete one grapheme cluster backward.
|
|
821
|
-
val prevBoundary = PositionBridge.snapToGraphemeBoundary(start - 1, text?.toString() ?: "")
|
|
822
|
-
val adjustedPrev = if (prevBoundary >= start) maxOf(0, start - 1) else prevBoundary
|
|
823
|
-
editable.delete(adjustedPrev, start)
|
|
824
|
-
}
|
|
825
|
-
return
|
|
826
|
-
}
|
|
1245
|
+
internal fun hasInvalidatedCompositionReplacementRangeForEditor(): Boolean =
|
|
1246
|
+
didInvalidateCompositionReplacementRange
|
|
827
1247
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1248
|
+
internal fun setComposingTextForEditor(text: String?) {
|
|
1249
|
+
composingText = text
|
|
1250
|
+
}
|
|
831
1251
|
|
|
832
|
-
|
|
833
|
-
// Range selection: delete the range.
|
|
834
|
-
val scalarStart = PositionBridge.utf16ToScalar(start, currentText)
|
|
835
|
-
val scalarEnd = PositionBridge.utf16ToScalar(end, currentText)
|
|
836
|
-
deleteRangeInRust(scalarStart, scalarEnd)
|
|
837
|
-
} else if (start > 0) {
|
|
838
|
-
if (currentText.getOrNull(start - 1) == EMPTY_BLOCK_PLACEHOLDER) {
|
|
839
|
-
val scalarCursor = PositionBridge.utf16ToScalar(start, currentText)
|
|
840
|
-
deleteBackwardAtSelectionScalarInRust(scalarCursor, scalarCursor)
|
|
841
|
-
return
|
|
842
|
-
}
|
|
843
|
-
// Cursor: delete one grapheme cluster backward.
|
|
844
|
-
// Find the previous grapheme boundary by snapping (start - 1).
|
|
845
|
-
val breakIter = java.text.BreakIterator.getCharacterInstance()
|
|
846
|
-
breakIter.setText(currentText)
|
|
847
|
-
val prevBoundary = breakIter.preceding(start)
|
|
848
|
-
val prevUtf16 = if (prevBoundary == java.text.BreakIterator.DONE) 0 else prevBoundary
|
|
1252
|
+
internal fun composingTextForEditor(): String? = composingText
|
|
849
1253
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1254
|
+
internal fun samsungSentenceCapsComposingTextForEditor(composingText: String?): String? {
|
|
1255
|
+
if (composingText.isNullOrEmpty()) return composingText
|
|
1256
|
+
if (!isSamsungKeyboardActiveForEditor()) return composingText
|
|
1257
|
+
if ((inputType and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) {
|
|
1258
|
+
return composingText
|
|
1259
|
+
}
|
|
1260
|
+
val (replacementStart, replacementEnd) = compositionReplacementRange() ?: return composingText
|
|
1261
|
+
if (replacementStart != replacementEnd) return composingText
|
|
1262
|
+
if (!isRenderedLineStartForSentenceCaps(lastAuthorizedText, replacementStart)) {
|
|
1263
|
+
return composingText
|
|
859
1264
|
}
|
|
1265
|
+
|
|
1266
|
+
val firstCodePoint = Character.codePointAt(composingText, 0)
|
|
1267
|
+
if (!Character.isLowerCase(firstCodePoint)) return composingText
|
|
1268
|
+
val adjusted = buildString(composingText.length) {
|
|
1269
|
+
appendCodePoint(Character.toTitleCase(firstCodePoint))
|
|
1270
|
+
append(composingText.substring(Character.charCount(firstCodePoint)))
|
|
1271
|
+
}
|
|
1272
|
+
recordImeTraceForTesting(
|
|
1273
|
+
"samsungSentenceCapsFallback",
|
|
1274
|
+
"range=$replacementStart..$replacementEnd textLength=${composingText.length}"
|
|
1275
|
+
)
|
|
1276
|
+
return adjusted
|
|
860
1277
|
}
|
|
861
1278
|
|
|
862
|
-
|
|
1279
|
+
internal fun applyTransientComposingTextStyleForEditor() {
|
|
1280
|
+
val editable = text ?: return
|
|
1281
|
+
removeTransientComposingTextStyleSpans(editable)
|
|
863
1282
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
fun handleReturnKey() {
|
|
868
|
-
if (!isEditable) return
|
|
869
|
-
if (isApplyingRustState) return
|
|
1283
|
+
val start = BaseInputConnection.getComposingSpanStart(editable)
|
|
1284
|
+
val end = BaseInputConnection.getComposingSpanEnd(editable)
|
|
1285
|
+
if (start < 0 || end < 0 || start >= end || end > editable.length) return
|
|
870
1286
|
|
|
871
|
-
val
|
|
872
|
-
val
|
|
873
|
-
val
|
|
1287
|
+
val textStyle = theme?.effectiveTextStyle("paragraph")
|
|
1288
|
+
val resolvedTextSize = textStyle?.fontSize?.times(resources.displayMetrics.density) ?: baseFontSize
|
|
1289
|
+
val resolvedTextColor = textStyle?.color ?: baseTextColor
|
|
874
1290
|
|
|
875
|
-
|
|
1291
|
+
editable.setSpan(
|
|
1292
|
+
TransientComposingSizeSpan(resolvedTextSize.toInt()),
|
|
1293
|
+
start,
|
|
1294
|
+
end,
|
|
1295
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1296
|
+
)
|
|
1297
|
+
editable.setSpan(
|
|
1298
|
+
TransientComposingColorSpan(resolvedTextColor),
|
|
1299
|
+
start,
|
|
1300
|
+
end,
|
|
1301
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
val typefaceStyle = textStyle?.typefaceStyle() ?: Typeface.NORMAL
|
|
1305
|
+
if (typefaceStyle != Typeface.NORMAL) {
|
|
1306
|
+
editable.setSpan(
|
|
1307
|
+
TransientComposingStyleSpan(typefaceStyle),
|
|
1308
|
+
start,
|
|
1309
|
+
end,
|
|
1310
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1311
|
+
)
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
val fontFamily = textStyle?.fontFamily?.takeIf { it.isNotBlank() }
|
|
1315
|
+
if (fontFamily != null) {
|
|
1316
|
+
editable.setSpan(
|
|
1317
|
+
TransientComposingTypefaceSpan(fontFamily),
|
|
1318
|
+
start,
|
|
1319
|
+
end,
|
|
1320
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
1321
|
+
)
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
private fun removeTransientComposingTextStyleSpans(editable: Editable) {
|
|
1326
|
+
editable
|
|
1327
|
+
.getSpans(0, editable.length, TransientComposingTextStyleSpan::class.java)
|
|
1328
|
+
.forEach(editable::removeSpan)
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
internal fun composingTextFromVisibleReplacementForEditor(): String? {
|
|
1332
|
+
val (start, end) = compositionReplacementRange() ?: return null
|
|
1333
|
+
val authorizedText = lastAuthorizedText
|
|
1334
|
+
val currentText = text?.toString() ?: return null
|
|
1335
|
+
if (start < 0 || end < start || end > authorizedText.length) return null
|
|
1336
|
+
|
|
1337
|
+
val authorizedPrefix = authorizedText.substring(0, start)
|
|
1338
|
+
val authorizedSuffix = authorizedText.substring(end)
|
|
1339
|
+
if (!currentText.startsWith(authorizedPrefix)) return null
|
|
1340
|
+
if (!currentText.endsWith(authorizedSuffix)) return null
|
|
1341
|
+
|
|
1342
|
+
val replacementEnd = currentText.length - authorizedSuffix.length
|
|
1343
|
+
if (replacementEnd < authorizedPrefix.length) return null
|
|
1344
|
+
return currentText.substring(authorizedPrefix.length, replacementEnd)
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
internal fun clearCompositionTrackingForEditor() {
|
|
1348
|
+
composingText = null
|
|
1349
|
+
composingReplacementStartUtf16 = null
|
|
1350
|
+
composingReplacementEndUtf16 = null
|
|
1351
|
+
composingReplacementAuthorizedTextRevision = null
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private fun hasCompositionTrackingForEditor(): Boolean =
|
|
1355
|
+
composingText != null ||
|
|
1356
|
+
composingReplacementStartUtf16 != null ||
|
|
1357
|
+
composingReplacementEndUtf16 != null ||
|
|
1358
|
+
composingReplacementAuthorizedTextRevision != null
|
|
1359
|
+
|
|
1360
|
+
private fun retireInputConnectionForEditor() {
|
|
1361
|
+
recordImeTraceForTesting("retireInputConnection")
|
|
1362
|
+
activeInputConnection?.clearCompositionTrackingForEditor()
|
|
1363
|
+
invalidateInputConnectionsForEditor()
|
|
1364
|
+
clearCompositionTrackingForEditor()
|
|
1365
|
+
clearCompositionInvalidationForEditor()
|
|
1366
|
+
clearNativeComposingSpans()
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
internal fun isEditorDestroyedForInput(): Boolean =
|
|
1370
|
+
editorId != 0L && NativeEditorViewRegistry.isDestroyed(editorId)
|
|
1371
|
+
|
|
1372
|
+
private fun hasLiveEditor(): Boolean =
|
|
1373
|
+
editorId != 0L && !isEditorDestroyedForInput()
|
|
1374
|
+
|
|
1375
|
+
private fun discardTransientInputForDestroyedEditorIfNeeded(): Boolean {
|
|
1376
|
+
if (!isEditorDestroyedForInput()) return false
|
|
1377
|
+
retireInputConnectionForEditor()
|
|
1378
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1379
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
1380
|
+
return true
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
private fun discardTransientInputAndRestoreAuthorizedTextForEditor() {
|
|
1384
|
+
retireInputConnectionForEditor()
|
|
1385
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1386
|
+
restoreAuthorizedTextSnapshotForEditor()
|
|
1387
|
+
suppressNativeTextMutationAdoptionForCurrentRevision()
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
private fun restoreAuthorizedTextSnapshotForEditor() {
|
|
1391
|
+
if ((text?.toString() ?: "") == lastAuthorizedText) return
|
|
1392
|
+
val authorizedSnapshot = lastAuthorizedRenderedText ?: lastAuthorizedText
|
|
1393
|
+
val wasApplyingRustState = isApplyingRustState
|
|
1394
|
+
isApplyingRustState = true
|
|
1395
|
+
beginBatchEdit()
|
|
1396
|
+
try {
|
|
1397
|
+
setText(authorizedSnapshot)
|
|
1398
|
+
} finally {
|
|
1399
|
+
endBatchEdit()
|
|
1400
|
+
isApplyingRustState = wasApplyingRustState
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
private fun restartInputAfterCompositionInvalidationIfNeeded(shouldRestart: Boolean) {
|
|
1405
|
+
if (!shouldRestart) return
|
|
1406
|
+
restartInputForEditorIfFocused("focused")
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
private fun restartInputForEditorIfFocused(source: String) {
|
|
1410
|
+
if (!hasFocus()) return
|
|
1411
|
+
restartInputForEditor(source)
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
private fun restartInputForEditor(source: String = "explicit") {
|
|
1415
|
+
recordImeTraceForTesting("restartInput", "source=$source")
|
|
1416
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1417
|
+
imm?.restartInput(this)
|
|
1418
|
+
scheduleSelectionUpdateAfterRestartInput(source)
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
private fun scheduleSelectionUpdateAfterRestartInput(source: String) {
|
|
1422
|
+
val generation = ++restartInputSelectionUpdateGeneration
|
|
1423
|
+
post {
|
|
1424
|
+
if (generation != restartInputSelectionUpdateGeneration) return@post
|
|
1425
|
+
if (!hasFocus()) return@post
|
|
1426
|
+
val start = selectionStart
|
|
1427
|
+
val end = selectionEnd
|
|
1428
|
+
if (start < 0 || end < 0) {
|
|
1429
|
+
recordImeTraceForTesting(
|
|
1430
|
+
"updateSelectionAfterRestartSkipped",
|
|
1431
|
+
"source=$source reason=selection start=$start end=$end"
|
|
1432
|
+
)
|
|
1433
|
+
return@post
|
|
1434
|
+
}
|
|
1435
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1436
|
+
imm?.updateSelection(this, start, end, -1, -1)
|
|
1437
|
+
recordImeTraceForTesting(
|
|
1438
|
+
"updateSelectionAfterRestart",
|
|
1439
|
+
"source=$source sel=$start..$end"
|
|
1440
|
+
)
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
private fun scheduleLineBoundaryInputRefreshForEditor(source: String) {
|
|
1445
|
+
if (!hasFocus()) return
|
|
1446
|
+
val generation = ++lineBoundaryInputRefreshGeneration
|
|
1447
|
+
recordImeTraceForTesting(
|
|
1448
|
+
"lineBoundaryInputRefreshScheduled",
|
|
1449
|
+
"source=$source generation=$generation"
|
|
1450
|
+
)
|
|
1451
|
+
post {
|
|
1452
|
+
if (generation != lineBoundaryInputRefreshGeneration) return@post
|
|
1453
|
+
if (!hasFocus()) return@post
|
|
1454
|
+
if (!isCursorAtRenderedLineStartForSentenceCaps()) {
|
|
1455
|
+
recordImeTraceForTesting(
|
|
1456
|
+
"lineBoundaryInputRefreshSkipped",
|
|
1457
|
+
"source=$source reason=cursor"
|
|
1458
|
+
)
|
|
1459
|
+
return@post
|
|
1460
|
+
}
|
|
1461
|
+
restartInputForEditor("lineBoundary:$source")
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
private fun clearCompositionInvalidationForEditor() {
|
|
1466
|
+
didInvalidateCompositionReplacementRange = false
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
private fun nextInputConnectionGenerationForEditor(): Long {
|
|
1470
|
+
return inputConnectionGeneration
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
internal fun isInputConnectionCurrentForEditor(
|
|
1474
|
+
boundEditorId: Long,
|
|
1475
|
+
boundGeneration: Long
|
|
1476
|
+
): Boolean =
|
|
1477
|
+
editorId == boundEditorId &&
|
|
1478
|
+
inputConnectionGeneration == boundGeneration &&
|
|
1479
|
+
!isEditorDestroyedForInput()
|
|
1480
|
+
|
|
1481
|
+
private fun invalidateInputConnectionsForEditor() {
|
|
1482
|
+
inputConnectionGeneration += 1L
|
|
1483
|
+
recordImeTraceForTesting("invalidateInputConnections", "nextGen=$inputConnectionGeneration")
|
|
1484
|
+
activeInputConnection = null
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
private fun clearNativeComposingSpans() {
|
|
1488
|
+
val editable = text ?: return
|
|
1489
|
+
BaseInputConnection.removeComposingSpans(editable)
|
|
1490
|
+
removeTransientComposingTextStyleSpans(editable)
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
internal fun restoreAuthorizedTextIfNeeded() {
|
|
1494
|
+
if (!hasLiveEditor()) return
|
|
1495
|
+
if ((text?.toString() ?: "") == lastAuthorizedText) return
|
|
1496
|
+
recordImeTraceForTesting(
|
|
1497
|
+
"restoreAuthorizedText",
|
|
1498
|
+
"authorizedLength=${lastAuthorizedText.length}"
|
|
1499
|
+
)
|
|
1500
|
+
val stateJSON = editorGetCurrentState(editorId.toULong())
|
|
1501
|
+
applyUpdateJSON(stateJSON)
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
fun discardTransientNativeInputForEditorRebind() {
|
|
1505
|
+
retireInputConnectionForEditor()
|
|
1506
|
+
nativeTextMutationAfterBlurWindow = null
|
|
1507
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
1508
|
+
clearImeTraceForTesting()
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
internal fun discardTransientNativeInputForExternalRecovery() {
|
|
1512
|
+
retireInputConnectionForEditor()
|
|
1513
|
+
nativeTextMutationAfterBlurWindow = null
|
|
1514
|
+
restoreAuthorizedTextIfNeeded()
|
|
1515
|
+
suppressNativeTextMutationAdoptionForCurrentRevision()
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
private fun discardTransientNativeInputForReadOnly() {
|
|
1519
|
+
discardTransientNativeInputForExternalRecovery()
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
fun prepareForExternalEditorUpdate(): Boolean {
|
|
1523
|
+
if (blockExternalEditorUpdatePreparationForTesting) return false
|
|
1524
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return false
|
|
1525
|
+
val inputConnection = activeInputConnection
|
|
1526
|
+
if (inputConnection?.flushPendingCompositionForExternalMutation() == false) {
|
|
1527
|
+
return false
|
|
1528
|
+
}
|
|
1529
|
+
return drainNativeTextMutationIfNeeded(allowAfterBlur = true)
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
fun prepareForExternalEditorCommand(): CommandPreparation {
|
|
1533
|
+
if (blockExternalEditorCommandPreparationForTesting) {
|
|
1534
|
+
return CommandPreparation(ready = false, updateJSON = null)
|
|
1535
|
+
}
|
|
1536
|
+
val previousAuthorizedText = lastAuthorizedText
|
|
1537
|
+
if (!prepareForExternalEditorUpdate()) {
|
|
1538
|
+
return CommandPreparation(ready = false, updateJSON = null)
|
|
1539
|
+
}
|
|
1540
|
+
if (!hasLiveEditor() || lastAuthorizedText == previousAuthorizedText) {
|
|
1541
|
+
return CommandPreparation(ready = true, updateJSON = null)
|
|
1542
|
+
}
|
|
1543
|
+
return CommandPreparation(
|
|
1544
|
+
ready = true,
|
|
1545
|
+
updateJSON = editorGetCurrentState(editorId.toULong())
|
|
1546
|
+
)
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
fun handleCompositionCommit(
|
|
1550
|
+
text: String,
|
|
1551
|
+
replacementStartUtf16: Int,
|
|
1552
|
+
replacementEndUtf16: Int,
|
|
1553
|
+
newCursorPosition: Int = 1
|
|
1554
|
+
) {
|
|
1555
|
+
val startedAt = System.nanoTime()
|
|
1556
|
+
if (!isEditable) {
|
|
1557
|
+
recordImeTraceForTesting("handleCompositionCommitNoop", "reason=notEditable textLength=${text.length}")
|
|
1558
|
+
return
|
|
1559
|
+
}
|
|
1560
|
+
if (isApplyingRustState) {
|
|
1561
|
+
recordImeTraceForTesting("handleCompositionCommitNoop", "reason=applyingRust textLength=${text.length}")
|
|
1562
|
+
return
|
|
1563
|
+
}
|
|
1564
|
+
if (!hasLiveEditor()) {
|
|
1565
|
+
recordImeTraceForTesting("handleCompositionCommitNoop", "reason=noLiveEditor textLength=${text.length}")
|
|
1566
|
+
return
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
val authorizedText = lastAuthorizedText
|
|
1570
|
+
val (startUtf16, endUtf16) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1571
|
+
replacementStartUtf16,
|
|
1572
|
+
replacementEndUtf16,
|
|
1573
|
+
authorizedText
|
|
1574
|
+
)
|
|
1575
|
+
val scalarStart = PositionBridge.utf16ToScalar(startUtf16, authorizedText)
|
|
1576
|
+
val scalarEnd = PositionBridge.utf16ToScalar(endUtf16, authorizedText)
|
|
1577
|
+
|
|
1578
|
+
if (
|
|
1579
|
+
startUtf16 <= endUtf16 &&
|
|
1580
|
+
endUtf16 <= authorizedText.length &&
|
|
1581
|
+
authorizedText.substring(startUtf16, endUtf16) == text
|
|
1582
|
+
) {
|
|
1583
|
+
val requestedCursor = requestedCursorScalar(
|
|
1584
|
+
scalarStart,
|
|
1585
|
+
scalarEnd,
|
|
1586
|
+
authorizedText,
|
|
1587
|
+
text,
|
|
1588
|
+
newCursorPosition
|
|
1589
|
+
) ?: scalarEnd
|
|
1590
|
+
recordImeTraceForTesting(
|
|
1591
|
+
"handleCompositionCommitNoop",
|
|
1592
|
+
"reason=alreadyAuthorized textLength=${text.length} requestedCursor=$requestedCursor range=$startUtf16..$endUtf16"
|
|
1593
|
+
)
|
|
1594
|
+
restoreAuthorizedTextIfNeeded()
|
|
1595
|
+
applyRequestedCursorScalar(requestedCursor)
|
|
1596
|
+
return
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (text == "\n") {
|
|
1600
|
+
recordImeTraceForTesting(
|
|
1601
|
+
"handleCompositionCommit",
|
|
1602
|
+
"route=return textLength=${text.length} utf16Range=$startUtf16..$endUtf16 scalarRange=$scalarStart..$scalarEnd"
|
|
1603
|
+
)
|
|
1604
|
+
if (scalarStart != scalarEnd) {
|
|
1605
|
+
deleteAndSplitInRust(scalarStart, scalarEnd)
|
|
1606
|
+
} else {
|
|
1607
|
+
splitBlockInRust(scalarStart)
|
|
1608
|
+
}
|
|
1609
|
+
recordImeTraceForTesting(
|
|
1610
|
+
"handleCompositionCommitDone",
|
|
1611
|
+
"route=return totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1612
|
+
)
|
|
1613
|
+
return
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
val requestedCursor = requestedCursorScalar(
|
|
1617
|
+
scalarStart,
|
|
1618
|
+
scalarEnd,
|
|
1619
|
+
authorizedText,
|
|
1620
|
+
text,
|
|
1621
|
+
newCursorPosition
|
|
1622
|
+
)
|
|
1623
|
+
recordImeTraceForTesting(
|
|
1624
|
+
"handleCompositionCommit",
|
|
1625
|
+
"textLength=${text.length} cursor=$newCursorPosition utf16Range=$startUtf16..$endUtf16 scalarRange=$scalarStart..$scalarEnd requestedCursor=$requestedCursor"
|
|
1626
|
+
)
|
|
1627
|
+
insertPlainTextRangeInRust(
|
|
1628
|
+
scalarStart,
|
|
1629
|
+
scalarEnd,
|
|
1630
|
+
text,
|
|
1631
|
+
requestedCursorScalar = requestedCursor
|
|
1632
|
+
)
|
|
1633
|
+
recordImeTraceForTesting(
|
|
1634
|
+
"handleCompositionCommitDone",
|
|
1635
|
+
"textLength=${text.length} totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1636
|
+
)
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
fun handleCorrectionCommit(
|
|
1640
|
+
offsetUtf16: Int,
|
|
1641
|
+
oldText: String,
|
|
1642
|
+
newText: String
|
|
1643
|
+
): Boolean {
|
|
1644
|
+
if (!isEditable) return true
|
|
1645
|
+
if (isApplyingRustState) return true
|
|
1646
|
+
if (!hasLiveEditor()) return false
|
|
1647
|
+
|
|
1648
|
+
val authorizedText = lastAuthorizedText
|
|
1649
|
+
if (offsetUtf16 < 0) {
|
|
1650
|
+
recordImeTraceForTesting(
|
|
1651
|
+
"correctionExplicitNoop",
|
|
1652
|
+
"reason=invalidOffset offset=$offsetUtf16 oldLength=${oldText.length} newLength=${newText.length}"
|
|
1653
|
+
)
|
|
1654
|
+
return false
|
|
1655
|
+
}
|
|
1656
|
+
val endUtf16 = offsetUtf16 + oldText.length
|
|
1657
|
+
if (endUtf16 < offsetUtf16 || endUtf16 > authorizedText.length) {
|
|
1658
|
+
recordImeTraceForTesting(
|
|
1659
|
+
"correctionExplicitNoop",
|
|
1660
|
+
"reason=outOfBounds offset=$offsetUtf16 oldLength=${oldText.length} authorizedLength=${authorizedText.length}"
|
|
1661
|
+
)
|
|
1662
|
+
return false
|
|
1663
|
+
}
|
|
1664
|
+
if (authorizedText.substring(offsetUtf16, endUtf16) != oldText) {
|
|
1665
|
+
recordImeTraceForTesting(
|
|
1666
|
+
"correctionExplicitNoop",
|
|
1667
|
+
"reason=staleText offset=$offsetUtf16 oldLength=${oldText.length} newLength=${newText.length}"
|
|
1668
|
+
)
|
|
1669
|
+
return false
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
val (startUtf16, snappedEndUtf16) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1673
|
+
offsetUtf16,
|
|
1674
|
+
endUtf16,
|
|
1675
|
+
authorizedText
|
|
1676
|
+
)
|
|
1677
|
+
if (
|
|
1678
|
+
startUtf16 != offsetUtf16 ||
|
|
1679
|
+
snappedEndUtf16 != endUtf16 ||
|
|
1680
|
+
startUtf16 > snappedEndUtf16
|
|
1681
|
+
) {
|
|
1682
|
+
recordImeTraceForTesting(
|
|
1683
|
+
"correctionExplicitNoop",
|
|
1684
|
+
"reason=unsnappedScalarBoundary range=$offsetUtf16..$endUtf16 snapped=$startUtf16..$snappedEndUtf16"
|
|
1685
|
+
)
|
|
1686
|
+
return false
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
val scalarStart = PositionBridge.utf16ToScalar(startUtf16, authorizedText)
|
|
1690
|
+
val scalarEnd = PositionBridge.utf16ToScalar(snappedEndUtf16, authorizedText)
|
|
1691
|
+
recordImeTraceForTesting(
|
|
1692
|
+
"correctionExplicitApply",
|
|
1693
|
+
"range=$scalarStart..$scalarEnd newLength=${newText.length}"
|
|
1694
|
+
)
|
|
1695
|
+
insertPlainTextRangeInRust(scalarStart, scalarEnd, newText)
|
|
1696
|
+
return true
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
fun handleMissingOldTextCorrectionCommit(
|
|
1700
|
+
offsetUtf16: Int,
|
|
1701
|
+
newText: String
|
|
1702
|
+
): Boolean {
|
|
1703
|
+
if (!isEditable) return true
|
|
1704
|
+
if (isApplyingRustState) return true
|
|
1705
|
+
if (!hasLiveEditor()) return false
|
|
1706
|
+
|
|
1707
|
+
val authorizedText = lastAuthorizedText
|
|
1708
|
+
val tokenRange = missingOldTextCorrectionTokenRange(authorizedText, offsetUtf16)
|
|
1709
|
+
?: run {
|
|
1710
|
+
recordImeTraceForTesting(
|
|
1711
|
+
"correctionInferredNoop",
|
|
1712
|
+
"reason=noToken offset=$offsetUtf16 newLength=${newText.length}"
|
|
1713
|
+
)
|
|
1714
|
+
return false
|
|
1715
|
+
}
|
|
1716
|
+
val (startUtf16, endUtf16) = tokenRange
|
|
1717
|
+
|
|
1718
|
+
val (snappedStartUtf16, snappedEndUtf16) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1719
|
+
startUtf16,
|
|
1720
|
+
endUtf16,
|
|
1721
|
+
authorizedText
|
|
1722
|
+
)
|
|
1723
|
+
if (snappedStartUtf16 >= snappedEndUtf16) {
|
|
1724
|
+
recordImeTraceForTesting(
|
|
1725
|
+
"correctionInferredNoop",
|
|
1726
|
+
"reason=emptySnappedRange token=$startUtf16..$endUtf16 snapped=$snappedStartUtf16..$snappedEndUtf16"
|
|
1727
|
+
)
|
|
1728
|
+
return false
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
val scalarStart = PositionBridge.utf16ToScalar(snappedStartUtf16, authorizedText)
|
|
1732
|
+
val scalarEnd = PositionBridge.utf16ToScalar(snappedEndUtf16, authorizedText)
|
|
1733
|
+
recordImeTraceForTesting(
|
|
1734
|
+
"correctionInferredApply",
|
|
1735
|
+
"range=$scalarStart..$scalarEnd utf16=$snappedStartUtf16..$snappedEndUtf16 newLength=${newText.length}"
|
|
1736
|
+
)
|
|
1737
|
+
insertPlainTextRangeInRust(scalarStart, scalarEnd, newText)
|
|
1738
|
+
return true
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
private fun missingOldTextCorrectionTokenRange(
|
|
1742
|
+
text: String,
|
|
1743
|
+
offsetUtf16: Int
|
|
1744
|
+
): Pair<Int, Int>? {
|
|
1745
|
+
if (offsetUtf16 < 0 || offsetUtf16 >= text.length) return null
|
|
1746
|
+
|
|
1747
|
+
val tokenOffset = PositionBridge.snapToScalarBoundary(
|
|
1748
|
+
offsetUtf16,
|
|
1749
|
+
text,
|
|
1750
|
+
biasForward = false
|
|
1751
|
+
)
|
|
1752
|
+
if (tokenOffset < 0 || tokenOffset >= text.length) return null
|
|
1753
|
+
if (!isMissingOldTextCorrectionTokenCodePointAt(text, tokenOffset)) return null
|
|
1754
|
+
|
|
1755
|
+
var startUtf16 = tokenOffset
|
|
1756
|
+
while (startUtf16 > 0) {
|
|
1757
|
+
val previousUtf16 = Character.offsetByCodePoints(text, startUtf16, -1)
|
|
1758
|
+
if (!isMissingOldTextCorrectionTokenCodePointAt(text, previousUtf16)) break
|
|
1759
|
+
startUtf16 = previousUtf16
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
var endUtf16 = tokenOffset + Character.charCount(Character.codePointAt(text, tokenOffset))
|
|
1763
|
+
while (endUtf16 < text.length) {
|
|
1764
|
+
if (!isMissingOldTextCorrectionTokenCodePointAt(text, endUtf16)) break
|
|
1765
|
+
endUtf16 += Character.charCount(Character.codePointAt(text, endUtf16))
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
return if (startUtf16 < endUtf16) startUtf16 to endUtf16 else null
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
private fun isMissingOldTextCorrectionTokenCodePointAt(text: String, utf16Offset: Int): Boolean {
|
|
1772
|
+
if (utf16Offset < 0 || utf16Offset >= text.length) return false
|
|
1773
|
+
val codePoint = Character.codePointAt(text, utf16Offset)
|
|
1774
|
+
if (isMissingOldTextCorrectionCoreTokenCodePoint(codePoint)) return true
|
|
1775
|
+
if (!isMissingOldTextCorrectionJoinerCodePoint(codePoint)) return false
|
|
1776
|
+
|
|
1777
|
+
val previousCodePoint = previousCodePointBefore(text, utf16Offset) ?: return false
|
|
1778
|
+
val nextUtf16Offset = utf16Offset + Character.charCount(codePoint)
|
|
1779
|
+
val nextCodePoint = nextCodePointAt(text, nextUtf16Offset) ?: return false
|
|
1780
|
+
return isMissingOldTextCorrectionCoreTokenCodePoint(previousCodePoint) &&
|
|
1781
|
+
isMissingOldTextCorrectionCoreTokenCodePoint(nextCodePoint)
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
private fun isMissingOldTextCorrectionCoreTokenCodePoint(codePoint: Int): Boolean {
|
|
1785
|
+
if (Character.isLetterOrDigit(codePoint)) return true
|
|
1786
|
+
return when (Character.getType(codePoint)) {
|
|
1787
|
+
Character.NON_SPACING_MARK.toInt(),
|
|
1788
|
+
Character.COMBINING_SPACING_MARK.toInt(),
|
|
1789
|
+
Character.ENCLOSING_MARK.toInt(),
|
|
1790
|
+
Character.CONNECTOR_PUNCTUATION.toInt(),
|
|
1791
|
+
Character.MATH_SYMBOL.toInt(),
|
|
1792
|
+
Character.CURRENCY_SYMBOL.toInt(),
|
|
1793
|
+
Character.MODIFIER_SYMBOL.toInt(),
|
|
1794
|
+
Character.OTHER_SYMBOL.toInt(),
|
|
1795
|
+
Character.SURROGATE.toInt() -> true
|
|
1796
|
+
else -> false
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
private fun isMissingOldTextCorrectionJoinerCodePoint(codePoint: Int): Boolean =
|
|
1801
|
+
codePoint == '\''.code ||
|
|
1802
|
+
codePoint == 0x2018 ||
|
|
1803
|
+
codePoint == 0x2019 ||
|
|
1804
|
+
codePoint == 0x201B ||
|
|
1805
|
+
codePoint == 0xFF07 ||
|
|
1806
|
+
codePoint == '-'.code ||
|
|
1807
|
+
codePoint == 0x2010 ||
|
|
1808
|
+
codePoint == 0x2011 ||
|
|
1809
|
+
codePoint == 0x2012 ||
|
|
1810
|
+
codePoint == 0x2013 ||
|
|
1811
|
+
codePoint == 0x2014 ||
|
|
1812
|
+
codePoint == 0x2212 ||
|
|
1813
|
+
codePoint == 0x200D
|
|
1814
|
+
|
|
1815
|
+
private fun previousCodePointBefore(text: String, utf16Offset: Int): Int? {
|
|
1816
|
+
if (utf16Offset <= 0 || utf16Offset > text.length) return null
|
|
1817
|
+
val previousUtf16 = Character.offsetByCodePoints(text, utf16Offset, -1)
|
|
1818
|
+
return Character.codePointAt(text, previousUtf16)
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
private fun nextCodePointAt(text: String, utf16Offset: Int): Int? {
|
|
1822
|
+
if (utf16Offset < 0 || utf16Offset >= text.length) return null
|
|
1823
|
+
return Character.codePointAt(text, utf16Offset)
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// ── Input Handling: Deletion ────────────────────────────────────────
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Handle surrounding text deletion from the IME.
|
|
1830
|
+
*
|
|
1831
|
+
* Called by [EditorInputConnection.deleteSurroundingText].
|
|
1832
|
+
*
|
|
1833
|
+
* @param beforeLength Number of UTF-16 code units to delete before the cursor.
|
|
1834
|
+
* @param afterLength Number of UTF-16 code units to delete after the cursor.
|
|
1835
|
+
*/
|
|
1836
|
+
fun handleDelete(beforeLength: Int, afterLength: Int) {
|
|
1837
|
+
if (!isEditable) return
|
|
1838
|
+
if (isApplyingRustState) return
|
|
1839
|
+
val selectionRange = normalizedUtf16SelectionRange()
|
|
1840
|
+
if (editorId == 0L) {
|
|
1841
|
+
// Dev mode: direct editing.
|
|
1842
|
+
val editable = this.text ?: return
|
|
1843
|
+
val (selectionStart, selectionEnd) = selectionRange ?: return
|
|
1844
|
+
val delStart: Int
|
|
1845
|
+
val delEnd: Int
|
|
1846
|
+
if (selectionStart != selectionEnd) {
|
|
1847
|
+
delStart = selectionStart
|
|
1848
|
+
delEnd = selectionEnd
|
|
1849
|
+
} else {
|
|
1850
|
+
delStart = maxOf(0, selectionStart - beforeLength.coerceAtLeast(0))
|
|
1851
|
+
delEnd = minOf(editable.length, selectionStart + afterLength.coerceAtLeast(0))
|
|
1852
|
+
}
|
|
1853
|
+
editable.delete(delStart, delEnd)
|
|
1854
|
+
return
|
|
1855
|
+
}
|
|
1856
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
1857
|
+
|
|
1858
|
+
val currentText = text?.toString() ?: ""
|
|
1859
|
+
val (selectionStart, selectionEnd) = selectionRange ?: return
|
|
1860
|
+
if (selectionStart != selectionEnd) {
|
|
1861
|
+
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
1862
|
+
deleteRangeInRust(scalarStart, scalarEnd)
|
|
1863
|
+
return
|
|
1864
|
+
}
|
|
1865
|
+
val cursor = selectionStart
|
|
1866
|
+
if (beforeLength > 0 &&
|
|
1867
|
+
afterLength == 0 &&
|
|
1868
|
+
cursor > 0 &&
|
|
1869
|
+
currentText.getOrNull(cursor - 1) == EMPTY_BLOCK_PLACEHOLDER
|
|
1870
|
+
) {
|
|
1871
|
+
val scalarCursor = PositionBridge.utf16ToScalar(cursor, currentText)
|
|
1872
|
+
deleteBackwardAtSelectionScalarInRust(scalarCursor, scalarCursor)
|
|
1873
|
+
return
|
|
1874
|
+
}
|
|
1875
|
+
val rawDelStart = maxOf(0, cursor - beforeLength.coerceAtLeast(0))
|
|
1876
|
+
val rawDelEnd = minOf(currentText.length, cursor + afterLength.coerceAtLeast(0))
|
|
1877
|
+
val (delStart, delEnd) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1878
|
+
rawDelStart,
|
|
1879
|
+
rawDelEnd,
|
|
1880
|
+
currentText
|
|
1881
|
+
)
|
|
1882
|
+
|
|
1883
|
+
val scalarStart = PositionBridge.utf16ToScalar(delStart, currentText)
|
|
1884
|
+
val scalarEnd = PositionBridge.utf16ToScalar(delEnd, currentText)
|
|
1885
|
+
|
|
1886
|
+
if (scalarStart < scalarEnd) {
|
|
1887
|
+
deleteRangeInRust(scalarStart, scalarEnd)
|
|
1888
|
+
} else if (beforeLength > 0 && afterLength == 0) {
|
|
1889
|
+
deleteBackwardAtSelectionScalarInRust(scalarEnd, scalarEnd)
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
/**
|
|
1894
|
+
* Handle backspace key press (hardware keyboard or key event).
|
|
1895
|
+
*
|
|
1896
|
+
* If there's a range selection, deletes the range. Otherwise deletes
|
|
1897
|
+
* the grapheme cluster before the cursor.
|
|
1898
|
+
*/
|
|
1899
|
+
fun handleBackspace() {
|
|
1900
|
+
if (!isEditable) return
|
|
1901
|
+
if (isApplyingRustState) return
|
|
1902
|
+
val selectionRange = normalizedUtf16SelectionRange() ?: return
|
|
1903
|
+
if (editorId == 0L) {
|
|
1904
|
+
// Dev mode: direct editing.
|
|
1905
|
+
val editable = this.text ?: return
|
|
1906
|
+
val (start, end) = selectionRange
|
|
1907
|
+
if (start != end) {
|
|
1908
|
+
editable.delete(start, end)
|
|
1909
|
+
} else if (start > 0) {
|
|
1910
|
+
// Delete one grapheme cluster backward.
|
|
1911
|
+
val prevBoundary = PositionBridge.snapToGraphemeBoundary(start - 1, text?.toString() ?: "")
|
|
1912
|
+
val adjustedPrev = if (prevBoundary >= start) maxOf(0, start - 1) else prevBoundary
|
|
1913
|
+
editable.delete(adjustedPrev, start)
|
|
1914
|
+
}
|
|
1915
|
+
return
|
|
1916
|
+
}
|
|
1917
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
1918
|
+
|
|
1919
|
+
val currentText = text?.toString() ?: ""
|
|
1920
|
+
val (start, end) = selectionRange
|
|
1921
|
+
|
|
1922
|
+
if (start != end) {
|
|
1923
|
+
// Range selection: delete the range.
|
|
1924
|
+
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
1925
|
+
deleteRangeInRust(scalarStart, scalarEnd)
|
|
1926
|
+
} else if (start > 0) {
|
|
1927
|
+
if (currentText.getOrNull(start - 1) == EMPTY_BLOCK_PLACEHOLDER) {
|
|
1928
|
+
val scalarCursor = PositionBridge.utf16ToScalar(start, currentText)
|
|
1929
|
+
deleteBackwardAtSelectionScalarInRust(scalarCursor, scalarCursor)
|
|
1930
|
+
return
|
|
1931
|
+
}
|
|
1932
|
+
// Cursor: delete one grapheme cluster backward.
|
|
1933
|
+
// Find the previous grapheme boundary by snapping (start - 1).
|
|
1934
|
+
val breakIter = java.text.BreakIterator.getCharacterInstance()
|
|
1935
|
+
breakIter.setText(currentText)
|
|
1936
|
+
val prevBoundary = breakIter.preceding(start)
|
|
1937
|
+
val prevUtf16 = if (prevBoundary == java.text.BreakIterator.DONE) 0 else prevBoundary
|
|
1938
|
+
|
|
1939
|
+
val scalarStart = PositionBridge.utf16ToScalar(prevUtf16, currentText)
|
|
1940
|
+
val scalarEnd = PositionBridge.utf16ToScalar(start, currentText)
|
|
1941
|
+
if (scalarStart < scalarEnd) {
|
|
1942
|
+
deleteRangeInRust(scalarStart, scalarEnd)
|
|
1943
|
+
} else {
|
|
1944
|
+
deleteBackwardAtSelectionScalarInRust(scalarEnd, scalarEnd)
|
|
1945
|
+
}
|
|
1946
|
+
} else {
|
|
1947
|
+
deleteBackwardAtSelectionScalarInRust(0, 0)
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
fun handleForwardDelete() {
|
|
1952
|
+
if (!isEditable) return
|
|
1953
|
+
if (isApplyingRustState) return
|
|
1954
|
+
val selectionRange = normalizedUtf16SelectionRange() ?: return
|
|
1955
|
+
if (editorId == 0L) {
|
|
1956
|
+
val editable = this.text ?: return
|
|
1957
|
+
val (start, end) = selectionRange
|
|
1958
|
+
if (start != end) {
|
|
1959
|
+
editable.delete(start, end)
|
|
1960
|
+
} else if (start < editable.length) {
|
|
1961
|
+
val breakIter = java.text.BreakIterator.getCharacterInstance()
|
|
1962
|
+
breakIter.setText(editable.toString())
|
|
1963
|
+
val nextBoundary = breakIter.following(start)
|
|
1964
|
+
val nextUtf16 = if (nextBoundary == java.text.BreakIterator.DONE) {
|
|
1965
|
+
editable.length
|
|
1966
|
+
} else {
|
|
1967
|
+
nextBoundary
|
|
1968
|
+
}
|
|
1969
|
+
editable.delete(start, nextUtf16.coerceIn(start, editable.length))
|
|
1970
|
+
}
|
|
1971
|
+
return
|
|
1972
|
+
}
|
|
1973
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
1974
|
+
|
|
1975
|
+
val currentText = text?.toString() ?: ""
|
|
1976
|
+
val (start, end) = selectionRange
|
|
1977
|
+
if (start != end) {
|
|
1978
|
+
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
1979
|
+
deleteRangeInRust(scalarStart, scalarEnd)
|
|
1980
|
+
} else if (start < currentText.length) {
|
|
1981
|
+
val breakIter = java.text.BreakIterator.getCharacterInstance()
|
|
1982
|
+
breakIter.setText(currentText)
|
|
1983
|
+
val nextBoundary = breakIter.following(start)
|
|
1984
|
+
val nextUtf16 = if (nextBoundary == java.text.BreakIterator.DONE) {
|
|
1985
|
+
currentText.length
|
|
1986
|
+
} else {
|
|
1987
|
+
nextBoundary
|
|
1988
|
+
}
|
|
1989
|
+
val (utf16Start, utf16End) = PositionBridge.snapRangeToScalarBoundaries(
|
|
1990
|
+
start,
|
|
1991
|
+
nextUtf16.coerceIn(start, currentText.length),
|
|
1992
|
+
currentText
|
|
1993
|
+
)
|
|
1994
|
+
val scalarStart = PositionBridge.utf16ToScalar(utf16Start, currentText)
|
|
1995
|
+
val scalarEnd = PositionBridge.utf16ToScalar(utf16End, currentText)
|
|
1996
|
+
if (scalarStart < scalarEnd) {
|
|
1997
|
+
deleteRangeInRust(scalarStart, scalarEnd)
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// ── Input Handling: Return Key ──────────────────────────────────────
|
|
2003
|
+
|
|
2004
|
+
/**
|
|
2005
|
+
* Handle return/enter key as a block split operation.
|
|
2006
|
+
*/
|
|
2007
|
+
fun handleReturnKey() {
|
|
2008
|
+
if (!isEditable) return
|
|
2009
|
+
if (isApplyingRustState) return
|
|
2010
|
+
|
|
2011
|
+
val currentText = text?.toString() ?: ""
|
|
2012
|
+
val (start, end) = normalizedUtf16SelectionRange() ?: return
|
|
2013
|
+
|
|
2014
|
+
if (editorId == 0L) {
|
|
876
2015
|
// Dev mode: insert newline directly.
|
|
877
2016
|
val editable = this.text ?: return
|
|
878
2017
|
editable.replace(start, end, "\n")
|
|
879
2018
|
return
|
|
880
2019
|
}
|
|
2020
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
881
2021
|
|
|
882
2022
|
if (start != end) {
|
|
883
2023
|
// Range selection: atomic delete-and-split via Rust.
|
|
884
|
-
val scalarStart =
|
|
885
|
-
|
|
886
|
-
val updateJSON = editorDeleteAndSplitScalar(
|
|
887
|
-
editorId.toULong(), scalarStart.toUInt(), scalarEnd.toUInt()
|
|
888
|
-
)
|
|
889
|
-
applyUpdateJSON(updateJSON)
|
|
2024
|
+
val (scalarStart, scalarEnd) = normalizedScalarSelectionRange(currentText) ?: return
|
|
2025
|
+
deleteAndSplitInRust(scalarStart, scalarEnd)
|
|
890
2026
|
} else {
|
|
891
2027
|
val scalarPos = PositionBridge.utf16ToScalar(start, currentText)
|
|
892
2028
|
splitBlockInRust(scalarPos)
|
|
@@ -907,6 +2043,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
907
2043
|
editable.replace(start, end, "\n")
|
|
908
2044
|
return
|
|
909
2045
|
}
|
|
2046
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
910
2047
|
|
|
911
2048
|
val selection = currentScalarSelection() ?: return
|
|
912
2049
|
val updateJSON = editorInsertNodeAtSelectionScalar(
|
|
@@ -924,7 +2061,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
924
2061
|
fun handleTab(shiftPressed: Boolean): Boolean {
|
|
925
2062
|
if (!isEditable) return false
|
|
926
2063
|
if (isApplyingRustState) return false
|
|
927
|
-
if (
|
|
2064
|
+
if (!hasLiveEditor()) return false
|
|
928
2065
|
if (!isSelectionInsideList()) return false
|
|
929
2066
|
val selection = currentScalarSelection() ?: return false
|
|
930
2067
|
|
|
@@ -952,6 +2089,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
952
2089
|
handleBackspace()
|
|
953
2090
|
true
|
|
954
2091
|
}
|
|
2092
|
+
KeyEvent.KEYCODE_FORWARD_DEL -> {
|
|
2093
|
+
handleForwardDelete()
|
|
2094
|
+
true
|
|
2095
|
+
}
|
|
955
2096
|
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> {
|
|
956
2097
|
if (shiftPressed) {
|
|
957
2098
|
handleHardBreak()
|
|
@@ -965,28 +2106,55 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
965
2106
|
}
|
|
966
2107
|
}
|
|
967
2108
|
|
|
2109
|
+
private fun isSupportedHardwareMutationKey(keyCode: Int): Boolean =
|
|
2110
|
+
when (keyCode) {
|
|
2111
|
+
KeyEvent.KEYCODE_DEL,
|
|
2112
|
+
KeyEvent.KEYCODE_FORWARD_DEL,
|
|
2113
|
+
KeyEvent.KEYCODE_ENTER,
|
|
2114
|
+
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
|
2115
|
+
KeyEvent.KEYCODE_TAB -> true
|
|
2116
|
+
else -> false
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
internal fun isReadOnlyTextMutationKeyEvent(event: KeyEvent): Boolean {
|
|
2120
|
+
if (isSupportedHardwareMutationKey(event.keyCode) ||
|
|
2121
|
+
event.keyCode == KeyEvent.KEYCODE_FORWARD_DEL
|
|
2122
|
+
) {
|
|
2123
|
+
return true
|
|
2124
|
+
}
|
|
2125
|
+
if (event.keyCode == KeyEvent.KEYCODE_INSERT && event.isShiftPressed) {
|
|
2126
|
+
return true
|
|
2127
|
+
}
|
|
2128
|
+
if (event.isCtrlPressed || event.isMetaPressed) {
|
|
2129
|
+
return when (event.keyCode) {
|
|
2130
|
+
KeyEvent.KEYCODE_V,
|
|
2131
|
+
KeyEvent.KEYCODE_X,
|
|
2132
|
+
KeyEvent.KEYCODE_Z,
|
|
2133
|
+
KeyEvent.KEYCODE_Y -> true
|
|
2134
|
+
else -> false
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
if (!keyEventCharacters(event).isNullOrEmpty()) return true
|
|
2138
|
+
return event.unicodeChar != 0
|
|
2139
|
+
}
|
|
2140
|
+
|
|
968
2141
|
fun handleHardwareKeyEvent(event: KeyEvent?): Boolean {
|
|
969
2142
|
if (event == null || !isEditable || isApplyingRustState) return false
|
|
970
2143
|
|
|
971
2144
|
return when (event.action) {
|
|
972
2145
|
KeyEvent.ACTION_DOWN -> {
|
|
973
|
-
|
|
974
|
-
KeyEvent.KEYCODE_DEL,
|
|
975
|
-
KeyEvent.KEYCODE_ENTER,
|
|
976
|
-
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
|
977
|
-
KeyEvent.KEYCODE_TAB -> true
|
|
978
|
-
else -> false
|
|
979
|
-
}
|
|
980
|
-
if (!supported) return false
|
|
2146
|
+
if (!isSupportedHardwareMutationKey(event.keyCode)) return false
|
|
981
2147
|
|
|
982
|
-
|
|
983
|
-
|
|
2148
|
+
val signature = hardwareKeyEventSignature(event)
|
|
2149
|
+
if (
|
|
2150
|
+
lastHandledHardwareKeySignature == signature ||
|
|
2151
|
+
didRecentlyHandleHardwareKeyDown(signature)
|
|
2152
|
+
) {
|
|
984
2153
|
return true
|
|
985
2154
|
}
|
|
986
2155
|
|
|
987
2156
|
if (handleHardwareKeyDown(event.keyCode, event.isShiftPressed)) {
|
|
988
|
-
|
|
989
|
-
lastHandledHardwareKeyDownTime = event.downTime
|
|
2157
|
+
markHandledHardwareKeyDown(signature)
|
|
990
2158
|
true
|
|
991
2159
|
} else {
|
|
992
2160
|
false
|
|
@@ -994,10 +2162,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
994
2162
|
}
|
|
995
2163
|
|
|
996
2164
|
KeyEvent.ACTION_UP -> {
|
|
997
|
-
if (
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
2165
|
+
if (lastHandledHardwareKeySignature?.let {
|
|
2166
|
+
it.keyCode == event.keyCode && it.downTime == event.downTime
|
|
2167
|
+
} == true) {
|
|
2168
|
+
lastHandledHardwareKeySignature = null
|
|
1001
2169
|
true
|
|
1002
2170
|
} else {
|
|
1003
2171
|
false
|
|
@@ -1008,8 +2176,119 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1008
2176
|
}
|
|
1009
2177
|
}
|
|
1010
2178
|
|
|
2179
|
+
internal fun handlePrintableHardwareKeyEvent(
|
|
2180
|
+
event: KeyEvent,
|
|
2181
|
+
applyBaseEvent: () -> Boolean
|
|
2182
|
+
): Boolean {
|
|
2183
|
+
if (!isEditable || isApplyingRustState || !isPrintableHardwareMutationKey(event)) {
|
|
2184
|
+
return false
|
|
2185
|
+
}
|
|
2186
|
+
val signature = hardwareKeyEventSignature(event)
|
|
2187
|
+
return when (event.action) {
|
|
2188
|
+
KeyEvent.ACTION_DOWN -> {
|
|
2189
|
+
if (
|
|
2190
|
+
lastHandledHardwareKeySignature == signature ||
|
|
2191
|
+
didRecentlyHandleHardwareKeyDown(signature)
|
|
2192
|
+
) {
|
|
2193
|
+
true
|
|
2194
|
+
} else {
|
|
2195
|
+
val inputConnection = activeInputConnection?.takeIf {
|
|
2196
|
+
it.hasPendingComposition()
|
|
2197
|
+
}
|
|
2198
|
+
if (inputConnection != null) {
|
|
2199
|
+
var didMutate = false
|
|
2200
|
+
runWithTransientInputMutationGuard {
|
|
2201
|
+
didMutate = insertTransientHardwareText(keyEventText(event))
|
|
2202
|
+
didMutate
|
|
2203
|
+
}
|
|
2204
|
+
if (!didMutate) return false
|
|
2205
|
+
inputConnection.refreshComposingTextFromEditableForEditor()
|
|
2206
|
+
} else {
|
|
2207
|
+
applyBaseEvent()
|
|
2208
|
+
}
|
|
2209
|
+
markHandledHardwareKeyDown(signature)
|
|
2210
|
+
true
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
KeyEvent.ACTION_UP -> {
|
|
2214
|
+
if (lastHandledHardwareKeySignature?.let {
|
|
2215
|
+
it.keyCode == event.keyCode && it.downTime == event.downTime
|
|
2216
|
+
} == true) {
|
|
2217
|
+
lastHandledHardwareKeySignature = null
|
|
2218
|
+
}
|
|
2219
|
+
false
|
|
2220
|
+
}
|
|
2221
|
+
else -> false
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
private fun isPrintableHardwareMutationKey(event: KeyEvent): Boolean {
|
|
2226
|
+
if (isSupportedHardwareMutationKey(event.keyCode)) return false
|
|
2227
|
+
if (event.isCtrlPressed || event.isMetaPressed) return false
|
|
2228
|
+
return !keyEventText(event).isNullOrEmpty()
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
private fun hardwareKeyEventSignature(event: KeyEvent): HardwareKeyEventSignature =
|
|
2232
|
+
HardwareKeyEventSignature(
|
|
2233
|
+
keyCode = event.keyCode,
|
|
2234
|
+
downTime = event.downTime,
|
|
2235
|
+
repeatCount = event.repeatCount
|
|
2236
|
+
)
|
|
2237
|
+
|
|
2238
|
+
@Suppress("DEPRECATION")
|
|
2239
|
+
private fun keyEventCharacters(event: KeyEvent): String? = event.characters
|
|
2240
|
+
|
|
2241
|
+
private fun keyEventText(event: KeyEvent): String? {
|
|
2242
|
+
val characters = keyEventCharacters(event)
|
|
2243
|
+
if (!characters.isNullOrEmpty()) return characters
|
|
2244
|
+
val unicodeChar = event.unicodeChar
|
|
2245
|
+
if (unicodeChar == 0) return null
|
|
2246
|
+
return runCatching {
|
|
2247
|
+
String(Character.toChars(unicodeChar))
|
|
2248
|
+
}.getOrNull()
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
private fun insertTransientHardwareText(insertedText: String?): Boolean {
|
|
2252
|
+
if (insertedText.isNullOrEmpty()) return false
|
|
2253
|
+
val editable = text ?: return false
|
|
2254
|
+
val currentText = editable.toString()
|
|
2255
|
+
val rawStart = selectionStart
|
|
2256
|
+
val rawEnd = selectionEnd
|
|
2257
|
+
if (rawStart < 0 || rawEnd < 0) return false
|
|
2258
|
+
val start = rawStart.coerceIn(0, editable.length)
|
|
2259
|
+
val end = rawEnd.coerceIn(0, editable.length)
|
|
2260
|
+
val normalizedStart = minOf(start, end)
|
|
2261
|
+
val normalizedEnd = maxOf(start, end)
|
|
2262
|
+
val (replaceStart, replaceEnd) = PositionBridge.snapRangeToScalarBoundaries(
|
|
2263
|
+
normalizedStart,
|
|
2264
|
+
normalizedEnd,
|
|
2265
|
+
currentText
|
|
2266
|
+
)
|
|
2267
|
+
editable.replace(replaceStart, replaceEnd, insertedText)
|
|
2268
|
+
val cursor = (replaceStart + insertedText.length).coerceIn(0, editable.length)
|
|
2269
|
+
setSelection(cursor)
|
|
2270
|
+
return true
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
private fun markHandledHardwareKeyDown(signature: HardwareKeyEventSignature) {
|
|
2274
|
+
lastHandledHardwareKeySignature = signature
|
|
2275
|
+
recentHandledHardwareKeyDownSignature = signature
|
|
2276
|
+
recentHandledHardwareKeyDownUptimeMs = SystemClock.uptimeMillis()
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
private fun didRecentlyHandleHardwareKeyDown(signature: HardwareKeyEventSignature): Boolean {
|
|
2280
|
+
val recentSignature = recentHandledHardwareKeyDownSignature ?: return false
|
|
2281
|
+
val elapsedMs = SystemClock.uptimeMillis() - recentHandledHardwareKeyDownUptimeMs
|
|
2282
|
+
if (elapsedMs > RECENT_HANDLED_HARDWARE_KEY_DOWN_WINDOW_MS) {
|
|
2283
|
+
recentHandledHardwareKeyDownSignature = null
|
|
2284
|
+
recentHandledHardwareKeyDownUptimeMs = 0L
|
|
2285
|
+
return false
|
|
2286
|
+
}
|
|
2287
|
+
return recentSignature == signature
|
|
2288
|
+
}
|
|
2289
|
+
|
|
1011
2290
|
fun performToolbarToggleMark(markName: String) {
|
|
1012
|
-
if (!isEditable || isApplyingRustState ||
|
|
2291
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1013
2292
|
val selection = currentScalarSelection() ?: return
|
|
1014
2293
|
val updateJSON = editorToggleMarkAtSelectionScalar(
|
|
1015
2294
|
editorId.toULong(),
|
|
@@ -1021,7 +2300,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1021
2300
|
}
|
|
1022
2301
|
|
|
1023
2302
|
fun performToolbarToggleList(listType: String, isActive: Boolean) {
|
|
1024
|
-
if (!isEditable || isApplyingRustState ||
|
|
2303
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1025
2304
|
val selection = currentScalarSelection() ?: return
|
|
1026
2305
|
val updateJSON = if (isActive) {
|
|
1027
2306
|
editorUnwrapFromListAtSelectionScalar(
|
|
@@ -1041,7 +2320,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1041
2320
|
}
|
|
1042
2321
|
|
|
1043
2322
|
fun performToolbarToggleBlockquote() {
|
|
1044
|
-
if (!isEditable || isApplyingRustState ||
|
|
2323
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1045
2324
|
val selection = currentScalarSelection() ?: return
|
|
1046
2325
|
val updateJSON = editorToggleBlockquoteAtSelectionScalar(
|
|
1047
2326
|
editorId.toULong(),
|
|
@@ -1052,7 +2331,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1052
2331
|
}
|
|
1053
2332
|
|
|
1054
2333
|
fun performToolbarToggleHeading(level: Int) {
|
|
1055
|
-
if (!isEditable || isApplyingRustState ||
|
|
2334
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1056
2335
|
if (level !in 1..6) return
|
|
1057
2336
|
val selection = currentScalarSelection() ?: return
|
|
1058
2337
|
val updateJSON = editorToggleHeadingAtSelectionScalar(
|
|
@@ -1065,7 +2344,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1065
2344
|
}
|
|
1066
2345
|
|
|
1067
2346
|
fun performToolbarIndentListItem() {
|
|
1068
|
-
if (!isEditable || isApplyingRustState ||
|
|
2347
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1069
2348
|
val selection = currentScalarSelection() ?: return
|
|
1070
2349
|
val updateJSON = editorIndentListItemAtSelectionScalar(
|
|
1071
2350
|
editorId.toULong(),
|
|
@@ -1076,7 +2355,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1076
2355
|
}
|
|
1077
2356
|
|
|
1078
2357
|
fun performToolbarOutdentListItem() {
|
|
1079
|
-
if (!isEditable || isApplyingRustState ||
|
|
2358
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1080
2359
|
val selection = currentScalarSelection() ?: return
|
|
1081
2360
|
val updateJSON = editorOutdentListItemAtSelectionScalar(
|
|
1082
2361
|
editorId.toULong(),
|
|
@@ -1087,7 +2366,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1087
2366
|
}
|
|
1088
2367
|
|
|
1089
2368
|
fun performToolbarInsertNode(nodeType: String) {
|
|
1090
|
-
if (!isEditable || isApplyingRustState ||
|
|
2369
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1091
2370
|
val selection = currentScalarSelection() ?: return
|
|
1092
2371
|
val updateJSON = editorInsertNodeAtSelectionScalar(
|
|
1093
2372
|
editorId.toULong(),
|
|
@@ -1099,12 +2378,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1099
2378
|
}
|
|
1100
2379
|
|
|
1101
2380
|
fun performToolbarUndo() {
|
|
1102
|
-
if (!isEditable || isApplyingRustState ||
|
|
2381
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1103
2382
|
applyUpdateJSON(editorUndo(editorId.toULong()))
|
|
1104
2383
|
}
|
|
1105
2384
|
|
|
1106
2385
|
fun performToolbarRedo() {
|
|
1107
|
-
if (!isEditable || isApplyingRustState ||
|
|
2386
|
+
if (!isEditable || isApplyingRustState || !hasLiveEditor()) return
|
|
1108
2387
|
applyUpdateJSON(editorRedo(editorId.toULong()))
|
|
1109
2388
|
}
|
|
1110
2389
|
|
|
@@ -1117,32 +2396,52 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1117
2396
|
* falling back to plain text.
|
|
1118
2397
|
*/
|
|
1119
2398
|
override fun onTextContextMenuItem(id: Int): Boolean {
|
|
1120
|
-
if (!isEditable && id
|
|
1121
|
-
if (id == android.R.id.
|
|
1122
|
-
|
|
2399
|
+
if (!isEditable && isMutatingContextMenuItem(id)) return true
|
|
2400
|
+
if (id == android.R.id.cut) {
|
|
2401
|
+
handleCut()
|
|
2402
|
+
return true
|
|
2403
|
+
}
|
|
2404
|
+
if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) {
|
|
2405
|
+
handlePaste(plainTextOnly = id == android.R.id.pasteAsPlainText)
|
|
1123
2406
|
return true
|
|
1124
2407
|
}
|
|
1125
2408
|
return super.onTextContextMenuItem(id)
|
|
1126
2409
|
}
|
|
1127
2410
|
|
|
2411
|
+
private fun isMutatingContextMenuItem(id: Int): Boolean =
|
|
2412
|
+
id == android.R.id.paste ||
|
|
2413
|
+
id == android.R.id.pasteAsPlainText ||
|
|
2414
|
+
id == android.R.id.cut
|
|
2415
|
+
|
|
1128
2416
|
/**
|
|
1129
|
-
* Block accessibility-initiated text mutations (paste, set text) when not editable.
|
|
2417
|
+
* Block accessibility-initiated text mutations (paste, cut, set text) when not editable.
|
|
1130
2418
|
* Selection and copy actions remain available.
|
|
1131
2419
|
*/
|
|
1132
2420
|
override fun performAccessibilityAction(action: Int, arguments: android.os.Bundle?): Boolean {
|
|
1133
|
-
if (!isEditable && (
|
|
1134
|
-
|
|
2421
|
+
if (!isEditable && (
|
|
2422
|
+
action == android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT ||
|
|
2423
|
+
action == android.view.accessibility.AccessibilityNodeInfo.ACTION_PASTE ||
|
|
2424
|
+
action == android.view.accessibility.AccessibilityNodeInfo.ACTION_CUT
|
|
2425
|
+
)
|
|
2426
|
+
) {
|
|
1135
2427
|
return false
|
|
1136
2428
|
}
|
|
2429
|
+
if (action == android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT) {
|
|
2430
|
+
return handleAccessibilitySetText(arguments)
|
|
2431
|
+
}
|
|
1137
2432
|
return super.performAccessibilityAction(action, arguments)
|
|
1138
2433
|
}
|
|
1139
2434
|
|
|
1140
|
-
private fun handlePaste() {
|
|
2435
|
+
private fun handlePaste(plainTextOnly: Boolean) {
|
|
1141
2436
|
if (editorId == 0L) {
|
|
1142
2437
|
// Dev mode: default paste behavior.
|
|
1143
|
-
super.onTextContextMenuItem(
|
|
2438
|
+
super.onTextContextMenuItem(
|
|
2439
|
+
if (plainTextOnly) android.R.id.pasteAsPlainText else android.R.id.paste
|
|
2440
|
+
)
|
|
1144
2441
|
return
|
|
1145
2442
|
}
|
|
2443
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
2444
|
+
if (!prepareForExternalEditorUpdate()) return
|
|
1146
2445
|
|
|
1147
2446
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
1148
2447
|
?: return
|
|
@@ -1151,84 +2450,475 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1151
2450
|
|
|
1152
2451
|
val item = clip.getItemAt(0)
|
|
1153
2452
|
|
|
1154
|
-
// Try HTML first for rich paste.
|
|
1155
|
-
val htmlText = item.htmlText
|
|
1156
|
-
if (htmlText != null) {
|
|
1157
|
-
pasteHTML(htmlText)
|
|
2453
|
+
// Try HTML first for rich paste.
|
|
2454
|
+
val htmlText = item.htmlText
|
|
2455
|
+
if (!plainTextOnly && htmlText != null) {
|
|
2456
|
+
pasteHTML(htmlText)
|
|
2457
|
+
return
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// Fallback to plain text.
|
|
2461
|
+
val plainText = item.text?.toString() ?: item.coerceToText(context)?.toString()
|
|
2462
|
+
if (plainText != null) {
|
|
2463
|
+
pastePlainText(plainText)
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
private fun handleCut() {
|
|
2468
|
+
if (editorId == 0L) {
|
|
2469
|
+
super.onTextContextMenuItem(android.R.id.cut)
|
|
2470
|
+
return
|
|
2471
|
+
}
|
|
2472
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
2473
|
+
if (!prepareForExternalEditorUpdate()) return
|
|
2474
|
+
|
|
2475
|
+
val currentText = text?.toString() ?: return
|
|
2476
|
+
val (selectionStart, selectionEnd) = normalizedUtf16SelectionRange(currentText) ?: return
|
|
2477
|
+
if (selectionStart == selectionEnd) return
|
|
2478
|
+
|
|
2479
|
+
val (utf16Start, utf16End) = PositionBridge.snapRangeToScalarBoundaries(
|
|
2480
|
+
selectionStart,
|
|
2481
|
+
selectionEnd,
|
|
2482
|
+
currentText
|
|
2483
|
+
)
|
|
2484
|
+
if (utf16Start >= utf16End) return
|
|
2485
|
+
|
|
2486
|
+
val selectedText = currentText.substring(utf16Start, utf16End)
|
|
2487
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
2488
|
+
clipboard?.setPrimaryClip(ClipData.newPlainText(null, selectedText))
|
|
2489
|
+
|
|
2490
|
+
val scalarStart = PositionBridge.utf16ToScalar(utf16Start, currentText)
|
|
2491
|
+
val scalarEnd = PositionBridge.utf16ToScalar(utf16End, currentText)
|
|
2492
|
+
deleteRangeInRust(scalarStart, scalarEnd)
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
private fun handleAccessibilitySetText(arguments: android.os.Bundle?): Boolean {
|
|
2496
|
+
val replacement = arguments
|
|
2497
|
+
?.getCharSequence(
|
|
2498
|
+
android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE
|
|
2499
|
+
)
|
|
2500
|
+
?.toString()
|
|
2501
|
+
?: return false
|
|
2502
|
+
if (editorId == 0L) {
|
|
2503
|
+
return super.performAccessibilityAction(
|
|
2504
|
+
android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT,
|
|
2505
|
+
arguments
|
|
2506
|
+
)
|
|
2507
|
+
}
|
|
2508
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return false
|
|
2509
|
+
if (!prepareForExternalEditorUpdate()) return false
|
|
2510
|
+
|
|
2511
|
+
val currentText = text?.toString() ?: return false
|
|
2512
|
+
val scalarStart = 0
|
|
2513
|
+
val scalarEnd = currentText.codePointCount(0, currentText.length)
|
|
2514
|
+
insertPlainTextRangeInRust(scalarStart, scalarEnd, replacement)
|
|
2515
|
+
return true
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// ── Selection Change ────────────────────────────────────────────────
|
|
2519
|
+
|
|
2520
|
+
/**
|
|
2521
|
+
* Override to notify the listener when selection changes.
|
|
2522
|
+
*
|
|
2523
|
+
* Converts the EditText selection to scalar offsets and notifies both
|
|
2524
|
+
* the listener and the Rust editor.
|
|
2525
|
+
*/
|
|
2526
|
+
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
|
2527
|
+
super.onSelectionChanged(selStart, selEnd)
|
|
2528
|
+
if (isApplyingRustState) return
|
|
2529
|
+
val spannable = text as? Spanned
|
|
2530
|
+
if (spannable != null && isExactImageSpanRange(spannable, selStart, selEnd)) {
|
|
2531
|
+
explicitSelectedImageRange = ImageSelectionRange(selStart, selEnd)
|
|
2532
|
+
}
|
|
2533
|
+
ensureSelectionVisible()
|
|
2534
|
+
onSelectionOrContentMayChange?.invoke()
|
|
2535
|
+
|
|
2536
|
+
syncCurrentSelectionToRust()
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
private fun syncCurrentSelectionToRust() {
|
|
2540
|
+
if (!hasLiveEditor()) return
|
|
2541
|
+
|
|
2542
|
+
val currentText = text?.toString() ?: ""
|
|
2543
|
+
if (currentText != lastAuthorizedText) return
|
|
2544
|
+
val (scalarAnchor, scalarHead) = rawScalarSelection(currentText) ?: return
|
|
2545
|
+
|
|
2546
|
+
val selectionHook = onSetSelectionScalarInRustForTesting
|
|
2547
|
+
val docAnchor: Int
|
|
2548
|
+
val docHead: Int
|
|
2549
|
+
if (selectionHook != null) {
|
|
2550
|
+
selectionHook(scalarAnchor, scalarHead)
|
|
2551
|
+
docAnchor = scalarAnchor
|
|
2552
|
+
docHead = scalarHead
|
|
2553
|
+
} else {
|
|
2554
|
+
// Sync selection to Rust (converts scalar→doc internally).
|
|
2555
|
+
editorSetSelectionScalar(
|
|
2556
|
+
editorId.toULong(),
|
|
2557
|
+
scalarAnchor.toUInt(),
|
|
2558
|
+
scalarHead.toUInt()
|
|
2559
|
+
)
|
|
2560
|
+
|
|
2561
|
+
// Emit doc positions (not scalar offsets) to match the Selection contract.
|
|
2562
|
+
docAnchor = editorScalarToDoc(editorId.toULong(), scalarAnchor.toUInt()).toInt()
|
|
2563
|
+
docHead = editorScalarToDoc(editorId.toULong(), scalarHead.toUInt()).toInt()
|
|
2564
|
+
}
|
|
2565
|
+
editorListener?.onSelectionChanged(docAnchor, docHead)
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// ── Rust Integration ────────────────────────────────────────────────
|
|
2569
|
+
|
|
2570
|
+
// Samsung Keyboard may call finishComposingText() and then commitText(" ")
|
|
2571
|
+
// for one space tap. Defer the render from finishComposingText() by one
|
|
2572
|
+
// loop so setText() does not restart input before the pending space arrives.
|
|
2573
|
+
internal fun runWithDeferredRustUpdateApplication(block: () -> Unit) {
|
|
2574
|
+
recordImeTraceForTesting(
|
|
2575
|
+
"deferRustUpdateBegin",
|
|
2576
|
+
"depth=$deferredRustUpdateApplicationDepth pending=${deferredRustUpdateJSON != null}"
|
|
2577
|
+
)
|
|
2578
|
+
deferredRustUpdateApplicationDepth += 1
|
|
2579
|
+
try {
|
|
2580
|
+
block()
|
|
2581
|
+
} finally {
|
|
2582
|
+
deferredRustUpdateApplicationDepth -= 1
|
|
2583
|
+
recordImeTraceForTesting(
|
|
2584
|
+
"deferRustUpdateEnd",
|
|
2585
|
+
"depth=$deferredRustUpdateApplicationDepth pending=${deferredRustUpdateJSON != null}"
|
|
2586
|
+
)
|
|
2587
|
+
if (deferredRustUpdateApplicationDepth == 0) {
|
|
2588
|
+
scheduleDeferredRustUpdateApplication()
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
private fun applyRustUpdateJSON(updateJSON: String) {
|
|
2594
|
+
if (deferredRustUpdateApplicationDepth > 0) {
|
|
2595
|
+
deferredRustUpdateJSON = updateJSON
|
|
2596
|
+
recordImeTraceForTesting(
|
|
2597
|
+
"rustUpdateDeferred",
|
|
2598
|
+
"jsonLength=${updateJSON.length} depth=$deferredRustUpdateApplicationDepth"
|
|
2599
|
+
)
|
|
2600
|
+
authorizeCurrentVisibleTextForDeferredRustUpdate()
|
|
1158
2601
|
return
|
|
1159
2602
|
}
|
|
2603
|
+
cancelDeferredRustUpdateApplication()
|
|
2604
|
+
recordImeTraceForTesting(
|
|
2605
|
+
"rustUpdateApply",
|
|
2606
|
+
"mode=immediate jsonLength=${updateJSON.length}"
|
|
2607
|
+
)
|
|
2608
|
+
applyUpdateJSON(updateJSON)
|
|
2609
|
+
}
|
|
1160
2610
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
2611
|
+
private fun authorizeCurrentVisibleTextForDeferredRustUpdate() {
|
|
2612
|
+
lastAuthorizedText = text?.toString().orEmpty()
|
|
2613
|
+
lastAuthorizedRenderedText = text?.let { SpannableStringBuilder(it) }
|
|
2614
|
+
lastAuthorizedTextRevision += 1L
|
|
2615
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
2616
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1166
2617
|
}
|
|
1167
2618
|
|
|
1168
|
-
|
|
2619
|
+
internal fun authorizeCurrentVisibleTextForPendingImeOperationForEditor() {
|
|
2620
|
+
pendingOptimisticRenderText = null
|
|
2621
|
+
authorizeCurrentVisibleTextForDeferredRustUpdate()
|
|
2622
|
+
recordImeTraceForTesting(
|
|
2623
|
+
"authorizePendingImeVisibleText",
|
|
2624
|
+
"textLength=${lastAuthorizedText.length}"
|
|
2625
|
+
)
|
|
2626
|
+
}
|
|
1169
2627
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
* Converts the EditText selection to scalar offsets and notifies both
|
|
1174
|
-
* the listener and the Rust editor.
|
|
1175
|
-
*/
|
|
1176
|
-
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
|
1177
|
-
super.onSelectionChanged(selStart, selEnd)
|
|
1178
|
-
if (isApplyingRustState) return
|
|
1179
|
-
val spannable = text as? Spanned
|
|
1180
|
-
if (spannable != null && isExactImageSpanRange(spannable, selStart, selEnd)) {
|
|
1181
|
-
explicitSelectedImageRange = ImageSelectionRange(selStart, selEnd)
|
|
1182
|
-
}
|
|
1183
|
-
ensureSelectionVisible()
|
|
1184
|
-
onSelectionOrContentMayChange?.invoke()
|
|
2628
|
+
internal fun deleteScalarRangeForPendingImeOperationForEditor(scalarFrom: Int, scalarTo: Int) {
|
|
2629
|
+
deleteRangeInRust(scalarFrom, scalarTo)
|
|
2630
|
+
}
|
|
1185
2631
|
|
|
1186
|
-
|
|
2632
|
+
internal fun applyVisibleCompositionCommitForPendingImeOperationForEditor(
|
|
2633
|
+
committedText: String,
|
|
2634
|
+
replacementStartUtf16: Int,
|
|
2635
|
+
replacementEndUtf16: Int,
|
|
2636
|
+
newCursorPosition: Int
|
|
2637
|
+
): Boolean {
|
|
2638
|
+
val editable = text ?: return false
|
|
2639
|
+
val currentText = editable.toString()
|
|
2640
|
+
val (startUtf16, endUtf16) = PositionBridge.snapRangeToScalarBoundaries(
|
|
2641
|
+
replacementStartUtf16,
|
|
2642
|
+
replacementEndUtf16,
|
|
2643
|
+
currentText
|
|
2644
|
+
)
|
|
2645
|
+
if (startUtf16 > endUtf16 || endUtf16 > editable.length) return false
|
|
2646
|
+
var didApply = false
|
|
2647
|
+
runWithTransientInputMutationGuard {
|
|
2648
|
+
editable.replace(startUtf16, endUtf16, committedText)
|
|
2649
|
+
val insertedEnd = startUtf16 + committedText.length
|
|
2650
|
+
val requestedCursor = when {
|
|
2651
|
+
newCursorPosition > 0 -> insertedEnd + newCursorPosition - 1
|
|
2652
|
+
newCursorPosition < 0 -> startUtf16 + newCursorPosition
|
|
2653
|
+
else -> insertedEnd
|
|
2654
|
+
}.coerceIn(0, editable.length)
|
|
2655
|
+
Selection.setSelection(editable, requestedCursor, requestedCursor)
|
|
2656
|
+
didApply = true
|
|
2657
|
+
true
|
|
2658
|
+
}
|
|
2659
|
+
if (didApply) {
|
|
2660
|
+
pendingOptimisticRenderText = null
|
|
2661
|
+
}
|
|
2662
|
+
return didApply
|
|
2663
|
+
}
|
|
1187
2664
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
2665
|
+
internal fun commitAlreadyVisibleCompositionMutationForPendingImeOperationForEditor(
|
|
2666
|
+
committedText: String,
|
|
2667
|
+
newCursorPosition: Int
|
|
2668
|
+
): Boolean {
|
|
2669
|
+
if (committedText.isEmpty()) return false
|
|
2670
|
+
val currentText = text?.toString() ?: return false
|
|
2671
|
+
val mutation = nativeTextMutationFromAuthorizedDiff(currentText) ?: return false
|
|
2672
|
+
val tokenRange = committedTokenRangeAroundMutation(
|
|
2673
|
+
currentText,
|
|
2674
|
+
mutation.replacementStartUtf16,
|
|
2675
|
+
mutation.replacementEndUtf16
|
|
2676
|
+
) ?: run {
|
|
2677
|
+
recordImeTraceForTesting(
|
|
2678
|
+
"alreadyVisibleCompositionNoop",
|
|
2679
|
+
"reason=noToken committedLength=${committedText.length} visibleRange=${mutation.replacementStartUtf16}..${mutation.replacementEndUtf16}"
|
|
2680
|
+
)
|
|
2681
|
+
return false
|
|
2682
|
+
}
|
|
2683
|
+
val visibleToken = currentText.substring(tokenRange.first, tokenRange.second)
|
|
2684
|
+
if (visibleToken != committedText) {
|
|
2685
|
+
recordImeTraceForTesting(
|
|
2686
|
+
"alreadyVisibleCompositionNoop",
|
|
2687
|
+
"reason=tokenMismatch committedLength=${committedText.length} tokenLength=${visibleToken.length} visibleRange=${mutation.replacementStartUtf16}..${mutation.replacementEndUtf16}"
|
|
2688
|
+
)
|
|
2689
|
+
return false
|
|
2690
|
+
}
|
|
1192
2691
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
2692
|
+
val authorizedText = lastAuthorizedText
|
|
2693
|
+
val requestedCursor = requestedCursorScalar(
|
|
2694
|
+
mutation.scalarFrom,
|
|
2695
|
+
mutation.scalarTo,
|
|
2696
|
+
authorizedText,
|
|
2697
|
+
mutation.replacementText,
|
|
2698
|
+
newCursorPosition
|
|
1198
2699
|
)
|
|
2700
|
+
recordImeTraceForTesting(
|
|
2701
|
+
"alreadyVisibleCompositionApply",
|
|
2702
|
+
"range=${mutation.scalarFrom}..${mutation.scalarTo} replacementLength=${mutation.replacementText.length} committedLength=${committedText.length} requestedCursor=$requestedCursor"
|
|
2703
|
+
)
|
|
2704
|
+
pendingOptimisticRenderText = null
|
|
2705
|
+
insertPlainTextRangeInRust(
|
|
2706
|
+
mutation.scalarFrom,
|
|
2707
|
+
mutation.scalarTo,
|
|
2708
|
+
mutation.replacementText,
|
|
2709
|
+
requestedCursorScalar = requestedCursor
|
|
2710
|
+
)
|
|
2711
|
+
return true
|
|
2712
|
+
}
|
|
1199
2713
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
2714
|
+
private fun committedTokenRangeAroundMutation(
|
|
2715
|
+
currentText: String,
|
|
2716
|
+
replacementStartUtf16: Int,
|
|
2717
|
+
replacementEndUtf16: Int
|
|
2718
|
+
): Pair<Int, Int>? {
|
|
2719
|
+
if (currentText.isEmpty()) return null
|
|
2720
|
+
val start = replacementStartUtf16.coerceIn(0, currentText.length)
|
|
2721
|
+
val end = replacementEndUtf16.coerceIn(start, currentText.length)
|
|
2722
|
+
val probe = when {
|
|
2723
|
+
start < end -> start
|
|
2724
|
+
start < currentText.length -> start
|
|
2725
|
+
start > 0 -> Character.offsetByCodePoints(currentText, start, -1)
|
|
2726
|
+
else -> return null
|
|
2727
|
+
}
|
|
2728
|
+
val tokenRange = missingOldTextCorrectionTokenRange(currentText, probe) ?: return null
|
|
2729
|
+
return if (start < end) {
|
|
2730
|
+
tokenRange.takeIf { it.first <= start && it.second >= end }
|
|
2731
|
+
} else {
|
|
2732
|
+
tokenRange.takeIf { start >= it.first && start <= it.second }
|
|
2733
|
+
}
|
|
1204
2734
|
}
|
|
1205
2735
|
|
|
1206
|
-
|
|
2736
|
+
private fun scheduleDeferredRustUpdateApplication() {
|
|
2737
|
+
val pendingUpdateJSON = deferredRustUpdateJSON ?: return
|
|
2738
|
+
val generation = ++deferredRustUpdateGeneration
|
|
2739
|
+
recordImeTraceForTesting(
|
|
2740
|
+
"rustUpdateDeferredScheduled",
|
|
2741
|
+
"generation=$generation jsonLength=${pendingUpdateJSON.length}"
|
|
2742
|
+
)
|
|
2743
|
+
Handler(Looper.getMainLooper()).post {
|
|
2744
|
+
if (generation != deferredRustUpdateGeneration) {
|
|
2745
|
+
recordImeTraceForTesting(
|
|
2746
|
+
"rustUpdateDeferredSkip",
|
|
2747
|
+
"reason=generation generation=$generation current=$deferredRustUpdateGeneration"
|
|
2748
|
+
)
|
|
2749
|
+
return@post
|
|
2750
|
+
}
|
|
2751
|
+
if (deferredRustUpdateJSON != pendingUpdateJSON) {
|
|
2752
|
+
recordImeTraceForTesting("rustUpdateDeferredSkip", "reason=replaced generation=$generation")
|
|
2753
|
+
return@post
|
|
2754
|
+
}
|
|
2755
|
+
deferredRustUpdateJSON = null
|
|
2756
|
+
recordImeTraceForTesting(
|
|
2757
|
+
"rustUpdateApply",
|
|
2758
|
+
"mode=deferred generation=$generation jsonLength=${pendingUpdateJSON.length}"
|
|
2759
|
+
)
|
|
2760
|
+
applyUpdateJSON(pendingUpdateJSON)
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
private fun cancelDeferredRustUpdateApplication() {
|
|
2765
|
+
if (deferredRustUpdateJSON == null) return
|
|
2766
|
+
recordImeTraceForTesting(
|
|
2767
|
+
"rustUpdateDeferredCancel",
|
|
2768
|
+
"generation=$deferredRustUpdateGeneration"
|
|
2769
|
+
)
|
|
2770
|
+
deferredRustUpdateJSON = null
|
|
2771
|
+
deferredRustUpdateGeneration += 1L
|
|
2772
|
+
}
|
|
1207
2773
|
|
|
1208
2774
|
/**
|
|
1209
2775
|
* Insert text at a scalar position via the Rust editor.
|
|
1210
2776
|
*/
|
|
1211
2777
|
private fun insertTextInRust(text: String, atScalarPos: Int) {
|
|
2778
|
+
if (!hasLiveEditor()) return
|
|
1212
2779
|
onInsertTextInRustForTesting?.let { callback ->
|
|
1213
2780
|
callback(text, atScalarPos)
|
|
1214
2781
|
return
|
|
1215
2782
|
}
|
|
2783
|
+
val startedAt = System.nanoTime()
|
|
1216
2784
|
val updateJSON = editorInsertTextScalar(editorId.toULong(), atScalarPos.toUInt(), text)
|
|
1217
|
-
|
|
2785
|
+
recordImeTraceForTesting(
|
|
2786
|
+
"rustInsertText",
|
|
2787
|
+
"at=$atScalarPos textLength=${text.length} rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
2788
|
+
)
|
|
2789
|
+
applyRustUpdateJSON(updateJSON)
|
|
1218
2790
|
}
|
|
1219
2791
|
|
|
1220
2792
|
private fun replaceTextRangeInRust(scalarFrom: Int, scalarTo: Int, text: String) {
|
|
2793
|
+
if (!hasLiveEditor()) return
|
|
1221
2794
|
onReplaceTextInRustForTesting?.let { callback ->
|
|
1222
2795
|
callback(scalarFrom, scalarTo, text)
|
|
1223
2796
|
return
|
|
1224
2797
|
}
|
|
2798
|
+
val startedAt = System.nanoTime()
|
|
1225
2799
|
val updateJSON = editorReplaceTextScalar(
|
|
1226
2800
|
editorId.toULong(),
|
|
1227
2801
|
scalarFrom.toUInt(),
|
|
1228
2802
|
scalarTo.toUInt(),
|
|
1229
2803
|
text
|
|
1230
2804
|
)
|
|
1231
|
-
|
|
2805
|
+
recordImeTraceForTesting(
|
|
2806
|
+
"rustReplaceText",
|
|
2807
|
+
"range=$scalarFrom..$scalarTo textLength=${text.length} rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
2808
|
+
)
|
|
2809
|
+
applyRustUpdateJSON(updateJSON)
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
private fun insertPlainTextRangeInRust(
|
|
2813
|
+
scalarFrom: Int,
|
|
2814
|
+
scalarTo: Int,
|
|
2815
|
+
text: String,
|
|
2816
|
+
requestedCursorScalar: Int? = null
|
|
2817
|
+
) {
|
|
2818
|
+
if (!hasLiveEditor()) return
|
|
2819
|
+
recordImeTraceForTesting(
|
|
2820
|
+
"rustPlainTextRoute",
|
|
2821
|
+
"range=$scalarFrom..$scalarTo textLength=${text.length} requestedCursor=$requestedCursorScalar"
|
|
2822
|
+
)
|
|
2823
|
+
if (text.isEmpty()) {
|
|
2824
|
+
if (scalarFrom != scalarTo) {
|
|
2825
|
+
deleteRangeInRust(scalarFrom, scalarTo)
|
|
2826
|
+
}
|
|
2827
|
+
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2828
|
+
return
|
|
2829
|
+
}
|
|
2830
|
+
if (text.indexOf('\n') >= 0 || text.indexOf('\r') >= 0) {
|
|
2831
|
+
val docJson = plainTextDocumentFragmentJson(text)
|
|
2832
|
+
onInsertContentJsonAtSelectionScalarForTesting?.let { callback ->
|
|
2833
|
+
callback(scalarFrom, scalarTo, docJson)
|
|
2834
|
+
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2835
|
+
return
|
|
2836
|
+
}
|
|
2837
|
+
val startedAt = System.nanoTime()
|
|
2838
|
+
val updateJSON = editorInsertContentJsonAtSelectionScalar(
|
|
2839
|
+
editorId.toULong(),
|
|
2840
|
+
scalarFrom.toUInt(),
|
|
2841
|
+
scalarTo.toUInt(),
|
|
2842
|
+
docJson
|
|
2843
|
+
)
|
|
2844
|
+
recordImeTraceForTesting(
|
|
2845
|
+
"rustInsertContentJson",
|
|
2846
|
+
"range=$scalarFrom..$scalarTo textLength=${text.length} rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
2847
|
+
)
|
|
2848
|
+
applyRustUpdateJSON(updateJSON)
|
|
2849
|
+
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2850
|
+
return
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
if (scalarFrom != scalarTo) {
|
|
2854
|
+
replaceTextRangeInRust(scalarFrom, scalarTo, text)
|
|
2855
|
+
} else {
|
|
2856
|
+
insertTextInRust(text, scalarFrom)
|
|
2857
|
+
}
|
|
2858
|
+
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
private fun requestedCursorScalar(
|
|
2862
|
+
scalarFrom: Int,
|
|
2863
|
+
scalarTo: Int,
|
|
2864
|
+
currentText: String,
|
|
2865
|
+
insertedText: String,
|
|
2866
|
+
newCursorPosition: Int
|
|
2867
|
+
): Int? {
|
|
2868
|
+
if (newCursorPosition == 1) return null
|
|
2869
|
+
val insertedScalarLength = insertedText.codePointCount(0, insertedText.length)
|
|
2870
|
+
val currentScalarLength = currentText.codePointCount(0, currentText.length)
|
|
2871
|
+
val nextScalarLength =
|
|
2872
|
+
(currentScalarLength - (scalarTo - scalarFrom) + insertedScalarLength).coerceAtLeast(0)
|
|
2873
|
+
val requested = if (newCursorPosition > 0) {
|
|
2874
|
+
scalarFrom + insertedScalarLength + newCursorPosition - 1
|
|
2875
|
+
} else {
|
|
2876
|
+
scalarFrom + newCursorPosition
|
|
2877
|
+
}
|
|
2878
|
+
return requested.coerceIn(0, nextScalarLength)
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
private fun applyRequestedCursorScalar(requestedCursorScalar: Int?) {
|
|
2882
|
+
val requested = requestedCursorScalar ?: return
|
|
2883
|
+
if (!hasLiveEditor()) return
|
|
2884
|
+
val currentText = text?.toString().orEmpty()
|
|
2885
|
+
val safeScalar = requested.coerceAtLeast(0)
|
|
2886
|
+
onSetSelectionScalarInRustForTesting?.let { callback ->
|
|
2887
|
+
callback(safeScalar, safeScalar)
|
|
2888
|
+
} ?: editorSetSelectionScalar(
|
|
2889
|
+
editorId.toULong(),
|
|
2890
|
+
safeScalar.toUInt(),
|
|
2891
|
+
safeScalar.toUInt()
|
|
2892
|
+
)
|
|
2893
|
+
val localScalar = safeScalar.coerceIn(0, currentText.codePointCount(0, currentText.length))
|
|
2894
|
+
val safeUtf16 = PositionBridge.scalarToUtf16(localScalar, currentText)
|
|
2895
|
+
.coerceIn(0, currentText.length)
|
|
2896
|
+
if (selectionStart != safeUtf16 || selectionEnd != safeUtf16) {
|
|
2897
|
+
setSelection(safeUtf16, safeUtf16)
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
private fun plainTextDocumentFragmentJson(text: String): String {
|
|
2902
|
+
val normalizedText = text.replace("\r\n", "\n").replace('\r', '\n')
|
|
2903
|
+
val content = org.json.JSONArray()
|
|
2904
|
+
for (line in normalizedText.split('\n')) {
|
|
2905
|
+
val paragraph = org.json.JSONObject().put("type", "paragraph")
|
|
2906
|
+
if (line.isNotEmpty()) {
|
|
2907
|
+
paragraph.put(
|
|
2908
|
+
"content",
|
|
2909
|
+
org.json.JSONArray().put(
|
|
2910
|
+
org.json.JSONObject()
|
|
2911
|
+
.put("type", "text")
|
|
2912
|
+
.put("text", line)
|
|
2913
|
+
)
|
|
2914
|
+
)
|
|
2915
|
+
}
|
|
2916
|
+
content.put(paragraph)
|
|
2917
|
+
}
|
|
2918
|
+
return org.json.JSONObject()
|
|
2919
|
+
.put("type", "doc")
|
|
2920
|
+
.put("content", content)
|
|
2921
|
+
.toString()
|
|
1232
2922
|
}
|
|
1233
2923
|
|
|
1234
2924
|
private fun nativeTextMutationFromAuthorizedDiff(currentText: String): NativeTextMutation? {
|
|
@@ -1243,6 +2933,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1243
2933
|
) {
|
|
1244
2934
|
prefix++
|
|
1245
2935
|
}
|
|
2936
|
+
prefix = minOf(
|
|
2937
|
+
PositionBridge.snapToScalarBoundary(prefix, authorizedText, biasForward = false),
|
|
2938
|
+
PositionBridge.snapToScalarBoundary(prefix, currentText, biasForward = false)
|
|
2939
|
+
)
|
|
1246
2940
|
|
|
1247
2941
|
var authorizedEnd = authorizedText.length
|
|
1248
2942
|
var currentEnd = currentText.length
|
|
@@ -1254,41 +2948,197 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1254
2948
|
authorizedEnd--
|
|
1255
2949
|
currentEnd--
|
|
1256
2950
|
}
|
|
2951
|
+
authorizedEnd = PositionBridge.snapToScalarBoundary(
|
|
2952
|
+
authorizedEnd,
|
|
2953
|
+
authorizedText,
|
|
2954
|
+
biasForward = true
|
|
2955
|
+
)
|
|
2956
|
+
currentEnd = PositionBridge.snapToScalarBoundary(
|
|
2957
|
+
currentEnd,
|
|
2958
|
+
currentText,
|
|
2959
|
+
biasForward = true
|
|
2960
|
+
)
|
|
1257
2961
|
|
|
1258
2962
|
val replacementText = currentText.substring(prefix, currentEnd)
|
|
2963
|
+
val rawSelectionStart = selectionStart
|
|
2964
|
+
val rawSelectionEnd = selectionEnd
|
|
2965
|
+
val selectionAnchorUtf16 = rawSelectionStart
|
|
2966
|
+
.takeIf { it >= 0 }
|
|
2967
|
+
?.let { PositionBridge.snapToScalarBoundary(it, currentText, biasForward = true) }
|
|
2968
|
+
val selectionHeadUtf16 = rawSelectionEnd
|
|
2969
|
+
.takeIf { it >= 0 }
|
|
2970
|
+
?.let { PositionBridge.snapToScalarBoundary(it, currentText, biasForward = true) }
|
|
1259
2971
|
return NativeTextMutation(
|
|
1260
2972
|
scalarFrom = PositionBridge.utf16ToScalar(prefix, authorizedText),
|
|
1261
2973
|
scalarTo = PositionBridge.utf16ToScalar(authorizedEnd, authorizedText),
|
|
1262
2974
|
replacementText = replacementText,
|
|
1263
|
-
resultingText = currentText
|
|
2975
|
+
resultingText = currentText,
|
|
2976
|
+
replacementStartUtf16 = prefix,
|
|
2977
|
+
replacementEndUtf16 = currentEnd,
|
|
2978
|
+
selectionScalarAnchor = selectionAnchorUtf16?.let {
|
|
2979
|
+
PositionBridge.utf16ToScalar(it, currentText)
|
|
2980
|
+
},
|
|
2981
|
+
selectionScalarHead = selectionHeadUtf16?.let {
|
|
2982
|
+
PositionBridge.utf16ToScalar(it, currentText)
|
|
2983
|
+
}
|
|
1264
2984
|
)
|
|
1265
2985
|
}
|
|
1266
2986
|
|
|
1267
|
-
private fun shouldAdoptNativeTextMutation(
|
|
1268
|
-
|
|
1269
|
-
|
|
2987
|
+
private fun shouldAdoptNativeTextMutation(
|
|
2988
|
+
mutation: NativeTextMutation,
|
|
2989
|
+
allowAfterBlur: Boolean = false
|
|
2990
|
+
): Boolean {
|
|
2991
|
+
if (!isEditable) return false
|
|
2992
|
+
if (isNativeTextMutationAdoptionSuppressedForCurrentRevision()) return false
|
|
2993
|
+
if (!hasFocus()) {
|
|
2994
|
+
return allowAfterBlur &&
|
|
2995
|
+
canAdoptNativeTextMutationAfterBlur() &&
|
|
2996
|
+
shouldAdoptFinalNativeTextMutation(mutation)
|
|
2997
|
+
}
|
|
2998
|
+
return shouldAdoptFinalNativeTextMutation(mutation)
|
|
2999
|
+
}
|
|
1270
3000
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
3001
|
+
private fun shouldAdoptFinalNativeTextMutation(mutation: NativeTextMutation): Boolean {
|
|
3002
|
+
if (composingTextForEditor() != null) return false
|
|
3003
|
+
val trackedRange = compositionReplacementRange() ?: return true
|
|
3004
|
+
val authorizedText = lastAuthorizedText
|
|
3005
|
+
val trackedStart = PositionBridge.utf16ToScalar(trackedRange.first, authorizedText)
|
|
3006
|
+
val trackedEnd = PositionBridge.utf16ToScalar(trackedRange.second, authorizedText)
|
|
3007
|
+
if (trackedStart == trackedEnd) {
|
|
3008
|
+
return mutation.scalarFrom == trackedStart &&
|
|
3009
|
+
mutation.scalarTo == trackedStart &&
|
|
3010
|
+
mutation.replacementText.isNotEmpty()
|
|
3011
|
+
}
|
|
3012
|
+
if (mutation.scalarFrom == mutation.scalarTo) {
|
|
3013
|
+
return mutation.replacementText.isNotEmpty() &&
|
|
3014
|
+
mutation.scalarFrom >= trackedStart &&
|
|
3015
|
+
mutation.scalarFrom <= trackedEnd
|
|
3016
|
+
}
|
|
3017
|
+
return mutation.scalarFrom < trackedEnd && mutation.scalarTo > trackedStart
|
|
1274
3018
|
}
|
|
1275
3019
|
|
|
1276
|
-
private fun
|
|
1277
|
-
if (
|
|
3020
|
+
private fun drainNativeTextMutationIfNeeded(allowAfterBlur: Boolean): Boolean {
|
|
3021
|
+
if (editorId == 0L) return true
|
|
3022
|
+
if (discardTransientInputForDestroyedEditorIfNeeded()) return false
|
|
3023
|
+
val editable = text
|
|
3024
|
+
val currentText = editable?.toString() ?: ""
|
|
3025
|
+
if (currentText == lastAuthorizedText) return true
|
|
1278
3026
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
}
|
|
3027
|
+
val mutation = nativeTextMutationFromAuthorizedDiff(currentText)
|
|
3028
|
+
if (mutation != null && shouldAdoptNativeTextMutation(mutation, allowAfterBlur)) {
|
|
3029
|
+
commitNativeTextMutation(mutation)
|
|
3030
|
+
return true
|
|
3031
|
+
}
|
|
3032
|
+
recordImeTraceForTesting(
|
|
3033
|
+
"nativeMutationNoop",
|
|
3034
|
+
"reason=${if (mutation == null) "noDiffRange" else "notAdoptable"} allowAfterBlur=$allowAfterBlur currentLength=${currentText.length} authorizedLength=${lastAuthorizedText.length}"
|
|
3035
|
+
)
|
|
3036
|
+
return false
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
private fun beginNativeTextMutationAfterBlurWindow() {
|
|
3040
|
+
if (!hasLiveEditor()) {
|
|
3041
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3042
|
+
return
|
|
3043
|
+
}
|
|
3044
|
+
nativeTextMutationAfterBlurWindow = NativeTextMutationAfterBlurWindow(
|
|
3045
|
+
editorId = editorId,
|
|
3046
|
+
authorizedTextRevision = lastAuthorizedTextRevision,
|
|
3047
|
+
deadlineMs = SystemClock.uptimeMillis() + NATIVE_TEXT_MUTATION_AFTER_BLUR_WINDOW_MS
|
|
3048
|
+
)
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
private fun clearNativeTextMutationAfterBlurWindow() {
|
|
3052
|
+
nativeTextMutationAfterBlurWindow = null
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
private fun suppressNativeTextMutationAdoptionForCurrentRevision() {
|
|
3056
|
+
if (!hasLiveEditor()) {
|
|
3057
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
3058
|
+
return
|
|
3059
|
+
}
|
|
3060
|
+
nativeTextMutationAdoptionSuppression = NativeTextMutationAdoptionSuppression(
|
|
3061
|
+
editorId = editorId,
|
|
3062
|
+
authorizedTextRevision = lastAuthorizedTextRevision
|
|
3063
|
+
)
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
private fun clearNativeTextMutationAdoptionSuppression() {
|
|
3067
|
+
nativeTextMutationAdoptionSuppression = null
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
private fun isNativeTextMutationAdoptionSuppressedForCurrentRevision(): Boolean {
|
|
3071
|
+
val suppression = nativeTextMutationAdoptionSuppression ?: return false
|
|
3072
|
+
if (
|
|
3073
|
+
suppression.editorId != editorId ||
|
|
3074
|
+
suppression.authorizedTextRevision != lastAuthorizedTextRevision
|
|
3075
|
+
) {
|
|
3076
|
+
nativeTextMutationAdoptionSuppression = null
|
|
3077
|
+
return false
|
|
3078
|
+
}
|
|
3079
|
+
return true
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
private fun canAdoptNativeTextMutationAfterBlur(): Boolean {
|
|
3083
|
+
val window = nativeTextMutationAfterBlurWindow ?: return false
|
|
3084
|
+
val now = SystemClock.uptimeMillis()
|
|
3085
|
+
if (now > window.deadlineMs ||
|
|
3086
|
+
window.editorId != editorId ||
|
|
3087
|
+
window.authorizedTextRevision != lastAuthorizedTextRevision ||
|
|
3088
|
+
window.didAdoptMutation
|
|
3089
|
+
) {
|
|
3090
|
+
nativeTextMutationAfterBlurWindow = null
|
|
3091
|
+
return false
|
|
3092
|
+
}
|
|
3093
|
+
return true
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
private fun commitNativeTextMutation(mutation: NativeTextMutation) {
|
|
3097
|
+
if (!hasLiveEditor()) return
|
|
3098
|
+
val startedAt = System.nanoTime()
|
|
3099
|
+
if ((text?.toString() ?: "") != mutation.resultingText) {
|
|
3100
|
+
recordImeTraceForTesting(
|
|
3101
|
+
"nativeMutationNoop",
|
|
3102
|
+
"reason=staleResult range=${mutation.scalarFrom}..${mutation.scalarTo} replacementLength=${mutation.replacementText.length}"
|
|
3103
|
+
)
|
|
3104
|
+
return
|
|
3105
|
+
}
|
|
3106
|
+
val shouldRestartInput = hasFocus()
|
|
3107
|
+
retireInputConnectionForEditor()
|
|
3108
|
+
nativeTextMutationAfterBlurWindow?.didAdoptMutation = true
|
|
3109
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3110
|
+
|
|
3111
|
+
recordImeTraceForTesting(
|
|
3112
|
+
"nativeMutationApply",
|
|
3113
|
+
"range=${mutation.scalarFrom}..${mutation.scalarTo} replacementLength=${mutation.replacementText.length} restartInput=$shouldRestartInput"
|
|
3114
|
+
)
|
|
3115
|
+
if (mutation.replacementText.isEmpty()) {
|
|
1284
3116
|
deleteRangeInRust(mutation.scalarFrom, mutation.scalarTo)
|
|
1285
3117
|
} else {
|
|
1286
|
-
|
|
3118
|
+
insertPlainTextRangeInRust(
|
|
1287
3119
|
mutation.scalarFrom,
|
|
1288
3120
|
mutation.scalarTo,
|
|
1289
3121
|
mutation.replacementText
|
|
1290
3122
|
)
|
|
1291
3123
|
}
|
|
3124
|
+
restoreSelectionAfterNativeTextMutation(mutation)
|
|
3125
|
+
if (shouldRestartInput) {
|
|
3126
|
+
restartInputForEditor()
|
|
3127
|
+
}
|
|
3128
|
+
recordImeTraceForTesting(
|
|
3129
|
+
"nativeMutationApplyDone",
|
|
3130
|
+
"totalUs=${nanosToMicros(System.nanoTime() - startedAt)} restartInput=$shouldRestartInput"
|
|
3131
|
+
)
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
private fun restoreSelectionAfterNativeTextMutation(mutation: NativeTextMutation) {
|
|
3135
|
+
val selectionScalarAnchor = mutation.selectionScalarAnchor ?: return
|
|
3136
|
+
val selectionScalarHead = mutation.selectionScalarHead ?: return
|
|
3137
|
+
val currentText = text?.toString() ?: return
|
|
3138
|
+
val anchorUtf16 = PositionBridge.scalarToUtf16(selectionScalarAnchor, currentText)
|
|
3139
|
+
val headUtf16 = PositionBridge.scalarToUtf16(selectionScalarHead, currentText)
|
|
3140
|
+
val length = currentText.length
|
|
3141
|
+
setSelection(anchorUtf16.coerceIn(0, length), headUtf16.coerceIn(0, length))
|
|
1292
3142
|
}
|
|
1293
3143
|
|
|
1294
3144
|
/**
|
|
@@ -1298,42 +3148,271 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1298
3148
|
* @param scalarTo End scalar offset (exclusive).
|
|
1299
3149
|
*/
|
|
1300
3150
|
private fun deleteRangeInRust(scalarFrom: Int, scalarTo: Int) {
|
|
3151
|
+
if (!hasLiveEditor()) return
|
|
1301
3152
|
if (scalarFrom >= scalarTo) return
|
|
1302
3153
|
onDeleteRangeInRustForTesting?.let { callback ->
|
|
1303
3154
|
callback(scalarFrom, scalarTo)
|
|
1304
3155
|
return
|
|
1305
3156
|
}
|
|
3157
|
+
val startedAt = System.nanoTime()
|
|
1306
3158
|
val updateJSON = editorDeleteScalarRange(editorId.toULong(), scalarFrom.toUInt(), scalarTo.toUInt())
|
|
1307
|
-
|
|
3159
|
+
recordImeTraceForTesting(
|
|
3160
|
+
"rustDeleteRange",
|
|
3161
|
+
"range=$scalarFrom..$scalarTo rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3162
|
+
)
|
|
3163
|
+
applyRustUpdateJSON(updateJSON)
|
|
1308
3164
|
}
|
|
1309
3165
|
|
|
1310
3166
|
private fun deleteBackwardAtSelectionScalarInRust(scalarAnchor: Int, scalarHead: Int) {
|
|
3167
|
+
if (!hasLiveEditor()) return
|
|
1311
3168
|
onDeleteBackwardAtSelectionScalarInRustForTesting?.let { callback ->
|
|
1312
3169
|
callback(scalarAnchor, scalarHead)
|
|
1313
3170
|
return
|
|
1314
3171
|
}
|
|
3172
|
+
val startedAt = System.nanoTime()
|
|
1315
3173
|
val updateJSON = editorDeleteBackwardAtSelectionScalar(
|
|
1316
3174
|
editorId.toULong(),
|
|
1317
3175
|
scalarAnchor.toUInt(),
|
|
1318
3176
|
scalarHead.toUInt()
|
|
1319
3177
|
)
|
|
1320
|
-
|
|
3178
|
+
recordImeTraceForTesting(
|
|
3179
|
+
"rustDeleteBackward",
|
|
3180
|
+
"selection=$scalarAnchor..$scalarHead rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3181
|
+
)
|
|
3182
|
+
applyRustUpdateJSON(updateJSON)
|
|
1321
3183
|
}
|
|
1322
3184
|
|
|
1323
3185
|
/**
|
|
1324
3186
|
* Split a block at a scalar position via the Rust editor.
|
|
1325
3187
|
*/
|
|
1326
3188
|
private fun splitBlockInRust(atScalarPos: Int) {
|
|
3189
|
+
if (!hasLiveEditor()) return
|
|
3190
|
+
val startedAt = System.nanoTime()
|
|
1327
3191
|
val updateJSON = editorSplitBlockScalar(editorId.toULong(), atScalarPos.toUInt())
|
|
1328
|
-
|
|
3192
|
+
recordImeTraceForTesting(
|
|
3193
|
+
"rustSplitBlock",
|
|
3194
|
+
"at=$atScalarPos rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3195
|
+
)
|
|
3196
|
+
applyRustUpdateJSON(updateJSON)
|
|
3197
|
+
scheduleLineBoundaryInputRefreshForEditor("splitBlock")
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
private fun deleteAndSplitInRust(scalarFrom: Int, scalarTo: Int) {
|
|
3201
|
+
if (!hasLiveEditor()) return
|
|
3202
|
+
onDeleteAndSplitScalarInRustForTesting?.let { callback ->
|
|
3203
|
+
callback(scalarFrom, scalarTo)
|
|
3204
|
+
return
|
|
3205
|
+
}
|
|
3206
|
+
val startedAt = System.nanoTime()
|
|
3207
|
+
val updateJSON = editorDeleteAndSplitScalar(
|
|
3208
|
+
editorId.toULong(),
|
|
3209
|
+
scalarFrom.toUInt(),
|
|
3210
|
+
scalarTo.toUInt()
|
|
3211
|
+
)
|
|
3212
|
+
recordImeTraceForTesting(
|
|
3213
|
+
"rustDeleteAndSplit",
|
|
3214
|
+
"range=$scalarFrom..$scalarTo rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3215
|
+
)
|
|
3216
|
+
applyRustUpdateJSON(updateJSON)
|
|
3217
|
+
scheduleLineBoundaryInputRefreshForEditor("deleteAndSplit")
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
internal fun currentScalarSelection(): Pair<Int, Int>? {
|
|
3221
|
+
val currentText = text?.toString() ?: return null
|
|
3222
|
+
return normalizedScalarSelectionRange(currentText)
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
internal fun cursorCapsModeForEditor(reqModes: Int, baseCapsMode: Int): Int {
|
|
3226
|
+
val sentenceCapsMode = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
3227
|
+
if ((reqModes and sentenceCapsMode) != sentenceCapsMode) return baseCapsMode
|
|
3228
|
+
if ((baseCapsMode and sentenceCapsMode) == sentenceCapsMode) return baseCapsMode
|
|
3229
|
+
if (!isCursorAtRenderedLineStartForSentenceCaps()) return baseCapsMode
|
|
3230
|
+
return baseCapsMode or sentenceCapsMode
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
internal fun textBeforeCursorForImeContextForEditor(n: Int, flags: Int): CharSequence? {
|
|
3234
|
+
if (n <= 0) return ""
|
|
3235
|
+
val content = text ?: return null
|
|
3236
|
+
val start = selectionStart
|
|
3237
|
+
val end = selectionEnd
|
|
3238
|
+
if (start < 0 || end < 0) return null
|
|
3239
|
+
val cursor = minOf(start, end).coerceIn(0, content.length)
|
|
3240
|
+
var effectiveCursor = cursor
|
|
3241
|
+
while (
|
|
3242
|
+
effectiveCursor > 0 &&
|
|
3243
|
+
content[effectiveCursor - 1] == LayoutConstants.SYNTHETIC_PLACEHOLDER_CHARACTER[0]
|
|
3244
|
+
) {
|
|
3245
|
+
effectiveCursor -= 1
|
|
3246
|
+
}
|
|
3247
|
+
val contextStart = maxOf(0, effectiveCursor - n)
|
|
3248
|
+
val context = content.subSequence(contextStart, effectiveCursor)
|
|
3249
|
+
return if ((flags and InputConnection.GET_TEXT_WITH_STYLES) != 0) {
|
|
3250
|
+
context
|
|
3251
|
+
} else {
|
|
3252
|
+
context.toString()
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
internal fun initialSurroundingTextForImeForEditor(): ImeInitialSurroundingText? {
|
|
3257
|
+
val rawText = text?.toString() ?: return null
|
|
3258
|
+
val placeholder = LayoutConstants.SYNTHETIC_PLACEHOLDER_CHARACTER[0]
|
|
3259
|
+
if (rawText.indexOf(placeholder) < 0) return null
|
|
3260
|
+
val start = selectionStart
|
|
3261
|
+
val end = selectionEnd
|
|
3262
|
+
if (start < 0 || end < 0) return null
|
|
3263
|
+
val rawSelectionStart = start.coerceIn(0, rawText.length)
|
|
3264
|
+
val rawSelectionEnd = end.coerceIn(0, rawText.length)
|
|
3265
|
+
|
|
3266
|
+
val sanitized = StringBuilder(rawText.length)
|
|
3267
|
+
var removedCount = 0
|
|
3268
|
+
var removedBeforeSelectionStart = 0
|
|
3269
|
+
var removedBeforeSelectionEnd = 0
|
|
3270
|
+
rawText.forEachIndexed { index, ch ->
|
|
3271
|
+
if (ch == placeholder) {
|
|
3272
|
+
removedCount += 1
|
|
3273
|
+
if (index < rawSelectionStart) removedBeforeSelectionStart += 1
|
|
3274
|
+
if (index < rawSelectionEnd) removedBeforeSelectionEnd += 1
|
|
3275
|
+
} else {
|
|
3276
|
+
sanitized.append(ch)
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
return ImeInitialSurroundingText(
|
|
3281
|
+
text = sanitized.toString(),
|
|
3282
|
+
selectionStart = rawSelectionStart - removedBeforeSelectionStart,
|
|
3283
|
+
selectionEnd = rawSelectionEnd - removedBeforeSelectionEnd,
|
|
3284
|
+
originalSelectionStart = rawSelectionStart,
|
|
3285
|
+
originalSelectionEnd = rawSelectionEnd,
|
|
3286
|
+
removedPlaceholderCount = removedCount
|
|
3287
|
+
)
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
private fun isCursorAtRenderedLineStartForSentenceCaps(): Boolean {
|
|
3291
|
+
val currentText = text?.toString() ?: return false
|
|
3292
|
+
val start = selectionStart
|
|
3293
|
+
val end = selectionEnd
|
|
3294
|
+
if (start < 0 || end < 0 || start != end) return false
|
|
3295
|
+
|
|
3296
|
+
val cursor = end.coerceIn(0, currentText.length)
|
|
3297
|
+
return isRenderedLineStartForSentenceCaps(currentText, cursor)
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
private fun isRenderedLineStartForSentenceCaps(text: String, cursor: Int): Boolean {
|
|
3301
|
+
val cursor = cursor.coerceIn(0, text.length)
|
|
3302
|
+
if (cursor == 0) return true
|
|
3303
|
+
|
|
3304
|
+
val lineStart = lastRenderedLineBreakBefore(text, cursor) + 1
|
|
3305
|
+
var index = lineStart
|
|
3306
|
+
while (index < cursor && isIgnoredSentenceCapsLinePrefix(text[index])) {
|
|
3307
|
+
index += 1
|
|
3308
|
+
}
|
|
3309
|
+
if (index == cursor) return true
|
|
3310
|
+
|
|
3311
|
+
val markerEnd = renderedListMarkerEnd(text, index, cursor) ?: return false
|
|
3312
|
+
index = markerEnd
|
|
3313
|
+
while (index < cursor && isIgnoredSentenceCapsLinePrefix(text[index])) {
|
|
3314
|
+
index += 1
|
|
3315
|
+
}
|
|
3316
|
+
return index == cursor
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
private fun isSamsungKeyboardActiveForEditor(): Boolean {
|
|
3320
|
+
val inputMethodId = Settings.Secure.getString(
|
|
3321
|
+
context.contentResolver,
|
|
3322
|
+
Settings.Secure.DEFAULT_INPUT_METHOD
|
|
3323
|
+
) ?: return false
|
|
3324
|
+
return inputMethodId.contains("samsung", ignoreCase = true) ||
|
|
3325
|
+
inputMethodId.contains("honeyboard", ignoreCase = true)
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
private fun lastRenderedLineBreakBefore(text: String, cursor: Int): Int {
|
|
3329
|
+
var index = cursor.coerceAtMost(text.length) - 1
|
|
3330
|
+
while (index >= 0) {
|
|
3331
|
+
when (text[index]) {
|
|
3332
|
+
'\n', '\r' -> return index
|
|
3333
|
+
}
|
|
3334
|
+
index -= 1
|
|
3335
|
+
}
|
|
3336
|
+
return -1
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
private fun isIgnoredSentenceCapsLinePrefix(ch: Char): Boolean =
|
|
3340
|
+
ch == ' ' ||
|
|
3341
|
+
ch == '\t' ||
|
|
3342
|
+
ch == '\u00A0' ||
|
|
3343
|
+
ch == LayoutConstants.SYNTHETIC_PLACEHOLDER_CHARACTER[0]
|
|
3344
|
+
|
|
3345
|
+
private fun renderedListMarkerEnd(text: String, start: Int, endExclusive: Int): Int? {
|
|
3346
|
+
if (start >= endExclusive) return null
|
|
3347
|
+
if (text[start] == LayoutConstants.UNORDERED_LIST_BULLET[0]) {
|
|
3348
|
+
return start + 1
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
var index = start
|
|
3352
|
+
while (index < endExclusive && text[index].isDigit()) {
|
|
3353
|
+
index += 1
|
|
3354
|
+
}
|
|
3355
|
+
if (index == start || index >= endExclusive) return null
|
|
3356
|
+
return when (text[index]) {
|
|
3357
|
+
'.', ')' -> index + 1
|
|
3358
|
+
else -> null
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
private fun normalizedUtf16SelectionRange(currentText: String): Pair<Int, Int>? {
|
|
3363
|
+
val start = selectionStart
|
|
3364
|
+
val end = selectionEnd
|
|
3365
|
+
if (start < 0 || end < 0) return null
|
|
3366
|
+
val clampedStart = start.coerceIn(0, currentText.length)
|
|
3367
|
+
val clampedEnd = end.coerceIn(0, currentText.length)
|
|
3368
|
+
return minOf(clampedStart, clampedEnd) to maxOf(clampedStart, clampedEnd)
|
|
1329
3369
|
}
|
|
1330
3370
|
|
|
1331
|
-
private fun
|
|
3371
|
+
private fun normalizedUtf16SelectionRange(): Pair<Int, Int>? {
|
|
1332
3372
|
val currentText = text?.toString() ?: return null
|
|
1333
|
-
return
|
|
1334
|
-
|
|
1335
|
-
|
|
3373
|
+
return normalizedUtf16SelectionRange(currentText)
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
private fun normalizedScalarSelectionRange(currentText: String): Pair<Int, Int>? {
|
|
3377
|
+
val (start, end) = normalizedUtf16SelectionRange(currentText) ?: return null
|
|
3378
|
+
val (snappedStart, snappedEnd) = if (start == end) {
|
|
3379
|
+
val snapped = PositionBridge.snapToScalarBoundary(
|
|
3380
|
+
start,
|
|
3381
|
+
currentText,
|
|
3382
|
+
biasForward = true
|
|
3383
|
+
)
|
|
3384
|
+
snapped to snapped
|
|
3385
|
+
} else {
|
|
3386
|
+
PositionBridge.snapRangeToScalarBoundaries(start, end, currentText)
|
|
3387
|
+
}
|
|
3388
|
+
return PositionBridge.utf16ToScalar(snappedStart, currentText) to
|
|
3389
|
+
PositionBridge.utf16ToScalar(snappedEnd, currentText)
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
private fun rawScalarSelection(currentText: String): Pair<Int, Int>? {
|
|
3393
|
+
val anchor = selectionStart
|
|
3394
|
+
val head = selectionEnd
|
|
3395
|
+
if (anchor < 0 || head < 0) return null
|
|
3396
|
+
val clampedAnchor = anchor.coerceIn(0, currentText.length)
|
|
3397
|
+
val clampedHead = head.coerceIn(0, currentText.length)
|
|
3398
|
+
if (clampedAnchor == clampedHead) {
|
|
3399
|
+
val snapped = PositionBridge.snapToScalarBoundary(
|
|
3400
|
+
clampedAnchor,
|
|
3401
|
+
currentText,
|
|
3402
|
+
biasForward = true
|
|
3403
|
+
)
|
|
3404
|
+
val scalar = PositionBridge.utf16ToScalar(snapped, currentText)
|
|
3405
|
+
return scalar to scalar
|
|
3406
|
+
}
|
|
3407
|
+
val (rangeStart, rangeEnd) = PositionBridge.snapRangeToScalarBoundaries(
|
|
3408
|
+
minOf(clampedAnchor, clampedHead),
|
|
3409
|
+
maxOf(clampedAnchor, clampedHead),
|
|
3410
|
+
currentText
|
|
1336
3411
|
)
|
|
3412
|
+
val snappedAnchor = if (clampedAnchor < clampedHead) rangeStart else rangeEnd
|
|
3413
|
+
val snappedHead = if (clampedAnchor < clampedHead) rangeEnd else rangeStart
|
|
3414
|
+
return PositionBridge.utf16ToScalar(snappedAnchor, currentText) to
|
|
3415
|
+
PositionBridge.utf16ToScalar(snappedHead, currentText)
|
|
1337
3416
|
}
|
|
1338
3417
|
|
|
1339
3418
|
fun selectedImageGeometry(): SelectedImageGeometry? {
|
|
@@ -1352,7 +3431,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1352
3431
|
val textLayout = layout ?: return null
|
|
1353
3432
|
val currentText = text?.toString() ?: return null
|
|
1354
3433
|
val scalarPos = PositionBridge.utf16ToScalar(spanStart, currentText)
|
|
1355
|
-
val docPos = if (
|
|
3434
|
+
val docPos = if (hasLiveEditor()) {
|
|
1356
3435
|
editorScalarToDoc(editorId.toULong(), scalarPos.toUInt()).toInt()
|
|
1357
3436
|
} else {
|
|
1358
3437
|
0
|
|
@@ -1366,7 +3445,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1366
3445
|
}
|
|
1367
3446
|
|
|
1368
3447
|
fun resizeImageAtDocPos(docPos: Int, widthPx: Float, heightPx: Float) {
|
|
1369
|
-
if (
|
|
3448
|
+
if (!hasLiveEditor()) return
|
|
1370
3449
|
val density = resources.displayMetrics.density
|
|
1371
3450
|
val widthDp = maxOf(48, (widthPx / density).roundToInt())
|
|
1372
3451
|
val heightDp = maxOf(48, (heightPx / density).roundToInt())
|
|
@@ -1380,7 +3459,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1380
3459
|
}
|
|
1381
3460
|
|
|
1382
3461
|
private fun isSelectionInsideList(): Boolean {
|
|
1383
|
-
if (
|
|
3462
|
+
if (!hasLiveEditor()) return false
|
|
1384
3463
|
|
|
1385
3464
|
return try {
|
|
1386
3465
|
val state = org.json.JSONObject(editorGetCurrentState(editorId.toULong()))
|
|
@@ -1396,6 +3475,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1396
3475
|
* Paste HTML content through Rust.
|
|
1397
3476
|
*/
|
|
1398
3477
|
private fun pasteHTML(html: String) {
|
|
3478
|
+
if (!hasLiveEditor()) return
|
|
3479
|
+
syncCurrentSelectionToRust()
|
|
3480
|
+
onInsertContentHtmlInRustForTesting?.let { callback ->
|
|
3481
|
+
callback(html)
|
|
3482
|
+
return
|
|
3483
|
+
}
|
|
1399
3484
|
val updateJSON = editorInsertContentHtml(editorId.toULong(), html)
|
|
1400
3485
|
applyUpdateJSON(updateJSON)
|
|
1401
3486
|
}
|
|
@@ -1404,9 +3489,8 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1404
3489
|
* Paste plain text through Rust.
|
|
1405
3490
|
*/
|
|
1406
3491
|
private fun pastePlainText(text: String) {
|
|
1407
|
-
val
|
|
1408
|
-
|
|
1409
|
-
insertTextInRust(text, scalarPos)
|
|
3492
|
+
val (scalarStart, scalarEnd) = currentScalarSelection() ?: return
|
|
3493
|
+
insertPlainTextRangeInRust(scalarStart, scalarEnd, text)
|
|
1410
3494
|
}
|
|
1411
3495
|
|
|
1412
3496
|
// ── Applying Rust State ─────────────────────────────────────────────
|
|
@@ -1477,6 +3561,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1477
3561
|
replaceRange: RenderReplaceRange? = null,
|
|
1478
3562
|
usedPatch: Boolean
|
|
1479
3563
|
) {
|
|
3564
|
+
val startedAt = System.nanoTime()
|
|
3565
|
+
val previousScrollX = scrollX
|
|
3566
|
+
val previousScrollY = scrollY
|
|
3567
|
+
val hadCompositionTracking = hasCompositionTrackingForEditor()
|
|
3568
|
+
var shouldRestartInput = false
|
|
3569
|
+
val mode = if (replaceRange != null) "replace" else "setText"
|
|
1480
3570
|
isApplyingRustState = true
|
|
1481
3571
|
beginBatchEdit()
|
|
1482
3572
|
try {
|
|
@@ -1486,11 +3576,43 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1486
3576
|
setText(spannable)
|
|
1487
3577
|
}
|
|
1488
3578
|
lastAuthorizedText = text?.toString().orEmpty()
|
|
3579
|
+
lastAuthorizedRenderedText = text?.let { SpannableStringBuilder(it) }
|
|
3580
|
+
lastAuthorizedTextRevision += 1L
|
|
3581
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
3582
|
+
if (hadCompositionTracking) {
|
|
3583
|
+
retireInputConnectionForEditor()
|
|
3584
|
+
shouldRestartInput = true
|
|
3585
|
+
} else {
|
|
3586
|
+
clearCompositionTrackingForEditor()
|
|
3587
|
+
}
|
|
1489
3588
|
lastRenderAppliedPatchForTesting = usedPatch
|
|
3589
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1490
3590
|
} finally {
|
|
1491
3591
|
endBatchEdit()
|
|
1492
3592
|
isApplyingRustState = false
|
|
1493
3593
|
}
|
|
3594
|
+
recordImeTraceForTesting(
|
|
3595
|
+
"applyRenderedSpannable",
|
|
3596
|
+
"mode=$mode usedPatch=$usedPatch incomingLength=${spannable.length} replace=${replaceRange?.start}..${replaceRange?.endExclusive} hadComposition=$hadCompositionTracking restartInput=$shouldRestartInput applyUs=${nanosToMicros(System.nanoTime() - startedAt)} scroll=$previousScrollX,$previousScrollY->$scrollX,$scrollY layout=${layout != null}"
|
|
3597
|
+
)
|
|
3598
|
+
restartInputAfterCompositionInvalidationIfNeeded(shouldRestartInput)
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
private fun authorizeVisibleTextForMatchedOptimisticRender(spannable: CharSequence) {
|
|
3602
|
+
val startedAt = System.nanoTime()
|
|
3603
|
+
val visibleText = text?.toString().orEmpty()
|
|
3604
|
+
lastAuthorizedText = visibleText
|
|
3605
|
+
lastAuthorizedRenderedText = text?.let { SpannableStringBuilder(it) }
|
|
3606
|
+
?: SpannableStringBuilder(spannable)
|
|
3607
|
+
lastAuthorizedTextRevision += 1L
|
|
3608
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
3609
|
+
clearCompositionTrackingForEditor()
|
|
3610
|
+
lastRenderAppliedPatchForTesting = false
|
|
3611
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
3612
|
+
recordImeTraceForTesting(
|
|
3613
|
+
"reuseOptimisticVisibleTextRender",
|
|
3614
|
+
"textLength=${visibleText.length} applyUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
3615
|
+
)
|
|
1494
3616
|
}
|
|
1495
3617
|
|
|
1496
3618
|
private fun buildPatchedSpannable(patch: ParsedRenderPatch): android.text.SpannableStringBuilder =
|
|
@@ -1662,9 +3784,14 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1662
3784
|
val parseStartedAt = totalStartedAt
|
|
1663
3785
|
val update = try {
|
|
1664
3786
|
org.json.JSONObject(updateJSON)
|
|
1665
|
-
} catch (
|
|
3787
|
+
} catch (error: Exception) {
|
|
3788
|
+
recordImeTraceForTesting(
|
|
3789
|
+
"applyUpdateJSONNoop",
|
|
3790
|
+
"reason=parseError jsonLength=${updateJSON.length} error=${error.javaClass.simpleName}"
|
|
3791
|
+
)
|
|
1666
3792
|
return
|
|
1667
3793
|
}
|
|
3794
|
+
cancelDeferredRustUpdateApplication()
|
|
1668
3795
|
val parseNanos = System.nanoTime() - parseStartedAt
|
|
1669
3796
|
|
|
1670
3797
|
val resolveRenderBlocksStartedAt = System.nanoTime()
|
|
@@ -1689,8 +3816,11 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1689
3816
|
val buildRenderNanos: Long
|
|
1690
3817
|
val applyRenderNanos: Long
|
|
1691
3818
|
if (shouldSkipRender) {
|
|
3819
|
+
pendingOptimisticRenderText = null
|
|
1692
3820
|
lastRenderAppliedPatchForTesting = false
|
|
1693
3821
|
currentRenderBlocksJson = resolvedRenderBlocks?.let(::cloneJsonArray)
|
|
3822
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
3823
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
1694
3824
|
buildRenderNanos = 0L
|
|
1695
3825
|
applyRenderNanos = 0L
|
|
1696
3826
|
} else {
|
|
@@ -1718,12 +3848,27 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1718
3848
|
this
|
|
1719
3849
|
)
|
|
1720
3850
|
} else {
|
|
3851
|
+
recordImeTraceForTesting(
|
|
3852
|
+
"applyUpdateJSONNoop",
|
|
3853
|
+
"reason=noRenderPayload jsonLength=${updateJSON.length}"
|
|
3854
|
+
)
|
|
1721
3855
|
return
|
|
1722
3856
|
}
|
|
1723
3857
|
buildRenderNanos = System.nanoTime() - buildStartedAt
|
|
1724
3858
|
currentRenderBlocksJson = resolvedRenderBlocks?.let(::cloneJsonArray)
|
|
1725
3859
|
val applyStartedAt = System.nanoTime()
|
|
1726
|
-
|
|
3860
|
+
val optimisticText = pendingOptimisticRenderText
|
|
3861
|
+
val canReuseOptimisticVisibleText =
|
|
3862
|
+
optimisticText != null &&
|
|
3863
|
+
text?.toString() == optimisticText &&
|
|
3864
|
+
fullSpannable.toString() == optimisticText &&
|
|
3865
|
+
!spannedContainsImageSpan(fullSpannable)
|
|
3866
|
+
if (canReuseOptimisticVisibleText) {
|
|
3867
|
+
authorizeVisibleTextForMatchedOptimisticRender(fullSpannable)
|
|
3868
|
+
} else {
|
|
3869
|
+
applyRenderedSpannable(fullSpannable, usedPatch = false)
|
|
3870
|
+
}
|
|
3871
|
+
pendingOptimisticRenderText = null
|
|
1727
3872
|
applyRenderNanos = System.nanoTime() - applyStartedAt
|
|
1728
3873
|
lastAppliedRenderAppearanceRevision = renderAppearanceRevision
|
|
1729
3874
|
}
|
|
@@ -1748,6 +3893,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1748
3893
|
}
|
|
1749
3894
|
val postApplyNanos = System.nanoTime() - postApplyStartedAt
|
|
1750
3895
|
|
|
3896
|
+
val totalNanos = System.nanoTime() - totalStartedAt
|
|
3897
|
+
recordImeTraceForTesting(
|
|
3898
|
+
"applyUpdateJSON",
|
|
3899
|
+
"notify=$notifyListener skippedRender=$shouldSkipRender attemptedPatch=${renderPatch != null} jsonLength=${updateJSON.length} parseUs=${nanosToMicros(parseNanos)} resolveUs=${nanosToMicros(resolveRenderBlocksNanos)} buildUs=${nanosToMicros(buildRenderNanos)} applyUs=${nanosToMicros(applyRenderNanos)} selectionUs=${nanosToMicros(selectionNanos)} postUs=${nanosToMicros(postApplyNanos)} totalUs=${nanosToMicros(totalNanos)}"
|
|
3900
|
+
)
|
|
3901
|
+
|
|
1751
3902
|
if (captureApplyUpdateTraceForTesting) {
|
|
1752
3903
|
lastApplyUpdateTraceForTesting = ApplyUpdateTrace(
|
|
1753
3904
|
attemptedPatch = renderPatch != null,
|
|
@@ -1760,7 +3911,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1760
3911
|
applyRenderNanos = applyRenderNanos,
|
|
1761
3912
|
selectionNanos = selectionNanos,
|
|
1762
3913
|
postApplyNanos = postApplyNanos,
|
|
1763
|
-
totalNanos =
|
|
3914
|
+
totalNanos = totalNanos
|
|
1764
3915
|
)
|
|
1765
3916
|
}
|
|
1766
3917
|
}
|
|
@@ -1774,6 +3925,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1774
3925
|
* @param renderJSON The JSON array string of render elements.
|
|
1775
3926
|
*/
|
|
1776
3927
|
fun applyRenderJSON(renderJSON: String) {
|
|
3928
|
+
val startedAt = System.nanoTime()
|
|
1777
3929
|
val spannable = RenderBridge.buildSpannable(
|
|
1778
3930
|
renderJSON,
|
|
1779
3931
|
baseFontSize,
|
|
@@ -1788,6 +3940,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1788
3940
|
|
|
1789
3941
|
explicitSelectedImageRange = null
|
|
1790
3942
|
currentRenderBlocksJson = null
|
|
3943
|
+
pendingOptimisticRenderText = null
|
|
1791
3944
|
applyRenderedSpannable(spannable, usedPatch = false)
|
|
1792
3945
|
onSelectionOrContentMayChange?.invoke()
|
|
1793
3946
|
if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
@@ -1795,6 +3948,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1795
3948
|
} else {
|
|
1796
3949
|
preserveScrollPosition(previousScrollX, previousScrollY)
|
|
1797
3950
|
}
|
|
3951
|
+
recordImeTraceForTesting(
|
|
3952
|
+
"applyRenderJSON",
|
|
3953
|
+
"jsonLength=${renderJSON.length} totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
3954
|
+
)
|
|
1798
3955
|
}
|
|
1799
3956
|
|
|
1800
3957
|
private fun textOffsetHitAt(x: Float, y: Float): Pair<Spanned, Int>? {
|
|
@@ -1911,7 +4068,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1911
4068
|
|
|
1912
4069
|
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
|
|
1913
4070
|
super.onFocusChanged(focused, direction, previouslyFocusedRect)
|
|
1914
|
-
if (
|
|
4071
|
+
if (focused) {
|
|
4072
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
4073
|
+
} else {
|
|
4074
|
+
beginNativeTextMutationAfterBlurWindow()
|
|
1915
4075
|
clearExplicitSelectedImageRange()
|
|
1916
4076
|
}
|
|
1917
4077
|
}
|
|
@@ -2018,6 +4178,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2018
4178
|
*/
|
|
2019
4179
|
private fun applySelectionFromJSON(selection: org.json.JSONObject) {
|
|
2020
4180
|
val type = selection.optString("type", "") ?: return
|
|
4181
|
+
if (isEditorDestroyedForInput()) return
|
|
2021
4182
|
|
|
2022
4183
|
isApplyingRustState = true
|
|
2023
4184
|
try {
|
|
@@ -2029,12 +4190,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2029
4190
|
// Convert doc positions to scalar offsets.
|
|
2030
4191
|
val scalarAnchor = editorDocToScalar(editorId.toULong(), docAnchor.toUInt()).toInt()
|
|
2031
4192
|
val scalarHead = editorDocToScalar(editorId.toULong(), docHead.toUInt()).toInt()
|
|
2032
|
-
val
|
|
2033
|
-
val
|
|
4193
|
+
val anchorUtf16 = PositionBridge.scalarToUtf16(scalarAnchor, currentText)
|
|
4194
|
+
val headUtf16 = PositionBridge.scalarToUtf16(scalarHead, currentText)
|
|
2034
4195
|
val len = text?.length ?: 0
|
|
2035
4196
|
setSelection(
|
|
2036
|
-
|
|
2037
|
-
|
|
4197
|
+
anchorUtf16.coerceIn(0, len),
|
|
4198
|
+
headUtf16.coerceIn(0, len)
|
|
2038
4199
|
)
|
|
2039
4200
|
}
|
|
2040
4201
|
"node" -> {
|
|
@@ -2081,13 +4242,13 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2081
4242
|
|
|
2082
4243
|
override fun afterTextChanged(s: Editable?) {
|
|
2083
4244
|
if (isApplyingRustState) return
|
|
2084
|
-
if (
|
|
4245
|
+
if (!hasLiveEditor()) return
|
|
2085
4246
|
|
|
2086
4247
|
val currentText = s?.toString() ?: ""
|
|
2087
4248
|
if (currentText == lastAuthorizedText) return
|
|
2088
4249
|
|
|
2089
4250
|
val mutation = nativeTextMutationFromAuthorizedDiff(currentText)
|
|
2090
|
-
if (mutation != null && shouldAdoptNativeTextMutation(
|
|
4251
|
+
if (mutation != null && shouldAdoptNativeTextMutation(mutation, allowAfterBlur = true)) {
|
|
2091
4252
|
commitNativeTextMutation(mutation)
|
|
2092
4253
|
return
|
|
2093
4254
|
}
|
|
@@ -2113,6 +4274,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2113
4274
|
private const val DEFAULT_AUTO_CORRECT = true
|
|
2114
4275
|
private const val DEFAULT_KEYBOARD_TYPE = "default"
|
|
2115
4276
|
private const val EMPTY_BLOCK_PLACEHOLDER = '\u200B'
|
|
4277
|
+
private const val IME_TRACE_LIMIT_FOR_TESTING = 80
|
|
4278
|
+
private const val IME_TRACE_LOG_TAG = "NativeEditorIme"
|
|
4279
|
+
private const val NATIVE_TEXT_MUTATION_AFTER_BLUR_WINDOW_MS = 750L
|
|
4280
|
+
private const val RECENT_HANDLED_HARDWARE_KEY_DOWN_WINDOW_MS = 750L
|
|
2116
4281
|
private const val LOG_TAG = "NativeEditor"
|
|
2117
4282
|
}
|
|
2118
4283
|
}
|