@apollohg/react-native-prose-editor 0.5.17 → 0.5.19
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 +950 -38
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +408 -33
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +104 -5
- package/dist/NativeRichTextEditor.js +37 -2
- package/ios/EditorAddons.swift +78 -6
- 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 +127 -18
- package/ios/RenderBridge.swift +3 -1
- package/ios/RichTextEditorView.swift +211 -14
- package/package.json +1 -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
|
@@ -6,16 +6,28 @@ import android.content.Context
|
|
|
6
6
|
import android.graphics.Typeface
|
|
7
7
|
import android.graphics.Rect
|
|
8
8
|
import android.graphics.RectF
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import android.os.Handler
|
|
11
|
+
import android.os.Looper
|
|
9
12
|
import android.os.SystemClock
|
|
13
|
+
import android.provider.Settings
|
|
10
14
|
import android.text.Annotation
|
|
11
15
|
import android.text.Editable
|
|
12
16
|
import android.text.InputType
|
|
13
17
|
import android.text.Layout
|
|
18
|
+
import android.text.Selection
|
|
14
19
|
import android.text.Spanned
|
|
15
20
|
import android.text.SpannableStringBuilder
|
|
16
21
|
import android.text.StaticLayout
|
|
17
22
|
import android.text.TextPaint
|
|
18
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
|
|
19
31
|
import android.util.AttributeSet
|
|
20
32
|
import android.util.Log
|
|
21
33
|
import android.util.TypedValue
|
|
@@ -73,6 +85,15 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
73
85
|
val totalNanos: Long
|
|
74
86
|
)
|
|
75
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
|
+
|
|
76
97
|
data class SelectedImageGeometry(
|
|
77
98
|
val docPos: Int,
|
|
78
99
|
val rect: RectF
|
|
@@ -127,6 +148,8 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
127
148
|
val scalarTo: Int,
|
|
128
149
|
val replacementText: String,
|
|
129
150
|
val resultingText: String,
|
|
151
|
+
val replacementStartUtf16: Int,
|
|
152
|
+
val replacementEndUtf16: Int,
|
|
130
153
|
val selectionScalarAnchor: Int?,
|
|
131
154
|
val selectionScalarHead: Int?
|
|
132
155
|
)
|
|
@@ -143,6 +166,24 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
143
166
|
val authorizedTextRevision: Long
|
|
144
167
|
)
|
|
145
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
|
+
|
|
146
187
|
/**
|
|
147
188
|
* Listener interface for editor events, parallel to iOS's EditorTextViewDelegate.
|
|
148
189
|
*/
|
|
@@ -246,9 +287,17 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
246
287
|
internal var captureApplyUpdateTraceForTesting: Boolean = false
|
|
247
288
|
private var lastApplyUpdateTraceForTesting: ApplyUpdateTrace? = null
|
|
248
289
|
private val imeTraceForTesting = java.util.ArrayDeque<String>()
|
|
290
|
+
private var imeTraceSequence: Long = 0L
|
|
291
|
+
private var lastImeTraceUptimeMs: Long = 0L
|
|
249
292
|
private var currentRenderBlocksJson: org.json.JSONArray? = null
|
|
250
293
|
private var renderAppearanceRevision: Long = 1L
|
|
251
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
|
|
252
301
|
internal var onDeleteRangeInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
253
302
|
internal var onDeleteBackwardAtSelectionScalarInRustForTesting: ((Int, Int) -> Unit)? = null
|
|
254
303
|
internal var onInsertTextInRustForTesting: ((String, Int) -> Unit)? = null
|
|
@@ -262,6 +311,11 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
262
311
|
|
|
263
312
|
fun lastRenderAppliedPatch(): Boolean = lastRenderAppliedPatchForTesting
|
|
264
313
|
fun lastApplyUpdateTrace(): ApplyUpdateTrace? = lastApplyUpdateTraceForTesting
|
|
314
|
+
internal fun hasDeferredRustUpdateApplicationForTesting(): Boolean = deferredRustUpdateJSON != null
|
|
315
|
+
|
|
316
|
+
internal fun applyRustUpdateJSONForTesting(updateJSON: String) {
|
|
317
|
+
applyRustUpdateJSON(updateJSON)
|
|
318
|
+
}
|
|
265
319
|
|
|
266
320
|
internal fun recordImeTraceForTesting(event: String, details: String = "") {
|
|
267
321
|
if (imeTraceForTesting.size >= IME_TRACE_LIMIT_FOR_TESTING) {
|
|
@@ -270,15 +324,55 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
270
324
|
imeTraceForTesting.addLast(
|
|
271
325
|
if (details.isEmpty()) event else "$event:$details"
|
|
272
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
|
+
}
|
|
273
363
|
}
|
|
274
364
|
|
|
275
365
|
internal fun clearImeTraceForTesting() {
|
|
276
366
|
imeTraceForTesting.clear()
|
|
367
|
+
imeTraceSequence = 0L
|
|
368
|
+
lastImeTraceUptimeMs = 0L
|
|
277
369
|
}
|
|
278
370
|
|
|
279
371
|
internal fun imeTraceSnapshotForTesting(): List<String> =
|
|
280
372
|
imeTraceForTesting.toList()
|
|
281
373
|
|
|
374
|
+
private fun nanosToMicros(nanos: Long): Long = nanos / 1_000L
|
|
375
|
+
|
|
282
376
|
init {
|
|
283
377
|
// Configure for rich text editing.
|
|
284
378
|
inputType = resolvedInputType()
|
|
@@ -417,12 +511,64 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
417
511
|
*/
|
|
418
512
|
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
|
419
513
|
val baseConnection = super.onCreateInputConnection(outAttrs) ?: return null
|
|
514
|
+
val originalInitialCapsMode = outAttrs.initialCapsMode
|
|
515
|
+
outAttrs.initialCapsMode = cursorCapsModeForEditor(
|
|
516
|
+
reqModes = outAttrs.inputType,
|
|
517
|
+
baseCapsMode = outAttrs.initialCapsMode
|
|
518
|
+
)
|
|
519
|
+
val initialSurroundingText = applyInitialSurroundingTextForIme(outAttrs)
|
|
420
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
|
+
)
|
|
421
529
|
return EditorInputConnection(this, baseConnection, editorId, generation).also {
|
|
422
530
|
activeInputConnection = it
|
|
423
531
|
}
|
|
424
532
|
}
|
|
425
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()
|
|
570
|
+
}
|
|
571
|
+
|
|
426
572
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
|
427
573
|
if (!isEditable && isReadOnlyTextMutationKeyEvent(event)) {
|
|
428
574
|
return true
|
|
@@ -824,40 +970,203 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
824
970
|
* the Rust editor instead of directly inserting into the EditText.
|
|
825
971
|
*/
|
|
826
972
|
fun handleTextCommit(text: String, newCursorPosition: Int = 1) {
|
|
827
|
-
|
|
828
|
-
if (
|
|
829
|
-
|
|
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
|
+
}
|
|
830
987
|
if (editorId == 0L) {
|
|
831
988
|
// No Rust editor bound — fall through to direct editing (dev mode).
|
|
832
989
|
val editable = this.text ?: return
|
|
833
990
|
val (start, end) = selectionRange
|
|
834
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}")
|
|
835
1000
|
return
|
|
836
1001
|
}
|
|
837
|
-
if (discardTransientInputForDestroyedEditorIfNeeded()) return
|
|
838
1002
|
|
|
839
1003
|
// Handle Enter/Return as a block split operation.
|
|
840
1004
|
if (text == "\n") {
|
|
1005
|
+
recordImeTraceForTesting(
|
|
1006
|
+
"handleTextCommit",
|
|
1007
|
+
"route=return utf16Sel=${selectionRange.first}..${selectionRange.second}"
|
|
1008
|
+
)
|
|
841
1009
|
handleReturnKey()
|
|
1010
|
+
recordImeTraceForTesting(
|
|
1011
|
+
"handleTextCommitDone",
|
|
1012
|
+
"route=return totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1013
|
+
)
|
|
842
1014
|
return
|
|
843
1015
|
}
|
|
844
1016
|
|
|
845
1017
|
val currentText = this.text?.toString() ?: ""
|
|
846
|
-
val
|
|
847
|
-
|
|
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(
|
|
848
1025
|
scalarStart,
|
|
849
1026
|
scalarEnd,
|
|
1027
|
+
currentText,
|
|
850
1028
|
text,
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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}"
|
|
857
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)}"
|
|
858
1056
|
)
|
|
859
1057
|
}
|
|
860
1058
|
|
|
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
|
+
)
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
861
1170
|
internal fun runWithTransientInputMutationGuard(block: () -> Boolean): Boolean {
|
|
862
1171
|
val wasApplyingRustState = isApplyingRustState
|
|
863
1172
|
isApplyingRustState = true
|
|
@@ -942,6 +1251,83 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
942
1251
|
|
|
943
1252
|
internal fun composingTextForEditor(): String? = composingText
|
|
944
1253
|
|
|
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
|
|
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
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
internal fun applyTransientComposingTextStyleForEditor() {
|
|
1280
|
+
val editable = text ?: return
|
|
1281
|
+
removeTransientComposingTextStyleSpans(editable)
|
|
1282
|
+
|
|
1283
|
+
val start = BaseInputConnection.getComposingSpanStart(editable)
|
|
1284
|
+
val end = BaseInputConnection.getComposingSpanEnd(editable)
|
|
1285
|
+
if (start < 0 || end < 0 || start >= end || end > editable.length) return
|
|
1286
|
+
|
|
1287
|
+
val textStyle = theme?.effectiveTextStyle("paragraph")
|
|
1288
|
+
val resolvedTextSize = textStyle?.fontSize?.times(resources.displayMetrics.density) ?: baseFontSize
|
|
1289
|
+
val resolvedTextColor = textStyle?.color ?: baseTextColor
|
|
1290
|
+
|
|
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
|
+
|
|
945
1331
|
internal fun composingTextFromVisibleReplacementForEditor(): String? {
|
|
946
1332
|
val (start, end) = compositionReplacementRange() ?: return null
|
|
947
1333
|
val authorizedText = lastAuthorizedText
|
|
@@ -972,6 +1358,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
972
1358
|
composingReplacementAuthorizedTextRevision != null
|
|
973
1359
|
|
|
974
1360
|
private fun retireInputConnectionForEditor() {
|
|
1361
|
+
recordImeTraceForTesting("retireInputConnection")
|
|
975
1362
|
activeInputConnection?.clearCompositionTrackingForEditor()
|
|
976
1363
|
invalidateInputConnectionsForEditor()
|
|
977
1364
|
clearCompositionTrackingForEditor()
|
|
@@ -1016,18 +1403,63 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1016
1403
|
|
|
1017
1404
|
private fun restartInputAfterCompositionInvalidationIfNeeded(shouldRestart: Boolean) {
|
|
1018
1405
|
if (!shouldRestart) return
|
|
1019
|
-
restartInputForEditorIfFocused()
|
|
1406
|
+
restartInputForEditorIfFocused("focused")
|
|
1020
1407
|
}
|
|
1021
1408
|
|
|
1022
|
-
private fun restartInputForEditorIfFocused() {
|
|
1409
|
+
private fun restartInputForEditorIfFocused(source: String) {
|
|
1023
1410
|
if (!hasFocus()) return
|
|
1024
|
-
|
|
1025
|
-
imm?.restartInput(this)
|
|
1411
|
+
restartInputForEditor(source)
|
|
1026
1412
|
}
|
|
1027
1413
|
|
|
1028
|
-
private fun restartInputForEditor() {
|
|
1414
|
+
private fun restartInputForEditor(source: String = "explicit") {
|
|
1415
|
+
recordImeTraceForTesting("restartInput", "source=$source")
|
|
1029
1416
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1030
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
|
+
}
|
|
1031
1463
|
}
|
|
1032
1464
|
|
|
1033
1465
|
private fun clearCompositionInvalidationForEditor() {
|
|
@@ -1035,7 +1467,6 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1035
1467
|
}
|
|
1036
1468
|
|
|
1037
1469
|
private fun nextInputConnectionGenerationForEditor(): Long {
|
|
1038
|
-
inputConnectionGeneration += 1L
|
|
1039
1470
|
return inputConnectionGeneration
|
|
1040
1471
|
}
|
|
1041
1472
|
|
|
@@ -1049,12 +1480,14 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1049
1480
|
|
|
1050
1481
|
private fun invalidateInputConnectionsForEditor() {
|
|
1051
1482
|
inputConnectionGeneration += 1L
|
|
1483
|
+
recordImeTraceForTesting("invalidateInputConnections", "nextGen=$inputConnectionGeneration")
|
|
1052
1484
|
activeInputConnection = null
|
|
1053
1485
|
}
|
|
1054
1486
|
|
|
1055
1487
|
private fun clearNativeComposingSpans() {
|
|
1056
1488
|
val editable = text ?: return
|
|
1057
1489
|
BaseInputConnection.removeComposingSpans(editable)
|
|
1490
|
+
removeTransientComposingTextStyleSpans(editable)
|
|
1058
1491
|
}
|
|
1059
1492
|
|
|
1060
1493
|
internal fun restoreAuthorizedTextIfNeeded() {
|
|
@@ -1119,9 +1552,19 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1119
1552
|
replacementEndUtf16: Int,
|
|
1120
1553
|
newCursorPosition: Int = 1
|
|
1121
1554
|
) {
|
|
1122
|
-
|
|
1123
|
-
if (
|
|
1124
|
-
|
|
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
|
+
}
|
|
1125
1568
|
|
|
1126
1569
|
val authorizedText = lastAuthorizedText
|
|
1127
1570
|
val (startUtf16, endUtf16) = PositionBridge.snapRangeToScalarBoundaries(
|
|
@@ -1144,31 +1587,52 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
1144
1587
|
text,
|
|
1145
1588
|
newCursorPosition
|
|
1146
1589
|
) ?: scalarEnd
|
|
1590
|
+
recordImeTraceForTesting(
|
|
1591
|
+
"handleCompositionCommitNoop",
|
|
1592
|
+
"reason=alreadyAuthorized textLength=${text.length} requestedCursor=$requestedCursor range=$startUtf16..$endUtf16"
|
|
1593
|
+
)
|
|
1147
1594
|
restoreAuthorizedTextIfNeeded()
|
|
1148
1595
|
applyRequestedCursorScalar(requestedCursor)
|
|
1149
1596
|
return
|
|
1150
1597
|
}
|
|
1151
1598
|
|
|
1152
1599
|
if (text == "\n") {
|
|
1600
|
+
recordImeTraceForTesting(
|
|
1601
|
+
"handleCompositionCommit",
|
|
1602
|
+
"route=return textLength=${text.length} utf16Range=$startUtf16..$endUtf16 scalarRange=$scalarStart..$scalarEnd"
|
|
1603
|
+
)
|
|
1153
1604
|
if (scalarStart != scalarEnd) {
|
|
1154
1605
|
deleteAndSplitInRust(scalarStart, scalarEnd)
|
|
1155
1606
|
} else {
|
|
1156
1607
|
splitBlockInRust(scalarStart)
|
|
1157
1608
|
}
|
|
1609
|
+
recordImeTraceForTesting(
|
|
1610
|
+
"handleCompositionCommitDone",
|
|
1611
|
+
"route=return totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1612
|
+
)
|
|
1158
1613
|
return
|
|
1159
1614
|
}
|
|
1160
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
|
+
)
|
|
1161
1627
|
insertPlainTextRangeInRust(
|
|
1162
1628
|
scalarStart,
|
|
1163
1629
|
scalarEnd,
|
|
1164
1630
|
text,
|
|
1165
|
-
requestedCursorScalar =
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
newCursorPosition
|
|
1171
|
-
)
|
|
1631
|
+
requestedCursorScalar = requestedCursor
|
|
1632
|
+
)
|
|
1633
|
+
recordImeTraceForTesting(
|
|
1634
|
+
"handleCompositionCommitDone",
|
|
1635
|
+
"textLength=${text.length} totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1172
1636
|
)
|
|
1173
1637
|
}
|
|
1174
1638
|
|
|
@@ -2103,6 +2567,210 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2103
2567
|
|
|
2104
2568
|
// ── Rust Integration ────────────────────────────────────────────────
|
|
2105
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()
|
|
2601
|
+
return
|
|
2602
|
+
}
|
|
2603
|
+
cancelDeferredRustUpdateApplication()
|
|
2604
|
+
recordImeTraceForTesting(
|
|
2605
|
+
"rustUpdateApply",
|
|
2606
|
+
"mode=immediate jsonLength=${updateJSON.length}"
|
|
2607
|
+
)
|
|
2608
|
+
applyUpdateJSON(updateJSON)
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
private fun authorizeCurrentVisibleTextForDeferredRustUpdate() {
|
|
2612
|
+
lastAuthorizedText = text?.toString().orEmpty()
|
|
2613
|
+
lastAuthorizedRenderedText = text?.let { SpannableStringBuilder(it) }
|
|
2614
|
+
lastAuthorizedTextRevision += 1L
|
|
2615
|
+
clearNativeTextMutationAdoptionSuppression()
|
|
2616
|
+
clearNativeTextMutationAfterBlurWindow()
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
internal fun authorizeCurrentVisibleTextForPendingImeOperationForEditor() {
|
|
2620
|
+
pendingOptimisticRenderText = null
|
|
2621
|
+
authorizeCurrentVisibleTextForDeferredRustUpdate()
|
|
2622
|
+
recordImeTraceForTesting(
|
|
2623
|
+
"authorizePendingImeVisibleText",
|
|
2624
|
+
"textLength=${lastAuthorizedText.length}"
|
|
2625
|
+
)
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
internal fun deleteScalarRangeForPendingImeOperationForEditor(scalarFrom: Int, scalarTo: Int) {
|
|
2629
|
+
deleteRangeInRust(scalarFrom, scalarTo)
|
|
2630
|
+
}
|
|
2631
|
+
|
|
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
|
+
}
|
|
2664
|
+
|
|
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
|
+
}
|
|
2691
|
+
|
|
2692
|
+
val authorizedText = lastAuthorizedText
|
|
2693
|
+
val requestedCursor = requestedCursorScalar(
|
|
2694
|
+
mutation.scalarFrom,
|
|
2695
|
+
mutation.scalarTo,
|
|
2696
|
+
authorizedText,
|
|
2697
|
+
mutation.replacementText,
|
|
2698
|
+
newCursorPosition
|
|
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
|
+
}
|
|
2713
|
+
|
|
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
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
|
|
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
|
+
}
|
|
2773
|
+
|
|
2106
2774
|
/**
|
|
2107
2775
|
* Insert text at a scalar position via the Rust editor.
|
|
2108
2776
|
*/
|
|
@@ -2112,8 +2780,13 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2112
2780
|
callback(text, atScalarPos)
|
|
2113
2781
|
return
|
|
2114
2782
|
}
|
|
2783
|
+
val startedAt = System.nanoTime()
|
|
2115
2784
|
val updateJSON = editorInsertTextScalar(editorId.toULong(), atScalarPos.toUInt(), text)
|
|
2116
|
-
|
|
2785
|
+
recordImeTraceForTesting(
|
|
2786
|
+
"rustInsertText",
|
|
2787
|
+
"at=$atScalarPos textLength=${text.length} rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
2788
|
+
)
|
|
2789
|
+
applyRustUpdateJSON(updateJSON)
|
|
2117
2790
|
}
|
|
2118
2791
|
|
|
2119
2792
|
private fun replaceTextRangeInRust(scalarFrom: Int, scalarTo: Int, text: String) {
|
|
@@ -2122,13 +2795,18 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2122
2795
|
callback(scalarFrom, scalarTo, text)
|
|
2123
2796
|
return
|
|
2124
2797
|
}
|
|
2798
|
+
val startedAt = System.nanoTime()
|
|
2125
2799
|
val updateJSON = editorReplaceTextScalar(
|
|
2126
2800
|
editorId.toULong(),
|
|
2127
2801
|
scalarFrom.toUInt(),
|
|
2128
2802
|
scalarTo.toUInt(),
|
|
2129
2803
|
text
|
|
2130
2804
|
)
|
|
2131
|
-
|
|
2805
|
+
recordImeTraceForTesting(
|
|
2806
|
+
"rustReplaceText",
|
|
2807
|
+
"range=$scalarFrom..$scalarTo textLength=${text.length} rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
2808
|
+
)
|
|
2809
|
+
applyRustUpdateJSON(updateJSON)
|
|
2132
2810
|
}
|
|
2133
2811
|
|
|
2134
2812
|
private fun insertPlainTextRangeInRust(
|
|
@@ -2138,6 +2816,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2138
2816
|
requestedCursorScalar: Int? = null
|
|
2139
2817
|
) {
|
|
2140
2818
|
if (!hasLiveEditor()) return
|
|
2819
|
+
recordImeTraceForTesting(
|
|
2820
|
+
"rustPlainTextRoute",
|
|
2821
|
+
"range=$scalarFrom..$scalarTo textLength=${text.length} requestedCursor=$requestedCursorScalar"
|
|
2822
|
+
)
|
|
2141
2823
|
if (text.isEmpty()) {
|
|
2142
2824
|
if (scalarFrom != scalarTo) {
|
|
2143
2825
|
deleteRangeInRust(scalarFrom, scalarTo)
|
|
@@ -2152,13 +2834,18 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2152
2834
|
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2153
2835
|
return
|
|
2154
2836
|
}
|
|
2837
|
+
val startedAt = System.nanoTime()
|
|
2155
2838
|
val updateJSON = editorInsertContentJsonAtSelectionScalar(
|
|
2156
2839
|
editorId.toULong(),
|
|
2157
2840
|
scalarFrom.toUInt(),
|
|
2158
2841
|
scalarTo.toUInt(),
|
|
2159
2842
|
docJson
|
|
2160
2843
|
)
|
|
2161
|
-
|
|
2844
|
+
recordImeTraceForTesting(
|
|
2845
|
+
"rustInsertContentJson",
|
|
2846
|
+
"range=$scalarFrom..$scalarTo textLength=${text.length} rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
2847
|
+
)
|
|
2848
|
+
applyRustUpdateJSON(updateJSON)
|
|
2162
2849
|
applyRequestedCursorScalar(requestedCursorScalar)
|
|
2163
2850
|
return
|
|
2164
2851
|
}
|
|
@@ -2286,6 +2973,8 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2286
2973
|
scalarTo = PositionBridge.utf16ToScalar(authorizedEnd, authorizedText),
|
|
2287
2974
|
replacementText = replacementText,
|
|
2288
2975
|
resultingText = currentText,
|
|
2976
|
+
replacementStartUtf16 = prefix,
|
|
2977
|
+
replacementEndUtf16 = currentEnd,
|
|
2289
2978
|
selectionScalarAnchor = selectionAnchorUtf16?.let {
|
|
2290
2979
|
PositionBridge.utf16ToScalar(it, currentText)
|
|
2291
2980
|
},
|
|
@@ -2406,6 +3095,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2406
3095
|
|
|
2407
3096
|
private fun commitNativeTextMutation(mutation: NativeTextMutation) {
|
|
2408
3097
|
if (!hasLiveEditor()) return
|
|
3098
|
+
val startedAt = System.nanoTime()
|
|
2409
3099
|
if ((text?.toString() ?: "") != mutation.resultingText) {
|
|
2410
3100
|
recordImeTraceForTesting(
|
|
2411
3101
|
"nativeMutationNoop",
|
|
@@ -2435,6 +3125,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2435
3125
|
if (shouldRestartInput) {
|
|
2436
3126
|
restartInputForEditor()
|
|
2437
3127
|
}
|
|
3128
|
+
recordImeTraceForTesting(
|
|
3129
|
+
"nativeMutationApplyDone",
|
|
3130
|
+
"totalUs=${nanosToMicros(System.nanoTime() - startedAt)} restartInput=$shouldRestartInput"
|
|
3131
|
+
)
|
|
2438
3132
|
}
|
|
2439
3133
|
|
|
2440
3134
|
private fun restoreSelectionAfterNativeTextMutation(mutation: NativeTextMutation) {
|
|
@@ -2460,8 +3154,13 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2460
3154
|
callback(scalarFrom, scalarTo)
|
|
2461
3155
|
return
|
|
2462
3156
|
}
|
|
3157
|
+
val startedAt = System.nanoTime()
|
|
2463
3158
|
val updateJSON = editorDeleteScalarRange(editorId.toULong(), scalarFrom.toUInt(), scalarTo.toUInt())
|
|
2464
|
-
|
|
3159
|
+
recordImeTraceForTesting(
|
|
3160
|
+
"rustDeleteRange",
|
|
3161
|
+
"range=$scalarFrom..$scalarTo rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3162
|
+
)
|
|
3163
|
+
applyRustUpdateJSON(updateJSON)
|
|
2465
3164
|
}
|
|
2466
3165
|
|
|
2467
3166
|
private fun deleteBackwardAtSelectionScalarInRust(scalarAnchor: Int, scalarHead: Int) {
|
|
@@ -2470,12 +3169,17 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2470
3169
|
callback(scalarAnchor, scalarHead)
|
|
2471
3170
|
return
|
|
2472
3171
|
}
|
|
3172
|
+
val startedAt = System.nanoTime()
|
|
2473
3173
|
val updateJSON = editorDeleteBackwardAtSelectionScalar(
|
|
2474
3174
|
editorId.toULong(),
|
|
2475
3175
|
scalarAnchor.toUInt(),
|
|
2476
3176
|
scalarHead.toUInt()
|
|
2477
3177
|
)
|
|
2478
|
-
|
|
3178
|
+
recordImeTraceForTesting(
|
|
3179
|
+
"rustDeleteBackward",
|
|
3180
|
+
"selection=$scalarAnchor..$scalarHead rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3181
|
+
)
|
|
3182
|
+
applyRustUpdateJSON(updateJSON)
|
|
2479
3183
|
}
|
|
2480
3184
|
|
|
2481
3185
|
/**
|
|
@@ -2483,8 +3187,14 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2483
3187
|
*/
|
|
2484
3188
|
private fun splitBlockInRust(atScalarPos: Int) {
|
|
2485
3189
|
if (!hasLiveEditor()) return
|
|
3190
|
+
val startedAt = System.nanoTime()
|
|
2486
3191
|
val updateJSON = editorSplitBlockScalar(editorId.toULong(), atScalarPos.toUInt())
|
|
2487
|
-
|
|
3192
|
+
recordImeTraceForTesting(
|
|
3193
|
+
"rustSplitBlock",
|
|
3194
|
+
"at=$atScalarPos rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3195
|
+
)
|
|
3196
|
+
applyRustUpdateJSON(updateJSON)
|
|
3197
|
+
scheduleLineBoundaryInputRefreshForEditor("splitBlock")
|
|
2488
3198
|
}
|
|
2489
3199
|
|
|
2490
3200
|
private fun deleteAndSplitInRust(scalarFrom: Int, scalarTo: Int) {
|
|
@@ -2493,12 +3203,18 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2493
3203
|
callback(scalarFrom, scalarTo)
|
|
2494
3204
|
return
|
|
2495
3205
|
}
|
|
3206
|
+
val startedAt = System.nanoTime()
|
|
2496
3207
|
val updateJSON = editorDeleteAndSplitScalar(
|
|
2497
3208
|
editorId.toULong(),
|
|
2498
3209
|
scalarFrom.toUInt(),
|
|
2499
3210
|
scalarTo.toUInt()
|
|
2500
3211
|
)
|
|
2501
|
-
|
|
3212
|
+
recordImeTraceForTesting(
|
|
3213
|
+
"rustDeleteAndSplit",
|
|
3214
|
+
"range=$scalarFrom..$scalarTo rustUs=${nanosToMicros(System.nanoTime() - startedAt)} jsonLength=${updateJSON.length}"
|
|
3215
|
+
)
|
|
3216
|
+
applyRustUpdateJSON(updateJSON)
|
|
3217
|
+
scheduleLineBoundaryInputRefreshForEditor("deleteAndSplit")
|
|
2502
3218
|
}
|
|
2503
3219
|
|
|
2504
3220
|
internal fun currentScalarSelection(): Pair<Int, Int>? {
|
|
@@ -2506,6 +3222,143 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2506
3222
|
return normalizedScalarSelectionRange(currentText)
|
|
2507
3223
|
}
|
|
2508
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
|
+
|
|
2509
3362
|
private fun normalizedUtf16SelectionRange(currentText: String): Pair<Int, Int>? {
|
|
2510
3363
|
val start = selectionStart
|
|
2511
3364
|
val end = selectionEnd
|
|
@@ -2708,8 +3561,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2708
3561
|
replaceRange: RenderReplaceRange? = null,
|
|
2709
3562
|
usedPatch: Boolean
|
|
2710
3563
|
) {
|
|
3564
|
+
val startedAt = System.nanoTime()
|
|
3565
|
+
val previousScrollX = scrollX
|
|
3566
|
+
val previousScrollY = scrollY
|
|
2711
3567
|
val hadCompositionTracking = hasCompositionTrackingForEditor()
|
|
2712
3568
|
var shouldRestartInput = false
|
|
3569
|
+
val mode = if (replaceRange != null) "replace" else "setText"
|
|
2713
3570
|
isApplyingRustState = true
|
|
2714
3571
|
beginBatchEdit()
|
|
2715
3572
|
try {
|
|
@@ -2734,9 +3591,30 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2734
3591
|
endBatchEdit()
|
|
2735
3592
|
isApplyingRustState = false
|
|
2736
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
|
+
)
|
|
2737
3598
|
restartInputAfterCompositionInvalidationIfNeeded(shouldRestartInput)
|
|
2738
3599
|
}
|
|
2739
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
|
+
)
|
|
3616
|
+
}
|
|
3617
|
+
|
|
2740
3618
|
private fun buildPatchedSpannable(patch: ParsedRenderPatch): android.text.SpannableStringBuilder =
|
|
2741
3619
|
RenderBridge.buildSpannableFromBlocks(
|
|
2742
3620
|
patch.renderBlocks,
|
|
@@ -2906,9 +3784,14 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2906
3784
|
val parseStartedAt = totalStartedAt
|
|
2907
3785
|
val update = try {
|
|
2908
3786
|
org.json.JSONObject(updateJSON)
|
|
2909
|
-
} catch (
|
|
3787
|
+
} catch (error: Exception) {
|
|
3788
|
+
recordImeTraceForTesting(
|
|
3789
|
+
"applyUpdateJSONNoop",
|
|
3790
|
+
"reason=parseError jsonLength=${updateJSON.length} error=${error.javaClass.simpleName}"
|
|
3791
|
+
)
|
|
2910
3792
|
return
|
|
2911
3793
|
}
|
|
3794
|
+
cancelDeferredRustUpdateApplication()
|
|
2912
3795
|
val parseNanos = System.nanoTime() - parseStartedAt
|
|
2913
3796
|
|
|
2914
3797
|
val resolveRenderBlocksStartedAt = System.nanoTime()
|
|
@@ -2933,6 +3816,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2933
3816
|
val buildRenderNanos: Long
|
|
2934
3817
|
val applyRenderNanos: Long
|
|
2935
3818
|
if (shouldSkipRender) {
|
|
3819
|
+
pendingOptimisticRenderText = null
|
|
2936
3820
|
lastRenderAppliedPatchForTesting = false
|
|
2937
3821
|
currentRenderBlocksJson = resolvedRenderBlocks?.let(::cloneJsonArray)
|
|
2938
3822
|
clearNativeTextMutationAdoptionSuppression()
|
|
@@ -2964,12 +3848,27 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2964
3848
|
this
|
|
2965
3849
|
)
|
|
2966
3850
|
} else {
|
|
3851
|
+
recordImeTraceForTesting(
|
|
3852
|
+
"applyUpdateJSONNoop",
|
|
3853
|
+
"reason=noRenderPayload jsonLength=${updateJSON.length}"
|
|
3854
|
+
)
|
|
2967
3855
|
return
|
|
2968
3856
|
}
|
|
2969
3857
|
buildRenderNanos = System.nanoTime() - buildStartedAt
|
|
2970
3858
|
currentRenderBlocksJson = resolvedRenderBlocks?.let(::cloneJsonArray)
|
|
2971
3859
|
val applyStartedAt = System.nanoTime()
|
|
2972
|
-
|
|
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
|
|
2973
3872
|
applyRenderNanos = System.nanoTime() - applyStartedAt
|
|
2974
3873
|
lastAppliedRenderAppearanceRevision = renderAppearanceRevision
|
|
2975
3874
|
}
|
|
@@ -2994,6 +3893,12 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
2994
3893
|
}
|
|
2995
3894
|
val postApplyNanos = System.nanoTime() - postApplyStartedAt
|
|
2996
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
|
+
|
|
2997
3902
|
if (captureApplyUpdateTraceForTesting) {
|
|
2998
3903
|
lastApplyUpdateTraceForTesting = ApplyUpdateTrace(
|
|
2999
3904
|
attemptedPatch = renderPatch != null,
|
|
@@ -3006,7 +3911,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
3006
3911
|
applyRenderNanos = applyRenderNanos,
|
|
3007
3912
|
selectionNanos = selectionNanos,
|
|
3008
3913
|
postApplyNanos = postApplyNanos,
|
|
3009
|
-
totalNanos =
|
|
3914
|
+
totalNanos = totalNanos
|
|
3010
3915
|
)
|
|
3011
3916
|
}
|
|
3012
3917
|
}
|
|
@@ -3020,6 +3925,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
3020
3925
|
* @param renderJSON The JSON array string of render elements.
|
|
3021
3926
|
*/
|
|
3022
3927
|
fun applyRenderJSON(renderJSON: String) {
|
|
3928
|
+
val startedAt = System.nanoTime()
|
|
3023
3929
|
val spannable = RenderBridge.buildSpannable(
|
|
3024
3930
|
renderJSON,
|
|
3025
3931
|
baseFontSize,
|
|
@@ -3034,6 +3940,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
3034
3940
|
|
|
3035
3941
|
explicitSelectedImageRange = null
|
|
3036
3942
|
currentRenderBlocksJson = null
|
|
3943
|
+
pendingOptimisticRenderText = null
|
|
3037
3944
|
applyRenderedSpannable(spannable, usedPatch = false)
|
|
3038
3945
|
onSelectionOrContentMayChange?.invoke()
|
|
3039
3946
|
if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
@@ -3041,6 +3948,10 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
3041
3948
|
} else {
|
|
3042
3949
|
preserveScrollPosition(previousScrollX, previousScrollY)
|
|
3043
3950
|
}
|
|
3951
|
+
recordImeTraceForTesting(
|
|
3952
|
+
"applyRenderJSON",
|
|
3953
|
+
"jsonLength=${renderJSON.length} totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
3954
|
+
)
|
|
3044
3955
|
}
|
|
3045
3956
|
|
|
3046
3957
|
private fun textOffsetHitAt(x: Float, y: Float): Pair<Spanned, Int>? {
|
|
@@ -3364,6 +4275,7 @@ class EditorEditText @JvmOverloads constructor(
|
|
|
3364
4275
|
private const val DEFAULT_KEYBOARD_TYPE = "default"
|
|
3365
4276
|
private const val EMPTY_BLOCK_PLACEHOLDER = '\u200B'
|
|
3366
4277
|
private const val IME_TRACE_LIMIT_FOR_TESTING = 80
|
|
4278
|
+
private const val IME_TRACE_LOG_TAG = "NativeEditorIme"
|
|
3367
4279
|
private const val NATIVE_TEXT_MUTATION_AFTER_BLUR_WINDOW_MS = 750L
|
|
3368
4280
|
private const val RECENT_HANDLED_HARDWARE_KEY_DOWN_WINDOW_MS = 750L
|
|
3369
4281
|
private const val LOG_TAG = "NativeEditor"
|