@openeditor/react-native-prose-editor 0.0.7 → 0.0.9

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.
Files changed (68) hide show
  1. package/android/build.gradle +35 -0
  2. package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
  4. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
  5. package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
  6. package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
  7. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
  8. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
  9. package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
  10. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
  11. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
  12. package/dist/EditorToolbar.d.ts +1 -1
  13. package/dist/EditorToolbar.js +19 -0
  14. package/dist/NativeEditorBridge.d.ts +62 -35
  15. package/dist/NativeEditorBridge.js +122 -79
  16. package/dist/NativeProseViewer.d.ts +13 -34
  17. package/dist/NativeProseViewer.js +40 -465
  18. package/dist/NativeRichTextEditor.d.ts +10 -1
  19. package/dist/NativeRichTextEditor.js +72 -34
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +2 -1
  22. package/dist/schemas.d.ts +11 -0
  23. package/dist/schemas.js +149 -0
  24. package/dist/useNativeEditor.d.ts +3 -3
  25. package/dist/useNativeEditor.js +3 -3
  26. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  27. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  28. package/ios/Generated_editor_core.swift +97 -2
  29. package/ios/NativeBlockEditorSurface.swift +1445 -0
  30. package/ios/NativeEditorExpoView.swift +3 -3
  31. package/ios/NativeEditorModule.swift +31 -46
  32. package/ios/NativeInputSupport.swift +396 -0
  33. package/ios/PositionBridge.swift +4 -4
  34. package/ios/RichTextEditorView.swift +352 -786
  35. package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
  36. package/package.json +1 -1
  37. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  38. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  39. package/rust/android/x86_64/libeditor_core.so +0 -0
  40. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2087 -1399
  41. package/android/src/androidTest/AndroidManifest.xml +0 -8
  42. package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
  43. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
  44. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
  45. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
  46. package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
  47. package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
  48. package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
  49. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
  50. package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
  51. package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
  52. package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
  53. package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
  54. package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
  55. package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
  56. package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
  57. package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
  58. package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
  59. package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
  60. package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
  61. package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
  62. package/ios/EditorLayoutManager.swift +0 -774
  63. package/ios/NativeProseViewerExpoView.swift +0 -276
  64. package/ios/RenderBridge.swift +0 -1708
  65. package/ios/Tests/NativePerformanceTests.swift +0 -687
  66. package/ios/Tests/PositionBridgeTests.swift +0 -706
  67. package/ios/Tests/RenderBridgeTests.swift +0 -2236
  68. package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
@@ -1,4202 +0,0 @@
1
- package com.openeditor.editor
2
-
3
- import android.content.ClipData
4
- import android.content.ClipboardManager
5
- import android.content.Context
6
- import android.content.Intent
7
- import android.graphics.Color
8
- import android.net.Uri
9
- import android.os.Bundle
10
- import android.os.Looper
11
- import android.provider.Settings
12
- import android.text.Selection
13
- import android.text.InputType
14
- import android.text.style.AbsoluteSizeSpan
15
- import android.view.KeyEvent
16
- import android.view.accessibility.AccessibilityNodeInfo
17
- import android.view.inputmethod.BaseInputConnection
18
- import android.view.inputmethod.CompletionInfo
19
- import android.view.inputmethod.CorrectionInfo
20
- import android.view.inputmethod.EditorInfo
21
- import org.json.JSONArray
22
- import org.json.JSONObject
23
- import org.junit.Assert.assertEquals
24
- import org.junit.Assert.assertFalse
25
- import org.junit.Assert.assertNotNull
26
- import org.junit.Assert.assertNull
27
- import org.junit.Assert.assertTrue
28
- import org.junit.Test
29
- import org.junit.runner.RunWith
30
- import org.robolectric.Shadows.shadowOf
31
- import org.robolectric.RobolectricTestRunner
32
- import org.robolectric.RuntimeEnvironment
33
- import org.robolectric.annotation.Config
34
- import java.time.Duration
35
-
36
- @RunWith(RobolectricTestRunner::class)
37
- @Config(sdk = [34])
38
- class EditorInputConnectionTest {
39
- @Test
40
- fun `editor input traits use rich text defaults`() {
41
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
42
-
43
- assertEquals(InputType.TYPE_CLASS_TEXT, editText.inputType and InputType.TYPE_MASK_CLASS)
44
- assertTrue(editText.inputType hasInputFlag InputType.TYPE_TEXT_FLAG_MULTI_LINE)
45
- assertTrue(editText.inputType hasInputFlag InputType.TYPE_TEXT_FLAG_AUTO_CORRECT)
46
- assertTrue(editText.inputType hasInputFlag InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
47
- }
48
-
49
- @Test
50
- fun `editor input traits apply React keyboard props`() {
51
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
52
-
53
- editText.setKeyboardType("email-address")
54
- editText.setAutoCapitalize("none")
55
- editText.setAutoCorrect(false)
56
-
57
- assertEquals(InputType.TYPE_CLASS_TEXT, editText.inputType and InputType.TYPE_MASK_CLASS)
58
- assertTrue(editText.inputType hasInputFlag InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS)
59
- assertTrue(editText.inputType hasInputFlag InputType.TYPE_TEXT_FLAG_MULTI_LINE)
60
- assertTrue(editText.inputType hasInputFlag InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS)
61
- assertFalse(editText.inputType hasInputFlag InputType.TYPE_TEXT_FLAG_AUTO_CORRECT)
62
- assertFalse(editText.inputType hasInputFlag InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
63
- }
64
-
65
- @Test
66
- fun `cursor caps mode treats rendered empty block start as sentence start`() {
67
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
68
- editText.applyUpdateJSON(renderBlocksUpdateJson("Hello", "\u200B"), notifyListener = false)
69
- editText.setSelection(editText.text?.length ?: 0)
70
-
71
- assertEquals("Hello\n\u200B", editText.text.toString())
72
- assertTrue(
73
- editText.cursorCapsModeForEditor(
74
- InputType.TYPE_TEXT_FLAG_CAP_SENTENCES,
75
- baseCapsMode = 0
76
- ) hasInputFlag InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
77
- )
78
-
79
- val editorInfo = EditorInfo()
80
- val inputConnection = editText.onCreateInputConnection(editorInfo)
81
- assertNotNull(inputConnection)
82
- assertTrue(editorInfo.initialCapsMode hasInputFlag InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
83
- assertTrue(
84
- inputConnection!!.getCursorCapsMode(InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
85
- hasInputFlag InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
86
- )
87
- }
88
-
89
- @Test
90
- fun `text before cursor hides synthetic empty block placeholder from IME context`() {
91
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
92
- editText.applyUpdateJSON(renderBlocksUpdateJson("Hello", "\u200B"), notifyListener = false)
93
- editText.setSelection(editText.text?.length ?: 0)
94
-
95
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
96
- assertNotNull(inputConnection)
97
-
98
- assertEquals("\n", inputConnection!!.getTextBeforeCursor(1, 0).toString())
99
- assertEquals("Hello\n", inputConnection.getTextBeforeCursor(20, 0).toString())
100
- }
101
-
102
- @Test
103
- fun `initial surrounding text removes synthetic placeholder for IME sentence caps`() {
104
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
105
- editText.applyUpdateJSON(renderBlocksUpdateJson("Hello", "\u200B"), notifyListener = false)
106
- editText.setSelection(editText.text?.length ?: 0)
107
-
108
- val editorInfo = EditorInfo()
109
- val inputConnection = editText.onCreateInputConnection(editorInfo)
110
- assertNotNull(inputConnection)
111
-
112
- assertEquals("Hello\n", editorInfo.getInitialTextBeforeCursor(20, 0).toString())
113
- assertEquals(editText.selectionStart - 1, editorInfo.initialSelStart)
114
- assertEquals(editText.selectionEnd - 1, editorInfo.initialSelEnd)
115
- assertFalse(editorInfo.getInitialTextBeforeCursor(20, 0).toString().contains("\u200B"))
116
- assertTrue(
117
- editorInfo.initialCapsMode hasInputFlag InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
118
- )
119
- }
120
-
121
- @Test
122
- fun `Samsung composing text at rendered line start is sentence capitalized`() {
123
- val context = RuntimeEnvironment.getApplication()
124
- val editText = EditorEditText(context)
125
- editText.applyUpdateJSON(renderBlocksUpdateJson("Hello", "\u200B"), notifyListener = false)
126
- editText.setSelection(editText.text?.length ?: 0)
127
- editText.editorId = 1
128
-
129
- withDefaultInputMethod(context, "com.samsung.android.honeyboard/.service.HoneyBoardService") {
130
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
131
- assertNotNull(inputConnection)
132
-
133
- assertTrue(inputConnection!!.setComposingText("test", 1))
134
-
135
- assertEquals("Test", editText.composingTextForEditor())
136
- assertTrue(
137
- editText.imeTraceSnapshotForTesting().any {
138
- it.contains("samsungSentenceCapsFallback")
139
- }
140
- )
141
- }
142
- }
143
-
144
- @Test
145
- fun `cursor caps mode does not force sentence caps mid line`() {
146
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
147
- editText.applyUpdateJSON(renderUpdateJson("Hello "), notifyListener = false)
148
- editText.setSelection(editText.text?.length ?: 0)
149
-
150
- assertFalse(
151
- editText.cursorCapsModeForEditor(
152
- InputType.TYPE_TEXT_FLAG_CAP_SENTENCES,
153
- baseCapsMode = 0
154
- ) hasInputFlag InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
155
- )
156
- }
157
-
158
- @Test
159
- fun `editor numeric keyboard type maps to numeric input class`() {
160
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
161
-
162
- editText.setKeyboardType("numeric")
163
-
164
- assertEquals(InputType.TYPE_CLASS_NUMBER, editText.inputType and InputType.TYPE_MASK_CLASS)
165
- assertTrue(editText.inputType hasInputFlag InputType.TYPE_NUMBER_FLAG_DECIMAL)
166
- assertTrue(editText.inputType hasInputFlag InputType.TYPE_NUMBER_FLAG_SIGNED)
167
- }
168
-
169
- @Test
170
- fun `input trait changes stale old connection and fresh connection accepts input`() {
171
- val traitChanges: List<(EditorEditText) -> Unit> = listOf(
172
- { it.setAutoCorrect(false) },
173
- { it.setKeyboardType("email-address") }
174
- )
175
-
176
- for (changeTrait in traitChanges) {
177
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
178
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
179
- editText.setSelection(3)
180
- editText.editorId = 1
181
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
182
-
183
- var insertedText: String? = null
184
- var insertedScalar: Int? = null
185
- editText.onInsertTextInRustForTesting = { text, scalar ->
186
- insertedText = text
187
- insertedScalar = scalar
188
- }
189
-
190
- val oldConnection = editText.onCreateInputConnection(EditorInfo())
191
- assertNotNull(oldConnection)
192
-
193
- changeTrait(editText)
194
-
195
- assertTrue(oldConnection!!.commitText("old", 1))
196
- assertNull(insertedText)
197
-
198
- val freshConnection = editText.onCreateInputConnection(EditorInfo())
199
- assertNotNull(freshConnection)
200
- assertTrue(freshConnection!!.commitText("fresh", 1))
201
-
202
- assertEquals("fresh", insertedText)
203
- }
204
- }
205
-
206
- @Test
207
- fun `external clear keeps same editor input connection accepting input`() {
208
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
209
- editText.applyUpdateJSON(renderUpdateJson("sent"), notifyListener = false)
210
- assertTrue(editText.requestFocus())
211
- editText.setSelection(4)
212
- editText.editorId = 1
213
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
214
-
215
- var insertedText: String? = null
216
- editText.onInsertTextInRustForTesting = { text, _ ->
217
- insertedText = text
218
- }
219
-
220
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
221
- assertNotNull(inputConnection)
222
-
223
- editText.applyUpdateJSON(
224
- renderUpdateJson("\u200B"),
225
- notifyListener = false,
226
- refreshInputConnectionForExternalUpdate = true
227
- )
228
- editText.setSelection(editText.text?.length ?: 0)
229
-
230
- assertTrue(
231
- editText.imeTraceSnapshotForTesting().any {
232
- it.contains("restartInput:source=externalUpdate")
233
- }
234
- )
235
-
236
- assertTrue(inputConnection!!.commitText("fresh", 1))
237
-
238
- assertEquals("fresh", insertedText)
239
- }
240
-
241
- @Test
242
- fun `external clear keeps same editor input connection accepting composition`() {
243
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
244
- editText.applyUpdateJSON(renderUpdateJson("sent"), notifyListener = false)
245
- assertTrue(editText.requestFocus())
246
- editText.setSelection(4)
247
- editText.editorId = 1
248
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
249
-
250
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
251
- assertNotNull(inputConnection)
252
-
253
- editText.applyUpdateJSON(
254
- renderUpdateJson("\u200B"),
255
- notifyListener = false,
256
- refreshInputConnectionForExternalUpdate = true
257
- )
258
- editText.setSelection(editText.text?.length ?: 0)
259
-
260
- assertTrue(inputConnection!!.setComposingText("f", 1))
261
- assertTrue(editText.text?.toString()?.contains("f") == true)
262
- }
263
-
264
- @Test
265
- fun `external clear invalidates rendered editor content`() {
266
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
267
- editText.applyUpdateJSON(renderUpdateJson("sent"), notifyListener = false)
268
- shadowOf(editText).clearWasInvalidated()
269
-
270
- editText.applyUpdateJSON(
271
- renderUpdateJson(""),
272
- notifyListener = false,
273
- refreshInputConnectionForExternalUpdate = true
274
- )
275
-
276
- assertEquals("", editText.text?.toString())
277
- assertTrue(shadowOf(editText).wasInvalidated())
278
- }
279
-
280
- @Test
281
- fun `external clear after deferred Rust update clears stale visible text`() {
282
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
283
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
284
- assertTrue(editText.requestFocus())
285
- editText.setSelection(0)
286
- editText.editorId = 1
287
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
288
-
289
- var insertedText: String? = null
290
- editText.onInsertTextInRustForTesting = { text, _ ->
291
- insertedText = text
292
- }
293
-
294
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
295
- assertNotNull(inputConnection)
296
-
297
- editText.runWithDeferredRustUpdateApplication {
298
- editText.runWithTransientInputMutationGuard {
299
- editText.text!!.insert(0, "second")
300
- editText.setSelection(6)
301
- true
302
- }
303
- editText.applyRustUpdateJSONForTesting(renderUpdateJson("second"))
304
- }
305
-
306
- assertTrue(editText.hasDeferredRustUpdateApplicationForTesting())
307
- assertEquals("second", editText.text?.toString())
308
-
309
- editText.applyUpdateJSON(
310
- renderUpdateJson(""),
311
- notifyListener = false,
312
- refreshInputConnectionForExternalUpdate = true
313
- )
314
- editText.setSelection(editText.text?.length ?: 0)
315
-
316
- assertFalse(editText.hasDeferredRustUpdateApplicationForTesting())
317
- assertEquals("", editText.text?.toString())
318
-
319
- assertTrue(inputConnection!!.commitText("next", 1))
320
- assertEquals("next", insertedText)
321
- }
322
-
323
- @Test
324
- fun `external clear after preflight native mutation keeps same editor input connection accepting input`() {
325
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
326
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
327
- assertTrue(editText.requestFocus())
328
- editText.setSelection(0)
329
- editText.editorId = 1
330
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
331
-
332
- var renderedText = ""
333
- var insertedText: String? = null
334
- editText.onInsertTextInRustForTesting = { text, scalar ->
335
- insertedText = text
336
- renderedText = renderedText.substring(0, scalar.coerceIn(0, renderedText.length)) +
337
- text +
338
- renderedText.substring(scalar.coerceIn(0, renderedText.length))
339
- editText.applyUpdateJSON(renderUpdateJson(renderedText), notifyListener = false)
340
- editText.setSelection(renderedText.length)
341
- }
342
-
343
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
344
- assertNotNull(inputConnection)
345
-
346
- editText.runWithTransientInputMutationGuard {
347
- editText.text!!.insert(0, "second")
348
- editText.setSelection(6)
349
- true
350
- }
351
-
352
- assertTrue(editText.prepareForExternalEditorUpdate())
353
- assertEquals("second", insertedText)
354
- assertEquals("second", editText.text?.toString())
355
-
356
- renderedText = ""
357
- insertedText = null
358
- editText.applyUpdateJSON(
359
- renderUpdateJson("\u200B"),
360
- notifyListener = false,
361
- refreshInputConnectionForExternalUpdate = true
362
- )
363
- editText.setSelection(editText.text?.length ?: 0)
364
-
365
- assertEquals("\u200B", editText.text?.toString())
366
- assertTrue(inputConnection!!.commitText("next", 1))
367
- assertEquals("next", insertedText)
368
- }
369
-
370
- @Test
371
- fun `destroyed editor input session consumes IME changes without Rust mutation`() {
372
- val editorId = 880001L
373
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
374
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
375
- editText.setSelection(3)
376
- editText.editorId = editorId
377
-
378
- var insertedText: String? = null
379
- var replacedText: Triple<Int, Int, String>? = null
380
- var deletedRange: Pair<Int, Int>? = null
381
- var deletedBackward: Pair<Int, Int>? = null
382
- var syncedSelection: Pair<Int, Int>? = null
383
- editText.onInsertTextInRustForTesting = { text, _ -> insertedText = text }
384
- editText.onReplaceTextInRustForTesting = { from, to, text ->
385
- replacedText = Triple(from, to, text)
386
- }
387
- editText.onDeleteRangeInRustForTesting = { from, to -> deletedRange = from to to }
388
- editText.onDeleteBackwardAtSelectionScalarInRustForTesting = { anchor, head ->
389
- deletedBackward = anchor to head
390
- }
391
- editText.onSetSelectionScalarInRustForTesting = { anchor, head ->
392
- syncedSelection = anchor to head
393
- }
394
-
395
- NativeEditorViewRegistry.markEditorCreated(editorId)
396
- try {
397
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
398
- assertNotNull(inputConnection)
399
-
400
- NativeEditorViewRegistry.invalidateDestroyedEditor(editorId)
401
-
402
- assertTrue(inputConnection!!.commitText("x", 1))
403
- assertTrue(inputConnection.commitCompletion(CompletionInfo(0, 0, "done")))
404
- assertTrue(inputConnection.commitCorrection(CorrectionInfo(0, "abc", "xyz")))
405
- assertTrue(inputConnection.setComposingText("z", 1))
406
- assertTrue(inputConnection.deleteSurroundingText(1, 0))
407
- editText.setSelection(0)
408
-
409
- assertNull(insertedText)
410
- assertNull(replacedText)
411
- assertNull(deletedRange)
412
- assertNull(deletedBackward)
413
- assertNull(syncedSelection)
414
- } finally {
415
- NativeEditorViewRegistry.markEditorCreated(editorId)
416
- }
417
- }
418
-
419
- @Test
420
- fun `input trait change during active composition restores authorized text before fresh input`() {
421
- val traitChanges: List<(EditorEditText) -> Unit> = listOf(
422
- { it.setAutoCorrect(false) },
423
- { it.setKeyboardType("email-address") }
424
- )
425
-
426
- for (changeTrait in traitChanges) {
427
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
428
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
429
- editText.setSelection(6)
430
- editText.editorId = 1
431
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
432
-
433
- var insertedText: String? = null
434
- var insertedScalar: Int? = null
435
- editText.onInsertTextInRustForTesting = { text, scalar ->
436
- insertedText = text
437
- insertedScalar = scalar
438
- }
439
-
440
- val oldConnection = editText.onCreateInputConnection(EditorInfo())
441
- assertNotNull(oldConnection)
442
- assertTrue(oldConnection!!.setComposingText("brave ", 1))
443
- assertEquals("Hello brave world", editText.text?.toString())
444
-
445
- changeTrait(editText)
446
-
447
- assertEquals("Hello world", editText.text?.toString())
448
- assertEquals(6, editText.selectionStart)
449
- assertEquals(6, editText.selectionEnd)
450
-
451
- assertTrue(oldConnection.commitText("brave ", 1))
452
- assertNull(insertedText)
453
- assertNull(insertedScalar)
454
-
455
- val freshConnection = editText.onCreateInputConnection(EditorInfo())
456
- assertNotNull(freshConnection)
457
- assertTrue(freshConnection!!.commitText("fresh", 1))
458
-
459
- assertEquals("fresh", insertedText)
460
- assertEquals(6, insertedScalar)
461
- }
462
- }
463
-
464
- @Test
465
- fun `old input connection remains usable after framework recreation from render`() {
466
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
467
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
468
- editText.setSelection(0)
469
- editText.editorId = 1
470
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
471
-
472
- val inserted = mutableListOf<Pair<String, Int>>()
473
- val rendered = StringBuilder()
474
- editText.onInsertTextInRustForTesting = { text, scalar ->
475
- inserted.add(text to scalar)
476
- rendered.insert(scalar.coerceIn(0, rendered.length), text)
477
- editText.applyUpdateJSON(renderUpdateJson(rendered.toString()), notifyListener = false)
478
- editText.setSelection(rendered.length)
479
- }
480
-
481
- val originalConnection = editText.onCreateInputConnection(EditorInfo())
482
- assertNotNull(originalConnection)
483
- assertTrue(originalConnection!!.commitText("a", 1))
484
-
485
- val recreatedConnection = editText.onCreateInputConnection(EditorInfo())
486
- assertNotNull(recreatedConnection)
487
-
488
- assertTrue(originalConnection.commitText("b", 1))
489
-
490
- assertEquals(listOf("a" to 0, "b" to 1), inserted)
491
- assertEquals("ab", editText.text?.toString())
492
- }
493
-
494
- @Test
495
- fun `input trait change suppresses stale direct native mutation adoption`() {
496
- val traitChanges: List<(EditorEditText) -> Unit> = listOf(
497
- { it.setAutoCorrect(false) },
498
- { it.setKeyboardType("email-address") }
499
- )
500
-
501
- for (changeTrait in traitChanges) {
502
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
503
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
504
- assertTrue(editText.requestFocus())
505
- editText.setSelection(6)
506
- editText.editorId = 1
507
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
508
-
509
- var insertedText: String? = null
510
- var replacement: Triple<Int, Int, String>? = null
511
- editText.onInsertTextInRustForTesting = { text, _ ->
512
- insertedText = text
513
- }
514
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
515
- replacement = Triple(scalarFrom, scalarTo, text)
516
- }
517
-
518
- val oldConnection = editText.onCreateInputConnection(EditorInfo())
519
- assertNotNull(oldConnection)
520
- assertTrue(oldConnection!!.setComposingText("brave ", 1))
521
-
522
- changeTrait(editText)
523
- assertEquals("Hello world", editText.text?.toString())
524
-
525
- editText.runWithTransientInputMutationGuard {
526
- editText.text!!.insert(6, "stale ")
527
- true
528
- }
529
-
530
- assertFalse(editText.prepareForExternalEditorUpdate())
531
- assertNull(insertedText)
532
- assertNull(replacement)
533
- }
534
- }
535
-
536
- @Test
537
- fun `native mutation adoption suppression clears after authorized render update`() {
538
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
539
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
540
- assertTrue(editText.requestFocus())
541
- editText.setSelection(6)
542
- editText.editorId = 1
543
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
544
-
545
- var insertedText: String? = null
546
- var insertedScalar: Int? = null
547
- editText.onInsertTextInRustForTesting = { text, scalar ->
548
- insertedText = text
549
- insertedScalar = scalar
550
- }
551
-
552
- val oldConnection = editText.onCreateInputConnection(EditorInfo())
553
- assertNotNull(oldConnection)
554
- assertTrue(oldConnection!!.setComposingText("brave ", 1))
555
-
556
- editText.setAutoCorrect(false)
557
- editText.runWithTransientInputMutationGuard {
558
- editText.text!!.insert(6, "stale ")
559
- true
560
- }
561
-
562
- assertFalse(editText.prepareForExternalEditorUpdate())
563
- assertNull(insertedText)
564
- assertNull(insertedScalar)
565
-
566
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
567
- editText.text!!.insert(6, "fresh ")
568
-
569
- assertEquals("fresh ", insertedText)
570
- assertEquals(6, insertedScalar)
571
- assertEquals(0, editText.reconciliationCount)
572
- }
573
-
574
- @Test
575
- fun `native mutation adoption suppression clears after skipped authorized render update`() {
576
- val editText = EditorEditText(RuntimeEnvironment.getApplication()).apply {
577
- captureApplyUpdateTraceForTesting = true
578
- }
579
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
580
- assertTrue(editText.requestFocus())
581
- editText.setSelection(6)
582
- editText.editorId = 1
583
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
584
-
585
- var insertedText: String? = null
586
- var insertedScalar: Int? = null
587
- editText.onInsertTextInRustForTesting = { text, scalar ->
588
- insertedText = text
589
- insertedScalar = scalar
590
- }
591
-
592
- val oldConnection = editText.onCreateInputConnection(EditorInfo())
593
- assertNotNull(oldConnection)
594
- assertTrue(oldConnection!!.setComposingText("brave ", 1))
595
-
596
- editText.setAutoCorrect(false)
597
- assertEquals("Hello world", editText.text?.toString())
598
-
599
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
600
- assertTrue(editText.lastApplyUpdateTrace()?.skippedRender == true)
601
-
602
- editText.text!!.insert(6, "fresh ")
603
-
604
- assertEquals("fresh ", insertedText)
605
- assertEquals(6, insertedScalar)
606
- assertEquals(0, editText.reconciliationCount)
607
- }
608
-
609
- @Test
610
- fun `code point delete length matches ascii backspace`() {
611
- val text = "Hello"
612
- val cursor = 5
613
-
614
- val beforeUtf16Length = EditorInputConnection.codePointsToUtf16Length(
615
- text = text,
616
- fromUtf16Offset = cursor,
617
- codePointCount = 1,
618
- forward = false
619
- )
620
-
621
- assertEquals(1, beforeUtf16Length)
622
- }
623
-
624
- @Test
625
- fun `code point delete length counts surrogate pair as two utf16 code units`() {
626
- val text = "A😀B"
627
- val cursor = 3
628
-
629
- val beforeUtf16Length = EditorInputConnection.codePointsToUtf16Length(
630
- text = text,
631
- fromUtf16Offset = cursor,
632
- codePointCount = 1,
633
- forward = false
634
- )
635
-
636
- assertEquals(2, beforeUtf16Length)
637
- }
638
-
639
- @Test
640
- fun `code point forward delete length counts surrogate pair as two utf16 code units`() {
641
- val text = "A😀B"
642
- val cursor = 1
643
-
644
- val afterUtf16Length = EditorInputConnection.codePointsToUtf16Length(
645
- text = text,
646
- fromUtf16Offset = cursor,
647
- codePointCount = 1,
648
- forward = true
649
- )
650
-
651
- assertEquals(2, afterUtf16Length)
652
- }
653
-
654
- @Test
655
- fun `read only composing text and region are consumed without mutating text`() {
656
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
657
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
658
- editText.setSelection(1)
659
- editText.editorId = 1
660
- editText.isEditable = false
661
-
662
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
663
- assertNotNull(inputConnection)
664
-
665
- assertTrue(inputConnection!!.setComposingText("X", 1))
666
- assertTrue(inputConnection.setComposingRegion(0, 2))
667
- assertEquals("abc", editText.text?.toString())
668
- assertNull(editText.composingTextForEditor())
669
- }
670
-
671
- @Test
672
- fun `read only input connection mutations are consumed without mutating text`() {
673
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
674
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
675
- editText.setSelection(3)
676
- editText.editorId = 1
677
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
678
- assertNotNull(inputConnection)
679
-
680
- editText.isEditable = false
681
-
682
- assertTrue(inputConnection!!.commitText("X", 1))
683
- assertTrue(inputConnection.deleteSurroundingText(1, 0))
684
- assertTrue(inputConnection.deleteSurroundingTextInCodePoints(1, 0))
685
- assertTrue(inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)))
686
- assertEquals("abc", editText.text?.toString())
687
- assertEquals(3, editText.selectionStart)
688
- assertEquals(3, editText.selectionEnd)
689
- }
690
-
691
- @Test
692
- fun `composing text does not trigger reconciliation while edit text is transient`() {
693
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
694
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
695
- editText.setSelection(0)
696
- editText.editorId = 1
697
-
698
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
699
- assertNotNull(inputConnection)
700
-
701
- val handled = inputConnection!!.setComposingText("abc", 1)
702
-
703
- assertTrue(handled)
704
- assertEquals("abc", editText.text?.toString())
705
- assertEquals(0, editText.reconciliationCount)
706
- }
707
-
708
- @Test
709
- fun `commit text uses original authorized offset while composing text is visible`() {
710
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
711
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
712
- editText.setSelection(6)
713
- editText.editorId = 1
714
-
715
- var insertedText: String? = null
716
- var insertedScalar: Int? = null
717
- editText.onInsertTextInRustForTesting = { text, scalar ->
718
- insertedText = text
719
- insertedScalar = scalar
720
- }
721
-
722
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
723
- assertNotNull(inputConnection)
724
- inputConnection!!.setComposingText("brave ", 1)
725
- assertEquals("Hello brave world", editText.text?.toString())
726
-
727
- val handled = inputConnection.commitText("brave ", 1)
728
-
729
- assertTrue(handled)
730
- assertEquals("brave ", insertedText)
731
- assertEquals(6, insertedScalar)
732
- }
733
-
734
- @Test
735
- fun `composing region after visible composing text preserves original authorized range`() {
736
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
737
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
738
- editText.setSelection(6)
739
- editText.editorId = 1
740
-
741
- var insertedText: String? = null
742
- var insertedScalar: Int? = null
743
- var replacement: Triple<Int, Int, String>? = null
744
- editText.onInsertTextInRustForTesting = { text, scalar ->
745
- insertedText = text
746
- insertedScalar = scalar
747
- }
748
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
749
- replacement = Triple(scalarFrom, scalarTo, text)
750
- }
751
-
752
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
753
- assertNotNull(inputConnection)
754
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
755
- assertEquals("Hello brave world", editText.text?.toString())
756
-
757
- assertTrue(inputConnection.setComposingRegion(0, 5))
758
- assertTrue(inputConnection.commitText("brave ", 1))
759
-
760
- assertEquals("brave ", insertedText)
761
- assertEquals(6, insertedScalar)
762
- assertEquals(null, replacement)
763
- }
764
-
765
- @Test
766
- fun `repeated composing region updates authorized replacement before visible composing text`() {
767
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
768
- editText.applyUpdateJSON(renderUpdateJson("abcde"), notifyListener = false)
769
- editText.setSelection(0)
770
- editText.editorId = 1
771
-
772
- var replacement: Triple<Int, Int, String>? = null
773
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
774
- replacement = Triple(scalarFrom, scalarTo, text)
775
- }
776
-
777
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
778
- assertNotNull(inputConnection)
779
-
780
- assertTrue(inputConnection!!.setComposingRegion(0, 1))
781
- assertTrue(inputConnection.setComposingRegion(0, 5))
782
- assertTrue(inputConnection.commitText("ABCDE", 1))
783
-
784
- assertEquals(Triple(0, 5, "ABCDE"), replacement)
785
- }
786
-
787
- @Test
788
- fun `commit text replaces original authorized selection while composing text is visible`() {
789
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
790
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
791
- editText.setSelection(6, 11)
792
- editText.editorId = 1
793
-
794
- var replacement: Triple<Int, Int, String>? = null
795
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
796
- replacement = Triple(scalarFrom, scalarTo, text)
797
- }
798
-
799
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
800
- assertNotNull(inputConnection)
801
- inputConnection!!.setComposingText("there", 1)
802
- assertEquals("Hello there", editText.text?.toString())
803
-
804
- val handled = inputConnection.commitText("there", 1)
805
-
806
- assertTrue(handled)
807
- assertEquals(Triple(6, 11, "there"), replacement)
808
- }
809
-
810
- @Test
811
- fun `delete during composition edits transient text without mutating rust`() {
812
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
813
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
814
- editText.setSelection(0)
815
- editText.editorId = 1
816
-
817
- var deleteCalled = false
818
- editText.onDeleteRangeInRustForTesting = { _, _ ->
819
- deleteCalled = true
820
- }
821
- editText.onInsertTextInRustForTesting = { _, _ -> }
822
- var insertedText: String? = null
823
- var insertedScalar: Int? = null
824
- editText.onInsertTextInRustForTesting = { text, scalar ->
825
- insertedText = text
826
- insertedScalar = scalar
827
- }
828
-
829
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
830
- assertNotNull(inputConnection)
831
- inputConnection!!.setComposingText("abc", 1)
832
-
833
- val deleteHandled = inputConnection.deleteSurroundingText(1, 0)
834
- val commitHandled = inputConnection.commitText("ab", 1)
835
-
836
- assertTrue(deleteHandled)
837
- assertTrue(commitHandled)
838
- assertFalse(deleteCalled)
839
- assertEquals("ab", insertedText)
840
- assertEquals(0, insertedScalar)
841
- }
842
-
843
- @Test
844
- fun `key event backspace during composition edits transient text without mutating rust`() {
845
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
846
- editText.applyUpdateJSON(renderUpdateJson("Hello "), notifyListener = false)
847
- editText.setSelection(6)
848
- editText.editorId = 1
849
-
850
- var deleteCalled = false
851
- editText.onDeleteRangeInRustForTesting = { _, _ ->
852
- deleteCalled = true
853
- }
854
- editText.onInsertTextInRustForTesting = { _, _ -> }
855
-
856
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
857
- assertNotNull(inputConnection)
858
- assertTrue(inputConnection!!.setComposingText("abc", 1))
859
-
860
- assertTrue(inputConnection.sendKeyEvent(android.view.KeyEvent(
861
- android.view.KeyEvent.ACTION_DOWN,
862
- android.view.KeyEvent.KEYCODE_DEL
863
- )))
864
- assertTrue(inputConnection.commitText("ab", 1))
865
-
866
- assertFalse(deleteCalled)
867
- assertEquals(0, editText.reconciliationCount)
868
- }
869
-
870
- @Test
871
- fun `duplicate composition key event across view and input connection edits transient text once`() {
872
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
873
- editText.applyUpdateJSON(renderUpdateJson("Hello "), notifyListener = false)
874
- editText.setSelection(6)
875
- editText.editorId = 1
876
-
877
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
878
- assertNotNull(inputConnection)
879
- assertTrue(inputConnection!!.setComposingText("abc", 1))
880
-
881
- val event = KeyEvent(100L, 100L, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0)
882
- assertTrue(editText.dispatchKeyEvent(event))
883
- assertTrue(inputConnection.sendKeyEvent(event))
884
-
885
- assertEquals("Hello ab", editText.text?.toString())
886
- }
887
-
888
- @Test
889
- fun `duplicate forward delete composition key event stays on transient composition path`() {
890
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
891
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
892
- editText.setSelection(0)
893
- editText.editorId = 1
894
-
895
- var deleteCalled = false
896
- editText.onDeleteRangeInRustForTesting = { _, _ ->
897
- deleteCalled = true
898
- }
899
- var insertedText: String? = null
900
- editText.onInsertTextInRustForTesting = { text, _ ->
901
- insertedText = text
902
- }
903
-
904
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
905
- assertNotNull(inputConnection)
906
- assertTrue(inputConnection!!.setComposingText("abc", 1))
907
- editText.setSelection(0)
908
-
909
- val event = KeyEvent(100L, 100L, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_FORWARD_DEL, 0)
910
- assertTrue(editText.dispatchKeyEvent(event))
911
- assertTrue(inputConnection.sendKeyEvent(event))
912
-
913
- assertFalse(deleteCalled)
914
- assertEquals("bc", editText.text?.toString())
915
- assertTrue(inputConnection.finishComposingText())
916
- assertEquals("bc", insertedText)
917
- }
918
-
919
- @Test
920
- fun `forward delete composition edit refreshes composing text before finish commit`() {
921
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
922
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
923
- editText.setSelection(0)
924
- editText.editorId = 1
925
-
926
- var deleteCalled = false
927
- editText.onDeleteRangeInRustForTesting = { _, _ ->
928
- deleteCalled = true
929
- }
930
- var insertedText: String? = null
931
- var insertedScalar: Int? = null
932
- editText.onInsertTextInRustForTesting = { text, scalar ->
933
- insertedText = text
934
- insertedScalar = scalar
935
- }
936
-
937
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
938
- assertNotNull(inputConnection)
939
- assertTrue(inputConnection!!.setComposingText("abc", 1))
940
- editText.setSelection(0)
941
-
942
- assertTrue(inputConnection.deleteSurroundingText(0, 1))
943
- assertTrue(inputConnection.finishComposingText())
944
-
945
- assertFalse(deleteCalled)
946
- assertEquals("bc", insertedText)
947
- assertEquals(0, insertedScalar)
948
- }
949
-
950
- @Test
951
- fun `hardware backspace composition fallback does not split emoji surrogate pair`() {
952
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
953
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
954
- editText.setSelection(0)
955
- editText.editorId = 1
956
-
957
- var insertedText: String? = null
958
- editText.onInsertTextInRustForTesting = { text, _ ->
959
- insertedText = text
960
- }
961
-
962
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
963
- assertNotNull(inputConnection)
964
- assertTrue(inputConnection!!.setComposingText("😀", 1))
965
- editText.setSelection("😀".length)
966
-
967
- val event = KeyEvent(100L, 100L, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0)
968
- assertTrue(editText.dispatchKeyEvent(event))
969
-
970
- assertEquals("", editText.text?.toString())
971
- assertTrue(inputConnection.finishComposingText())
972
- assertNull(insertedText)
973
- assertEquals(0, editText.reconciliationCount)
974
- }
975
-
976
- @Test
977
- fun `hardware forward delete composition fallback does not split emoji surrogate pair`() {
978
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
979
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
980
- editText.setSelection(0)
981
- editText.editorId = 1
982
-
983
- var insertedText: String? = null
984
- editText.onInsertTextInRustForTesting = { text, _ ->
985
- insertedText = text
986
- }
987
-
988
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
989
- assertNotNull(inputConnection)
990
- assertTrue(inputConnection!!.setComposingText("😀", 1))
991
- editText.setSelection(0)
992
-
993
- val event = KeyEvent(100L, 100L, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_FORWARD_DEL, 0)
994
- assertTrue(editText.dispatchKeyEvent(event))
995
-
996
- assertEquals("", editText.text?.toString())
997
- assertTrue(inputConnection.finishComposingText())
998
- assertNull(insertedText)
999
- assertEquals(0, editText.reconciliationCount)
1000
- }
1001
-
1002
- @Test
1003
- fun `hardware backspace inside composing emoji deletes whole surrogate pair`() {
1004
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1005
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
1006
- editText.setSelection(0)
1007
- editText.editorId = 1
1008
-
1009
- var insertedText: String? = null
1010
- editText.onInsertTextInRustForTesting = { text, _ ->
1011
- insertedText = text
1012
- }
1013
-
1014
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1015
- assertNotNull(inputConnection)
1016
- assertTrue(inputConnection!!.setComposingText("😀", 1))
1017
- editText.setSelection(1)
1018
-
1019
- val event = KeyEvent(100L, 100L, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0)
1020
- assertTrue(editText.dispatchKeyEvent(event))
1021
-
1022
- assertEquals("", editText.text?.toString())
1023
- assertTrue(inputConnection.finishComposingText())
1024
- assertNull(insertedText)
1025
- }
1026
-
1027
- @Test
1028
- fun `hardware forward delete inside composing emoji deletes whole surrogate pair`() {
1029
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1030
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
1031
- editText.setSelection(0)
1032
- editText.editorId = 1
1033
-
1034
- var insertedText: String? = null
1035
- editText.onInsertTextInRustForTesting = { text, _ ->
1036
- insertedText = text
1037
- }
1038
-
1039
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1040
- assertNotNull(inputConnection)
1041
- assertTrue(inputConnection!!.setComposingText("😀", 1))
1042
- editText.setSelection(1)
1043
-
1044
- val event = KeyEvent(100L, 100L, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_FORWARD_DEL, 0)
1045
- assertTrue(editText.dispatchKeyEvent(event))
1046
-
1047
- assertEquals("", editText.text?.toString())
1048
- assertTrue(inputConnection.finishComposingText())
1049
- assertNull(insertedText)
1050
- }
1051
-
1052
- @Test
1053
- fun `printable hardware key inside composing emoji replaces whole surrogate pair`() {
1054
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1055
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
1056
- editText.setSelection(0)
1057
- editText.editorId = 1
1058
-
1059
- var insertedText: String? = null
1060
- editText.onInsertTextInRustForTesting = { text, _ ->
1061
- insertedText = text
1062
- }
1063
-
1064
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1065
- assertNotNull(inputConnection)
1066
- assertTrue(inputConnection!!.setComposingText("😀", 1))
1067
- editText.setSelection(1)
1068
-
1069
- val event = KeyEvent(100L, 100L, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0)
1070
- assertTrue(editText.dispatchKeyEvent(event))
1071
-
1072
- assertEquals("a", editText.text?.toString())
1073
- assertTrue(inputConnection.finishComposingText())
1074
- assertEquals("a", insertedText)
1075
- }
1076
-
1077
- @Test
1078
- fun `hardware backspace composition fallback deletes one code point from combining text`() {
1079
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1080
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
1081
- editText.setSelection(0)
1082
- editText.editorId = 1
1083
-
1084
- var insertedText: String? = null
1085
- editText.onInsertTextInRustForTesting = { text, _ ->
1086
- insertedText = text
1087
- }
1088
-
1089
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1090
- assertNotNull(inputConnection)
1091
- assertTrue(inputConnection!!.setComposingText("e\u0301", 1))
1092
- editText.setSelection("e\u0301".length)
1093
-
1094
- val event = KeyEvent(100L, 100L, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0)
1095
- assertTrue(editText.dispatchKeyEvent(event))
1096
-
1097
- assertEquals("e", editText.text?.toString())
1098
- assertTrue(inputConnection.finishComposingText())
1099
- assertEquals("e", insertedText)
1100
- assertEquals(0, editText.reconciliationCount)
1101
- }
1102
-
1103
- @Test
1104
- fun `commit completion routes autocomplete text through rust`() {
1105
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1106
- editText.applyUpdateJSON(renderUpdateJson("hel"), notifyListener = false)
1107
- editText.setSelection(0, 3)
1108
- editText.editorId = 1
1109
-
1110
- var replacement: Triple<Int, Int, String>? = null
1111
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1112
- replacement = Triple(scalarFrom, scalarTo, text)
1113
- }
1114
-
1115
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1116
- assertNotNull(inputConnection)
1117
-
1118
- assertTrue(inputConnection!!.commitCompletion(CompletionInfo(1L, 0, "hello")))
1119
-
1120
- assertEquals(Triple(0, 3, "hello"), replacement)
1121
- }
1122
-
1123
- @Test
1124
- fun `commit correction routes corrected text through rust`() {
1125
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1126
- editText.applyUpdateJSON(renderUpdateJson("teh"), notifyListener = false)
1127
- editText.setSelection(0, 3)
1128
- editText.editorId = 1
1129
-
1130
- var replacement: Triple<Int, Int, String>? = null
1131
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1132
- replacement = Triple(scalarFrom, scalarTo, text)
1133
- }
1134
-
1135
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1136
- assertNotNull(inputConnection)
1137
-
1138
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(0, "teh", "the")))
1139
-
1140
- assertEquals(Triple(0, 3, "the"), replacement)
1141
- }
1142
-
1143
- @Test
1144
- fun `commit correction replaces correction offset when caret is collapsed after word`() {
1145
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1146
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
1147
- editText.setSelection(4)
1148
- editText.editorId = 1
1149
-
1150
- var replacement: Triple<Int, Int, String>? = null
1151
- var insertedText: String? = null
1152
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1153
- replacement = Triple(scalarFrom, scalarTo, text)
1154
- }
1155
- editText.onInsertTextInRustForTesting = { text, _ ->
1156
- insertedText = text
1157
- }
1158
-
1159
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1160
- assertNotNull(inputConnection)
1161
-
1162
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(0, "teh", "the")))
1163
-
1164
- assertEquals(Triple(0, 3, "the"), replacement)
1165
- assertNull(insertedText)
1166
- }
1167
-
1168
- @Test
1169
- fun `stale commit correction range is consumed without inserting at caret`() {
1170
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1171
- editText.applyUpdateJSON(renderUpdateJson("tah "), notifyListener = false)
1172
- editText.setSelection(4)
1173
- editText.editorId = 1
1174
-
1175
- var replacement: Triple<Int, Int, String>? = null
1176
- var insertedText: String? = null
1177
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1178
- replacement = Triple(scalarFrom, scalarTo, text)
1179
- }
1180
- editText.onInsertTextInRustForTesting = { text, _ ->
1181
- insertedText = text
1182
- }
1183
-
1184
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1185
- assertNotNull(inputConnection)
1186
-
1187
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(0, "teh", "the")))
1188
-
1189
- assertNull(replacement)
1190
- assertNull(insertedText)
1191
- assertEquals("tah ", editText.text?.toString())
1192
- }
1193
-
1194
- @Test
1195
- fun `commit correction with missing old text replaces word at offset`() {
1196
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1197
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
1198
- editText.setSelection(4)
1199
- editText.editorId = 1
1200
-
1201
- var replacement: Triple<Int, Int, String>? = null
1202
- var insertedText: String? = null
1203
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1204
- replacement = Triple(scalarFrom, scalarTo, text)
1205
- }
1206
- editText.onInsertTextInRustForTesting = { text, _ ->
1207
- insertedText = text
1208
- }
1209
-
1210
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1211
- assertNotNull(inputConnection)
1212
-
1213
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(0, null, "the")))
1214
-
1215
- assertEquals(Triple(0, 3, "the"), replacement)
1216
- assertNull(insertedText)
1217
- }
1218
-
1219
- @Test
1220
- fun `commit correction with missing old text and invalid offset is consumed without inserting`() {
1221
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1222
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
1223
- editText.setSelection(4)
1224
- editText.editorId = 1
1225
-
1226
- var replacement: Triple<Int, Int, String>? = null
1227
- var insertedText: String? = null
1228
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1229
- replacement = Triple(scalarFrom, scalarTo, text)
1230
- }
1231
- editText.onInsertTextInRustForTesting = { text, _ ->
1232
- insertedText = text
1233
- }
1234
-
1235
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1236
- assertNotNull(inputConnection)
1237
-
1238
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(-1, null, "the")))
1239
-
1240
- assertNull(replacement)
1241
- assertNull(insertedText)
1242
- assertEquals("teh ", editText.text?.toString())
1243
- }
1244
-
1245
- @Test
1246
- fun `commit correction with missing old text replaces word at sentence offset`() {
1247
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1248
- editText.applyUpdateJSON(renderUpdateJson("say teh now"), notifyListener = false)
1249
- editText.setSelection(7)
1250
- editText.editorId = 1
1251
-
1252
- var replacement: Triple<Int, Int, String>? = null
1253
- var insertedText: String? = null
1254
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1255
- replacement = Triple(scalarFrom, scalarTo, text)
1256
- }
1257
- editText.onInsertTextInRustForTesting = { text, _ ->
1258
- insertedText = text
1259
- }
1260
-
1261
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1262
- assertNotNull(inputConnection)
1263
-
1264
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(4, null, "the")))
1265
-
1266
- assertEquals(Triple(4, 7, "the"), replacement)
1267
- assertNull(insertedText)
1268
- }
1269
-
1270
- @Test
1271
- fun `commit correction with missing old text replaces word containing offset`() {
1272
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1273
- editText.applyUpdateJSON(renderUpdateJson("say teh now"), notifyListener = false)
1274
- editText.setSelection(7)
1275
- editText.editorId = 1
1276
-
1277
- var replacement: Triple<Int, Int, String>? = null
1278
- var insertedText: String? = null
1279
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1280
- replacement = Triple(scalarFrom, scalarTo, text)
1281
- }
1282
- editText.onInsertTextInRustForTesting = { text, _ ->
1283
- insertedText = text
1284
- }
1285
-
1286
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1287
- assertNotNull(inputConnection)
1288
-
1289
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(5, null, "the")))
1290
-
1291
- assertEquals(Triple(4, 7, "the"), replacement)
1292
- assertNull(insertedText)
1293
- }
1294
-
1295
- @Test
1296
- fun `commit correction with missing old text preserves trailing punctuation`() {
1297
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1298
- editText.applyUpdateJSON(renderUpdateJson("teh."), notifyListener = false)
1299
- editText.setSelection(4)
1300
- editText.editorId = 1
1301
-
1302
- var replacement: Triple<Int, Int, String>? = null
1303
- var insertedText: String? = null
1304
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1305
- replacement = Triple(scalarFrom, scalarTo, text)
1306
- }
1307
- editText.onInsertTextInRustForTesting = { text, _ ->
1308
- insertedText = text
1309
- }
1310
-
1311
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1312
- assertNotNull(inputConnection)
1313
-
1314
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(0, null, "the")))
1315
-
1316
- assertEquals(Triple(0, 3, "the"), replacement)
1317
- assertNull(insertedText)
1318
- }
1319
-
1320
- @Test
1321
- fun `commit correction with missing old text preserves punctuation inside sentence`() {
1322
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1323
- editText.applyUpdateJSON(renderUpdateJson("say teh, now"), notifyListener = false)
1324
- editText.setSelection(8)
1325
- editText.editorId = 1
1326
-
1327
- var replacement: Triple<Int, Int, String>? = null
1328
- var insertedText: String? = null
1329
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1330
- replacement = Triple(scalarFrom, scalarTo, text)
1331
- }
1332
- editText.onInsertTextInRustForTesting = { text, _ ->
1333
- insertedText = text
1334
- }
1335
-
1336
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1337
- assertNotNull(inputConnection)
1338
-
1339
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(5, null, "the")))
1340
-
1341
- assertEquals(Triple(4, 7, "the"), replacement)
1342
- assertNull(insertedText)
1343
- }
1344
-
1345
- @Test
1346
- fun `commit correction with missing old text on punctuation is consumed without inserting`() {
1347
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1348
- editText.applyUpdateJSON(renderUpdateJson("teh."), notifyListener = false)
1349
- editText.setSelection(4)
1350
- editText.editorId = 1
1351
-
1352
- var replacement: Triple<Int, Int, String>? = null
1353
- var insertedText: String? = null
1354
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1355
- replacement = Triple(scalarFrom, scalarTo, text)
1356
- }
1357
- editText.onInsertTextInRustForTesting = { text, _ ->
1358
- insertedText = text
1359
- }
1360
-
1361
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1362
- assertNotNull(inputConnection)
1363
-
1364
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(3, null, "the")))
1365
-
1366
- assertNull(replacement)
1367
- assertNull(insertedText)
1368
- assertEquals("teh.", editText.text?.toString())
1369
- }
1370
-
1371
- @Test
1372
- fun `commit correction with missing old text keeps internal hyphen in token`() {
1373
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1374
- editText.applyUpdateJSON(renderUpdateJson("dont-stop "), notifyListener = false)
1375
- editText.setSelection(10)
1376
- editText.editorId = 1
1377
-
1378
- var replacement: Triple<Int, Int, String>? = null
1379
- var insertedText: String? = null
1380
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1381
- replacement = Triple(scalarFrom, scalarTo, text)
1382
- }
1383
- editText.onInsertTextInRustForTesting = { text, _ ->
1384
- insertedText = text
1385
- }
1386
-
1387
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1388
- assertNotNull(inputConnection)
1389
-
1390
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(4, null, "don't-stop")))
1391
-
1392
- assertEquals(Triple(0, 9, "don't-stop"), replacement)
1393
- assertNull(insertedText)
1394
- }
1395
-
1396
- @Test
1397
- fun `commit correction with missing old text keeps internal apostrophe in token`() {
1398
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1399
- editText.applyUpdateJSON(renderUpdateJson("cant's "), notifyListener = false)
1400
- editText.setSelection(7)
1401
- editText.editorId = 1
1402
-
1403
- var replacement: Triple<Int, Int, String>? = null
1404
- var insertedText: String? = null
1405
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1406
- replacement = Triple(scalarFrom, scalarTo, text)
1407
- }
1408
- editText.onInsertTextInRustForTesting = { text, _ ->
1409
- insertedText = text
1410
- }
1411
-
1412
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1413
- assertNotNull(inputConnection)
1414
-
1415
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(4, null, "can't")))
1416
-
1417
- assertEquals(Triple(0, 6, "can't"), replacement)
1418
- assertNull(insertedText)
1419
- }
1420
-
1421
- @Test
1422
- fun `commit correction with missing old text on whitespace is consumed without inserting`() {
1423
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1424
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
1425
- editText.setSelection(4)
1426
- editText.editorId = 1
1427
-
1428
- var replacement: Triple<Int, Int, String>? = null
1429
- var insertedText: String? = null
1430
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1431
- replacement = Triple(scalarFrom, scalarTo, text)
1432
- }
1433
- editText.onInsertTextInRustForTesting = { text, _ ->
1434
- insertedText = text
1435
- }
1436
-
1437
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1438
- assertNotNull(inputConnection)
1439
-
1440
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(3, null, "the")))
1441
-
1442
- assertNull(replacement)
1443
- assertNull(insertedText)
1444
- assertEquals("teh ", editText.text?.toString())
1445
- }
1446
-
1447
- @Test
1448
- fun `commit correction with missing old text does not split surrogate pair word`() {
1449
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1450
- editText.applyUpdateJSON(renderUpdateJson("te😀h "), notifyListener = false)
1451
- editText.setSelection(5)
1452
- editText.editorId = 1
1453
-
1454
- var replacement: Triple<Int, Int, String>? = null
1455
- var insertedText: String? = null
1456
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1457
- replacement = Triple(scalarFrom, scalarTo, text)
1458
- }
1459
- editText.onInsertTextInRustForTesting = { text, _ ->
1460
- insertedText = text
1461
- }
1462
-
1463
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1464
- assertNotNull(inputConnection)
1465
-
1466
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(3, null, "term")))
1467
-
1468
- assertEquals(Triple(0, 4, "term"), replacement)
1469
- assertNull(insertedText)
1470
- }
1471
-
1472
- @Test
1473
- fun `commit correction with old text and invalid offset is consumed without inserting`() {
1474
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1475
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
1476
- editText.setSelection(4)
1477
- editText.editorId = 1
1478
-
1479
- var replacement: Triple<Int, Int, String>? = null
1480
- var insertedText: String? = null
1481
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1482
- replacement = Triple(scalarFrom, scalarTo, text)
1483
- }
1484
- editText.onInsertTextInRustForTesting = { text, _ ->
1485
- insertedText = text
1486
- }
1487
-
1488
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1489
- assertNotNull(inputConnection)
1490
-
1491
- assertTrue(inputConnection!!.commitCorrection(CorrectionInfo(-1, "teh", "the")))
1492
-
1493
- assertNull(replacement)
1494
- assertNull(insertedText)
1495
- assertEquals("teh ", editText.text?.toString())
1496
- }
1497
-
1498
- @Test
1499
- fun `commit correction during visible composition commits corrected composing text`() {
1500
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1501
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
1502
- editText.setSelection(0)
1503
- editText.editorId = 1
1504
-
1505
- var insertedText: String? = null
1506
- var insertedScalar: Int? = null
1507
- editText.onInsertTextInRustForTesting = { text, scalar ->
1508
- insertedText = text
1509
- insertedScalar = scalar
1510
- }
1511
-
1512
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1513
- assertNotNull(inputConnection)
1514
- assertTrue(inputConnection!!.setComposingText("teh", 1))
1515
-
1516
- assertTrue(inputConnection.commitCorrection(CorrectionInfo(0, "teh", "the")))
1517
-
1518
- assertNull(insertedText)
1519
- assertNull(insertedScalar)
1520
-
1521
- assertTrue(inputConnection.commitText("the", 1))
1522
-
1523
- assertEquals("the", insertedText)
1524
- assertEquals(0, insertedScalar)
1525
- assertEquals(0, editText.reconciliationCount)
1526
- }
1527
-
1528
- @Test
1529
- fun `matching commit text after composition correction applies once so space can follow`() {
1530
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1531
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
1532
- editText.setSelection(0)
1533
- editText.editorId = 1
1534
-
1535
- val rendered = StringBuilder()
1536
- val inserts = mutableListOf<Pair<String, Int>>()
1537
- editText.onInsertTextInRustForTesting = { text, scalar ->
1538
- inserts.add(text to scalar)
1539
- rendered.insert(scalar.coerceIn(0, rendered.length), text)
1540
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
1541
- }
1542
-
1543
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1544
- assertNotNull(inputConnection)
1545
- assertTrue(inputConnection!!.setComposingText("wouldnt", 1))
1546
-
1547
- assertTrue(inputConnection.commitCorrection(CorrectionInfo(0, "wouldnt", "wouldn't")))
1548
-
1549
- assertTrue(inserts.isEmpty())
1550
- assertEquals("wouldnt", editText.text?.toString())
1551
- assertFalse(editText.hasDeferredRustUpdateApplicationForTesting())
1552
-
1553
- assertTrue(inputConnection.commitText("wouldn't", 1))
1554
-
1555
- assertEquals(listOf("wouldn't" to 0), inserts)
1556
- assertEquals("wouldn't", editText.text?.toString())
1557
- assertTrue(editText.hasDeferredRustUpdateApplicationForTesting())
1558
-
1559
- assertTrue(inputConnection.commitText(" ", 1))
1560
-
1561
- assertEquals(listOf("wouldn't" to 0, " " to 8), inserts)
1562
- assertEquals("wouldn't ", editText.text?.toString())
1563
- assertFalse(editText.hasDeferredRustUpdateApplicationForTesting())
1564
-
1565
- shadowOf(Looper.getMainLooper()).idle()
1566
-
1567
- assertEquals("wouldn't ", editText.text?.toString())
1568
- }
1569
-
1570
- @Test
1571
- fun `single letter composition correction followed by commit text keeps uppercase replacement`() {
1572
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1573
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
1574
- editText.setSelection(0)
1575
- editText.editorId = 1
1576
-
1577
- val rendered = StringBuilder()
1578
- val inserts = mutableListOf<Pair<String, Int>>()
1579
- editText.onInsertTextInRustForTesting = { text, scalar ->
1580
- inserts.add(text to scalar)
1581
- rendered.insert(scalar.coerceIn(0, rendered.length), text)
1582
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
1583
- }
1584
-
1585
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1586
- assertNotNull(inputConnection)
1587
- assertTrue(inputConnection!!.setComposingText("i", 1))
1588
-
1589
- assertTrue(inputConnection.commitCorrection(CorrectionInfo(0, "i", "I")))
1590
- assertTrue(inputConnection.commitText("I", 1))
1591
-
1592
- assertEquals(listOf("I" to 0), inserts)
1593
- assertEquals("I", editText.text?.toString())
1594
-
1595
- shadowOf(Looper.getMainLooper()).idle()
1596
-
1597
- assertEquals("I", editText.text?.toString())
1598
- }
1599
-
1600
- @Test
1601
- fun `single letter composition correction applies when ime sends no follow up commit text`() {
1602
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1603
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
1604
- editText.setSelection(0)
1605
- editText.editorId = 1
1606
-
1607
- val rendered = StringBuilder()
1608
- val inserts = mutableListOf<Pair<String, Int>>()
1609
- editText.onInsertTextInRustForTesting = { text, scalar ->
1610
- inserts.add(text to scalar)
1611
- rendered.insert(scalar.coerceIn(0, rendered.length), text)
1612
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
1613
- }
1614
-
1615
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1616
- assertNotNull(inputConnection)
1617
- assertTrue(inputConnection!!.setComposingText("i", 1))
1618
-
1619
- assertTrue(inputConnection.commitCorrection(CorrectionInfo(0, "i", "I")))
1620
-
1621
- assertTrue(inserts.isEmpty())
1622
- assertEquals("i", editText.text?.toString())
1623
-
1624
- shadowOf(Looper.getMainLooper()).idle()
1625
-
1626
- assertEquals(listOf("I" to 0), inserts)
1627
- assertEquals("I", editText.text?.toString())
1628
- }
1629
-
1630
- @Test
1631
- fun `printable hardware key during composition stays transient until finish commit`() {
1632
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1633
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
1634
- editText.setSelection(0)
1635
- editText.editorId = 1
1636
-
1637
- var insertedText: String? = null
1638
- editText.onInsertTextInRustForTesting = { text, _ ->
1639
- insertedText = text
1640
- }
1641
-
1642
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1643
- assertNotNull(inputConnection)
1644
- assertTrue(inputConnection!!.setComposingText("b", 1))
1645
-
1646
- val event = KeyEvent(100L, 100L, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0)
1647
- assertTrue(editText.dispatchKeyEvent(event))
1648
-
1649
- assertEquals(0, editText.reconciliationCount)
1650
- assertEquals("ba", editText.text?.toString())
1651
- assertTrue(inputConnection.finishComposingText())
1652
- assertEquals("ba", insertedText)
1653
- }
1654
-
1655
- @Test
1656
- fun `printable input connection key during composition stays transient until finish commit`() {
1657
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1658
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
1659
- editText.setSelection(0)
1660
- editText.editorId = 1
1661
-
1662
- var insertedText: String? = null
1663
- editText.onInsertTextInRustForTesting = { text, _ ->
1664
- insertedText = text
1665
- }
1666
-
1667
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1668
- assertNotNull(inputConnection)
1669
- assertTrue(inputConnection!!.setComposingText("b", 1))
1670
-
1671
- val event = KeyEvent(100L, 100L, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0)
1672
- assertTrue(inputConnection.sendKeyEvent(event))
1673
-
1674
- assertEquals(0, editText.reconciliationCount)
1675
- assertEquals("ba", editText.text?.toString())
1676
- assertTrue(inputConnection.finishComposingText())
1677
- assertEquals("ba", insertedText)
1678
- }
1679
-
1680
- @Test
1681
- fun `read only completion and correction are consumed without mutating text`() {
1682
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1683
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
1684
- editText.setSelection(0, 3)
1685
- editText.editorId = 1
1686
- editText.isEditable = false
1687
-
1688
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1689
- assertNotNull(inputConnection)
1690
-
1691
- assertTrue(inputConnection!!.commitCompletion(CompletionInfo(1L, 0, "replacement")))
1692
- assertTrue(inputConnection.commitCorrection(CorrectionInfo(0, "abc", "replacement")))
1693
- assertEquals("abc", editText.text?.toString())
1694
- }
1695
-
1696
- @Test
1697
- fun `key event enter during composition does not split rust before commit`() {
1698
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1699
- editText.applyUpdateJSON(renderUpdateJson("Hello "), notifyListener = false)
1700
- editText.setSelection(6)
1701
- editText.editorId = 1
1702
-
1703
- var deleteAndSplitCalled = false
1704
- editText.onDeleteAndSplitScalarInRustForTesting = { _, _ ->
1705
- deleteAndSplitCalled = true
1706
- }
1707
-
1708
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1709
- assertNotNull(inputConnection)
1710
- assertTrue(inputConnection!!.setComposingText("abc", 1))
1711
-
1712
- assertTrue(inputConnection.sendKeyEvent(android.view.KeyEvent(
1713
- android.view.KeyEvent.ACTION_DOWN,
1714
- android.view.KeyEvent.KEYCODE_ENTER
1715
- )))
1716
-
1717
- assertFalse(deleteAndSplitCalled)
1718
- assertEquals(0, editText.reconciliationCount)
1719
- }
1720
-
1721
- @Test
1722
- fun `commit text after composing region replaces original authorized range`() {
1723
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1724
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
1725
- editText.setSelection(3)
1726
- editText.editorId = 1
1727
-
1728
- var replacement: Triple<Int, Int, String>? = null
1729
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1730
- replacement = Triple(scalarFrom, scalarTo, text)
1731
- }
1732
-
1733
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1734
- assertNotNull(inputConnection)
1735
-
1736
- assertTrue(inputConnection!!.setComposingRegion(0, 3))
1737
- assertTrue(inputConnection.commitText("the", 1))
1738
-
1739
- assertEquals(Triple(0, 3, "the"), replacement)
1740
- assertEquals(0, editText.reconciliationCount)
1741
- }
1742
-
1743
- @Test
1744
- fun `multiline composition commits as structured content`() {
1745
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1746
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
1747
- editText.setSelection(6, 11)
1748
- editText.editorId = 1
1749
-
1750
- var insertedContent: Triple<Int, Int, String>? = null
1751
- editText.onInsertContentJsonAtSelectionScalarForTesting = { scalarFrom, scalarTo, json ->
1752
- insertedContent = Triple(scalarFrom, scalarTo, json)
1753
- }
1754
-
1755
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1756
- assertNotNull(inputConnection)
1757
-
1758
- assertTrue(inputConnection!!.setComposingRegion(6, 11))
1759
- assertTrue(inputConnection.commitText("one\ntwo", 1))
1760
-
1761
- val (scalarFrom, scalarTo, json) = insertedContent!!
1762
- assertEquals(6, scalarFrom)
1763
- assertEquals(11, scalarTo)
1764
- val content = JSONObject(json).getJSONArray("content")
1765
- assertEquals("one", content.getJSONObject(0).getJSONArray("content").getJSONObject(0).getString("text"))
1766
- assertEquals("two", content.getJSONObject(1).getJSONArray("content").getJSONObject(0).getString("text"))
1767
- }
1768
-
1769
- @Test
1770
- fun `commit newline after composing region delete splits original authorized range`() {
1771
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1772
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
1773
- editText.setSelection(6, 11)
1774
- editText.editorId = 1
1775
-
1776
- var deletedAndSplitRange: Pair<Int, Int>? = null
1777
- editText.onDeleteAndSplitScalarInRustForTesting = { scalarFrom, scalarTo ->
1778
- deletedAndSplitRange = scalarFrom to scalarTo
1779
- }
1780
-
1781
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1782
- assertNotNull(inputConnection)
1783
-
1784
- assertTrue(inputConnection!!.setComposingRegion(6, 11))
1785
- assertTrue(inputConnection.commitText("\n", 1))
1786
-
1787
- assertEquals(6 to 11, deletedAndSplitRange)
1788
- assertEquals(0, editText.reconciliationCount)
1789
- }
1790
-
1791
- @Test
1792
- fun `empty commit text after composing region deletes authorized text`() {
1793
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1794
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
1795
- editText.setSelection(3)
1796
- editText.editorId = 1
1797
-
1798
- var replacement: Triple<Int, Int, String>? = null
1799
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1800
- replacement = Triple(scalarFrom, scalarTo, text)
1801
- }
1802
- var deletedRange: Pair<Int, Int>? = null
1803
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
1804
- deletedRange = scalarFrom to scalarTo
1805
- }
1806
-
1807
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1808
- assertNotNull(inputConnection)
1809
-
1810
- assertTrue(inputConnection!!.setComposingRegion(0, 3))
1811
- assertTrue(inputConnection.commitText("", 1))
1812
-
1813
- assertEquals(null, replacement)
1814
- assertEquals(0 to 3, deletedRange)
1815
- assertEquals(0, editText.reconciliationCount)
1816
- }
1817
-
1818
- @Test
1819
- fun `finish composing text after unchanged composing region skips no-op replacement`() {
1820
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1821
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
1822
- editText.setSelection(3)
1823
- editText.editorId = 1
1824
-
1825
- var replacement: Triple<Int, Int, String>? = null
1826
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1827
- replacement = Triple(scalarFrom, scalarTo, text)
1828
- }
1829
- var syncedSelection: Pair<Int, Int>? = null
1830
- editText.onSetSelectionScalarInRustForTesting = { anchor, head ->
1831
- syncedSelection = anchor to head
1832
- }
1833
-
1834
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1835
- assertNotNull(inputConnection)
1836
-
1837
- assertTrue(inputConnection!!.setComposingRegion(0, 3))
1838
- assertTrue(inputConnection.finishComposingText())
1839
-
1840
- assertEquals(null, replacement)
1841
- assertNotNull(syncedSelection)
1842
- assertEquals(0, editText.reconciliationCount)
1843
- }
1844
-
1845
- @Test
1846
- fun `unchanged newline composition is treated as no-op before split handling`() {
1847
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1848
- editText.applyUpdateJSON(renderUpdateJson("\n"), notifyListener = false)
1849
- editText.setSelection(0, 1)
1850
- editText.editorId = 1
1851
-
1852
- var deleteAndSplitCalled = false
1853
- editText.onDeleteAndSplitScalarInRustForTesting = { _, _ ->
1854
- deleteAndSplitCalled = true
1855
- }
1856
- var selectedScalar: Pair<Int, Int>? = null
1857
- editText.onSetSelectionScalarInRustForTesting = { anchor, head ->
1858
- selectedScalar = anchor to head
1859
- }
1860
-
1861
- editText.handleCompositionCommit("\n", 0, 1)
1862
-
1863
- assertFalse(deleteAndSplitCalled)
1864
- assertEquals(1 to 1, selectedScalar)
1865
- assertEquals("\n", editText.text?.toString())
1866
- }
1867
-
1868
- @Test
1869
- fun `finish composing text after unchanged composing region moves default cursor to range end`() {
1870
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1871
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
1872
- editText.setSelection(0)
1873
- editText.editorId = 1
1874
-
1875
- var selectedScalar: Pair<Int, Int>? = null
1876
- editText.onSetSelectionScalarInRustForTesting = { anchor, head ->
1877
- selectedScalar = anchor to head
1878
- }
1879
-
1880
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1881
- assertNotNull(inputConnection)
1882
-
1883
- assertTrue(inputConnection!!.setComposingRegion(0, 3))
1884
- assertTrue(inputConnection.commitText("abc", 1))
1885
-
1886
- assertEquals(3 to 3, selectedScalar)
1887
- }
1888
-
1889
- @Test
1890
- fun `finish composing text with empty composition restores and handles cancellation`() {
1891
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1892
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
1893
- editText.setSelection(0)
1894
- editText.editorId = 1
1895
-
1896
- var insertedText: String? = null
1897
- editText.onInsertTextInRustForTesting = { text, _ ->
1898
- insertedText = text
1899
- }
1900
-
1901
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1902
- assertNotNull(inputConnection)
1903
-
1904
- assertTrue(inputConnection!!.setComposingText("", 1))
1905
- assertTrue(inputConnection.finishComposingText())
1906
-
1907
- assertEquals("", editText.text?.toString())
1908
- assertEquals(null, insertedText)
1909
- assertEquals(0, editText.reconciliationCount)
1910
- }
1911
-
1912
- @Test
1913
- fun `finish composing text with empty selected composition deletes replacement range`() {
1914
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1915
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
1916
- editText.setSelection(6, 11)
1917
- editText.editorId = 1
1918
-
1919
- var deletedRange: Pair<Int, Int>? = null
1920
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
1921
- deletedRange = scalarFrom to scalarTo
1922
- }
1923
-
1924
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1925
- assertNotNull(inputConnection)
1926
- assertTrue(inputConnection!!.setComposingText("", 1))
1927
-
1928
- assertTrue(inputConnection.finishComposingText())
1929
-
1930
- assertEquals(6 to 11, deletedRange)
1931
- assertEquals(0, editText.reconciliationCount)
1932
- }
1933
-
1934
- @Test
1935
- fun `composition replacement range invalidates after authorized render changes`() {
1936
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1937
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
1938
- editText.setSelection(6)
1939
- editText.editorId = 1
1940
-
1941
- var insertedText: String? = null
1942
- var replacement: Triple<Int, Int, String>? = null
1943
- editText.onInsertTextInRustForTesting = { text, _ ->
1944
- insertedText = text
1945
- }
1946
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1947
- replacement = Triple(scalarFrom, scalarTo, text)
1948
- }
1949
-
1950
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1951
- assertNotNull(inputConnection)
1952
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
1953
- assertEquals("Hello brave world", editText.text?.toString())
1954
-
1955
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
1956
-
1957
- assertTrue(inputConnection.finishComposingText())
1958
- assertEquals("Hello updated world", editText.text?.toString())
1959
- assertNull(insertedText)
1960
- assertNull(replacement)
1961
- }
1962
-
1963
- @Test
1964
- fun `commit text after authorized render change is consumed without inserting stale composition`() {
1965
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1966
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
1967
- editText.setSelection(6)
1968
- editText.editorId = 1
1969
-
1970
- var insertedText: String? = null
1971
- var replacement: Triple<Int, Int, String>? = null
1972
- editText.onInsertTextInRustForTesting = { text, _ ->
1973
- insertedText = text
1974
- }
1975
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
1976
- replacement = Triple(scalarFrom, scalarTo, text)
1977
- }
1978
-
1979
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
1980
- assertNotNull(inputConnection)
1981
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
1982
- assertEquals("Hello brave world", editText.text?.toString())
1983
-
1984
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
1985
-
1986
- assertTrue(inputConnection.commitText("brave ", 1))
1987
- assertEquals("Hello updated world", editText.text?.toString())
1988
- assertNull(insertedText)
1989
- assertNull(replacement)
1990
- }
1991
-
1992
- @Test
1993
- fun `commit correction after authorized render change is consumed without replacing matching text`() {
1994
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
1995
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
1996
- editText.setSelection(6)
1997
- editText.editorId = 1
1998
-
1999
- var insertedText: String? = null
2000
- var replacement: Triple<Int, Int, String>? = null
2001
- editText.onInsertTextInRustForTesting = { text, _ ->
2002
- insertedText = text
2003
- }
2004
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2005
- replacement = Triple(scalarFrom, scalarTo, text)
2006
- }
2007
-
2008
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2009
- assertNotNull(inputConnection)
2010
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
2011
- assertEquals("Hello brave world", editText.text?.toString())
2012
-
2013
- editText.applyUpdateJSON(renderUpdateJson("Hello brave world"), notifyListener = false)
2014
-
2015
- assertTrue(inputConnection.commitCorrection(CorrectionInfo(6, "brave ", "braver ")))
2016
- assertEquals("Hello brave world", editText.text?.toString())
2017
- assertNull(insertedText)
2018
- assertNull(replacement)
2019
- }
2020
-
2021
- @Test
2022
- fun `composing text after authorized render change does not reauthorize stale commit`() {
2023
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2024
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2025
- editText.setSelection(6)
2026
- editText.editorId = 1
2027
-
2028
- var insertedText: String? = null
2029
- var replacement: Triple<Int, Int, String>? = null
2030
- editText.onInsertTextInRustForTesting = { text, _ ->
2031
- insertedText = text
2032
- }
2033
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2034
- replacement = Triple(scalarFrom, scalarTo, text)
2035
- }
2036
-
2037
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2038
- assertNotNull(inputConnection)
2039
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
2040
- assertEquals("Hello brave world", editText.text?.toString())
2041
-
2042
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
2043
-
2044
- assertTrue(inputConnection.setComposingText("braver ", 1))
2045
- assertTrue(inputConnection.commitText("braver ", 1))
2046
- assertEquals("Hello updated world", editText.text?.toString())
2047
- assertNull(insertedText)
2048
- assertNull(replacement)
2049
- }
2050
-
2051
- @Test
2052
- fun `composing region after authorized render change does not reauthorize stale commit`() {
2053
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2054
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2055
- editText.setSelection(6)
2056
- editText.editorId = 1
2057
-
2058
- var insertedText: String? = null
2059
- var replacement: Triple<Int, Int, String>? = null
2060
- editText.onInsertTextInRustForTesting = { text, _ ->
2061
- insertedText = text
2062
- }
2063
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2064
- replacement = Triple(scalarFrom, scalarTo, text)
2065
- }
2066
-
2067
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2068
- assertNotNull(inputConnection)
2069
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
2070
- assertEquals("Hello brave world", editText.text?.toString())
2071
-
2072
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
2073
-
2074
- assertTrue(inputConnection.setComposingRegion(6, 13))
2075
- assertTrue(inputConnection.commitText("braver ", 1))
2076
- assertEquals("Hello updated world", editText.text?.toString())
2077
- assertNull(insertedText)
2078
- assertNull(replacement)
2079
- }
2080
-
2081
- @Test
2082
- fun `delete surrounding text after authorized render change is consumed without deleting authorized text`() {
2083
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2084
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2085
- editText.setSelection(6)
2086
- editText.editorId = 1
2087
-
2088
- var deleteRange: Pair<Int, Int>? = null
2089
- var deleteBackward: Pair<Int, Int>? = null
2090
- var insertedText: String? = null
2091
- var replacement: Triple<Int, Int, String>? = null
2092
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
2093
- deleteRange = scalarFrom to scalarTo
2094
- }
2095
- editText.onDeleteBackwardAtSelectionScalarInRustForTesting = { anchor, head ->
2096
- deleteBackward = anchor to head
2097
- }
2098
- editText.onInsertTextInRustForTesting = { text, _ ->
2099
- insertedText = text
2100
- }
2101
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2102
- replacement = Triple(scalarFrom, scalarTo, text)
2103
- }
2104
-
2105
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2106
- assertNotNull(inputConnection)
2107
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
2108
- assertEquals("Hello brave world", editText.text?.toString())
2109
-
2110
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
2111
-
2112
- assertTrue(inputConnection.deleteSurroundingText(1, 0))
2113
- assertEquals("Hello updated world", editText.text?.toString())
2114
- assertNull(deleteRange)
2115
- assertNull(deleteBackward)
2116
- assertNull(insertedText)
2117
- assertNull(replacement)
2118
- }
2119
-
2120
- @Test
2121
- fun `delete surrounding text in code points after authorized render change is consumed without mutation`() {
2122
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2123
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2124
- editText.setSelection(6)
2125
- editText.editorId = 1
2126
-
2127
- var deleteRange: Pair<Int, Int>? = null
2128
- var deleteBackward: Pair<Int, Int>? = null
2129
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
2130
- deleteRange = scalarFrom to scalarTo
2131
- }
2132
- editText.onDeleteBackwardAtSelectionScalarInRustForTesting = { anchor, head ->
2133
- deleteBackward = anchor to head
2134
- }
2135
-
2136
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2137
- assertNotNull(inputConnection)
2138
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
2139
- assertEquals("Hello brave world", editText.text?.toString())
2140
-
2141
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
2142
-
2143
- assertTrue(inputConnection.deleteSurroundingTextInCodePoints(1, 0))
2144
- assertEquals("Hello updated world", editText.text?.toString())
2145
- assertNull(deleteRange)
2146
- assertNull(deleteBackward)
2147
- }
2148
-
2149
- @Test
2150
- fun `no-op delete after authorized render change does not allow stale commit`() {
2151
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2152
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2153
- editText.setSelection(6)
2154
- editText.editorId = 1
2155
-
2156
- var insertedText: String? = null
2157
- var replacement: Triple<Int, Int, String>? = null
2158
- editText.onInsertTextInRustForTesting = { text, _ ->
2159
- insertedText = text
2160
- }
2161
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2162
- replacement = Triple(scalarFrom, scalarTo, text)
2163
- }
2164
-
2165
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2166
- assertNotNull(inputConnection)
2167
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
2168
- assertEquals("Hello brave world", editText.text?.toString())
2169
-
2170
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
2171
-
2172
- assertTrue(inputConnection.deleteSurroundingText(0, 0))
2173
- assertTrue(inputConnection.commitText("braver ", 1))
2174
- assertEquals("Hello updated world", editText.text?.toString())
2175
- assertNull(insertedText)
2176
- assertNull(replacement)
2177
- }
2178
-
2179
- @Test
2180
- fun `no-op code point delete after authorized render change does not allow stale commit`() {
2181
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2182
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2183
- editText.setSelection(6)
2184
- editText.editorId = 1
2185
-
2186
- var insertedText: String? = null
2187
- var replacement: Triple<Int, Int, String>? = null
2188
- editText.onInsertTextInRustForTesting = { text, _ ->
2189
- insertedText = text
2190
- }
2191
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2192
- replacement = Triple(scalarFrom, scalarTo, text)
2193
- }
2194
-
2195
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2196
- assertNotNull(inputConnection)
2197
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
2198
- assertEquals("Hello brave world", editText.text?.toString())
2199
-
2200
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
2201
-
2202
- assertTrue(inputConnection.deleteSurroundingTextInCodePoints(0, 0))
2203
- assertTrue(inputConnection.commitText("braver ", 1))
2204
- assertEquals("Hello updated world", editText.text?.toString())
2205
- assertNull(insertedText)
2206
- assertNull(replacement)
2207
- }
2208
-
2209
- @Test
2210
- fun `delete key event after authorized render change is consumed without rust mutation`() {
2211
- for (keyCode in listOf(KeyEvent.KEYCODE_DEL, KeyEvent.KEYCODE_FORWARD_DEL)) {
2212
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2213
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2214
- editText.setSelection(6)
2215
- editText.editorId = 1
2216
-
2217
- var deleteRange: Pair<Int, Int>? = null
2218
- var deleteBackward: Pair<Int, Int>? = null
2219
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
2220
- deleteRange = scalarFrom to scalarTo
2221
- }
2222
- editText.onDeleteBackwardAtSelectionScalarInRustForTesting = { anchor, head ->
2223
- deleteBackward = anchor to head
2224
- }
2225
-
2226
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2227
- assertNotNull(inputConnection)
2228
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
2229
- assertEquals("Hello brave world", editText.text?.toString())
2230
-
2231
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
2232
-
2233
- assertTrue(inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode)))
2234
- assertEquals("Hello updated world", editText.text?.toString())
2235
- assertNull(deleteRange)
2236
- assertNull(deleteBackward)
2237
- }
2238
- }
2239
-
2240
- @Test
2241
- fun `printable key event after authorized render change is consumed without inserting text`() {
2242
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2243
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2244
- editText.setSelection(6)
2245
- editText.editorId = 1
2246
-
2247
- var insertedText: String? = null
2248
- var replacement: Triple<Int, Int, String>? = null
2249
- editText.onInsertTextInRustForTesting = { text, _ ->
2250
- insertedText = text
2251
- }
2252
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2253
- replacement = Triple(scalarFrom, scalarTo, text)
2254
- }
2255
-
2256
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2257
- assertNotNull(inputConnection)
2258
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
2259
- assertEquals("Hello brave world", editText.text?.toString())
2260
-
2261
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
2262
-
2263
- val event = KeyEvent(100L, 100L, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0)
2264
- assertTrue(inputConnection.sendKeyEvent(event))
2265
- assertEquals("Hello updated world", editText.text?.toString())
2266
- assertNull(insertedText)
2267
- assertNull(replacement)
2268
- }
2269
-
2270
- @Test
2271
- fun `fresh input connection after stale key up accepts new commit`() {
2272
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2273
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2274
- editText.setSelection(6)
2275
- editText.editorId = 1
2276
-
2277
- val staleConnection = editText.onCreateInputConnection(EditorInfo())
2278
- assertNotNull(staleConnection)
2279
- assertTrue(staleConnection!!.setComposingText("brave ", 1))
2280
- assertEquals("Hello brave world", editText.text?.toString())
2281
-
2282
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
2283
-
2284
- assertTrue(staleConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)))
2285
- assertEquals("Hello updated world", editText.text?.toString())
2286
-
2287
- var insertedText: String? = null
2288
- editText.onInsertTextInRustForTesting = { text, _ ->
2289
- insertedText = text
2290
- }
2291
-
2292
- val freshConnection = editText.onCreateInputConnection(EditorInfo())
2293
- assertNotNull(freshConnection)
2294
- assertTrue(freshConnection!!.commitText(" fresh", 1))
2295
-
2296
- assertEquals(" fresh", insertedText)
2297
- }
2298
-
2299
- @Test
2300
- fun `key up after authorized render change does not clear invalidation before stale commit`() {
2301
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2302
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2303
- editText.setSelection(6)
2304
- editText.editorId = 1
2305
-
2306
- var insertedText: String? = null
2307
- var replacement: Triple<Int, Int, String>? = null
2308
- editText.onInsertTextInRustForTesting = { text, _ ->
2309
- insertedText = text
2310
- }
2311
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2312
- replacement = Triple(scalarFrom, scalarTo, text)
2313
- }
2314
-
2315
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2316
- assertNotNull(inputConnection)
2317
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
2318
- assertEquals("Hello brave world", editText.text?.toString())
2319
-
2320
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
2321
-
2322
- assertTrue(inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)))
2323
- assertTrue(inputConnection.commitText("brave ", 1))
2324
- assertEquals("Hello updated world", editText.text?.toString())
2325
- assertNull(insertedText)
2326
- assertNull(replacement)
2327
- }
2328
-
2329
- @Test
2330
- fun `fresh input connection after stale selection accepts new commit`() {
2331
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2332
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2333
- editText.setSelection(6)
2334
- editText.editorId = 1
2335
-
2336
- val staleConnection = editText.onCreateInputConnection(EditorInfo())
2337
- assertNotNull(staleConnection)
2338
- assertTrue(staleConnection!!.setComposingText("brave ", 1))
2339
- assertEquals("Hello brave world", editText.text?.toString())
2340
-
2341
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
2342
-
2343
- var syncedSelection: Pair<Int, Int>? = null
2344
- editText.onSetSelectionScalarInRustForTesting = { anchor, head ->
2345
- syncedSelection = anchor to head
2346
- }
2347
-
2348
- assertTrue(staleConnection.setSelection(6, 13))
2349
- assertEquals("Hello updated world", editText.text?.toString())
2350
- assertNull(syncedSelection)
2351
-
2352
- var insertedText: String? = null
2353
- editText.onInsertTextInRustForTesting = { text, _ ->
2354
- insertedText = text
2355
- }
2356
-
2357
- val freshConnection = editText.onCreateInputConnection(EditorInfo())
2358
- assertNotNull(freshConnection)
2359
- assertTrue(freshConnection!!.commitText(" fresh", 1))
2360
-
2361
- assertEquals(" fresh", insertedText)
2362
- }
2363
-
2364
- @Test
2365
- fun `set selection after authorized render change does not reauthorize stale commit`() {
2366
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2367
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2368
- editText.setSelection(6)
2369
- editText.editorId = 1
2370
-
2371
- var insertedText: String? = null
2372
- var replacement: Triple<Int, Int, String>? = null
2373
- editText.onInsertTextInRustForTesting = { text, _ ->
2374
- insertedText = text
2375
- }
2376
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2377
- replacement = Triple(scalarFrom, scalarTo, text)
2378
- }
2379
-
2380
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2381
- assertNotNull(inputConnection)
2382
- assertTrue(inputConnection!!.setComposingText("brave ", 1))
2383
- assertEquals("Hello brave world", editText.text?.toString())
2384
-
2385
- editText.applyUpdateJSON(renderUpdateJson("Hello updated world"), notifyListener = false)
2386
-
2387
- var syncedSelection: Pair<Int, Int>? = null
2388
- editText.onSetSelectionScalarInRustForTesting = { anchor, head ->
2389
- syncedSelection = anchor to head
2390
- }
2391
-
2392
- assertTrue(inputConnection.setSelection(6, 13))
2393
- assertEquals("Hello updated world", editText.text?.toString())
2394
- assertNull(syncedSelection)
2395
-
2396
- assertTrue(inputConnection.commitText("braver ", 1))
2397
- assertEquals("Hello updated world", editText.text?.toString())
2398
- assertNull(insertedText)
2399
- assertNull(replacement)
2400
- }
2401
-
2402
- @Test
2403
- fun `set selection without invalidation delegates and syncs selection`() {
2404
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2405
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2406
- editText.setSelection(0)
2407
- editText.editorId = 1
2408
-
2409
- var syncedSelection: Pair<Int, Int>? = null
2410
- editText.onSetSelectionScalarInRustForTesting = { anchor, head ->
2411
- syncedSelection = anchor to head
2412
- }
2413
-
2414
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2415
- assertNotNull(inputConnection)
2416
-
2417
- assertTrue(inputConnection!!.setSelection(6, 11))
2418
- assertEquals(6 to 11, syncedSelection)
2419
- assertEquals(6, editText.selectionStart)
2420
- assertEquals(11, editText.selectionEnd)
2421
- }
2422
-
2423
- @Test
2424
- fun `stale input connection is consumed after editor rebind`() {
2425
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2426
- editText.applyUpdateJSON(renderUpdateJson("first"), notifyListener = false)
2427
- editText.setSelection(5)
2428
- editText.editorId = 1
2429
-
2430
- var insertedText: String? = null
2431
- var deleteRange: Pair<Int, Int>? = null
2432
- editText.onInsertTextInRustForTesting = { text, _ ->
2433
- insertedText = text
2434
- }
2435
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
2436
- deleteRange = scalarFrom to scalarTo
2437
- }
2438
-
2439
- val staleConnection = editText.onCreateInputConnection(EditorInfo())
2440
- assertNotNull(staleConnection)
2441
-
2442
- editText.discardTransientNativeInputForEditorRebind()
2443
- editText.editorId = 0
2444
- editText.applyUpdateJSON(renderUpdateJson("second"), notifyListener = false)
2445
- editText.setSelection(6)
2446
- editText.editorId = 2
2447
-
2448
- assertTrue(staleConnection!!.commitText("X", 1))
2449
- assertTrue(staleConnection.deleteSurroundingText(1, 0))
2450
-
2451
- assertEquals("second", editText.text?.toString())
2452
- assertNull(insertedText)
2453
- assertNull(deleteRange)
2454
- }
2455
-
2456
- @Test
2457
- fun `focused read only toggle restarts input and keeps stale connection blocked`() {
2458
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2459
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
2460
- editText.setSelection(3)
2461
- editText.editorId = 1
2462
- assertTrue(editText.requestFocus())
2463
-
2464
- var insertedText: String? = null
2465
- editText.onInsertTextInRustForTesting = { text, _ ->
2466
- insertedText = text
2467
- }
2468
-
2469
- val staleConnection = editText.onCreateInputConnection(EditorInfo())
2470
- assertNotNull(staleConnection)
2471
-
2472
- editText.isEditable = false
2473
- editText.isEditable = true
2474
-
2475
- assertTrue(
2476
- editText.imeTraceSnapshotForTesting().any {
2477
- it.contains("restartInput:source=editable")
2478
- }
2479
- )
2480
-
2481
- assertTrue(staleConnection!!.commitText("X", 1))
2482
-
2483
- assertEquals("abc", editText.text?.toString())
2484
- assertNull(insertedText)
2485
-
2486
- val freshConnection = editText.onCreateInputConnection(EditorInfo())
2487
- assertNotNull(freshConnection)
2488
-
2489
- assertTrue(freshConnection!!.commitText("Y", 1))
2490
- assertEquals("Y", insertedText)
2491
- }
2492
-
2493
- @Test
2494
- fun `command preflight flushes empty selected composition as deletion`() {
2495
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2496
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2497
- editText.setSelection(6, 11)
2498
- editText.editorId = 1
2499
-
2500
- var deletedRange: Pair<Int, Int>? = null
2501
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
2502
- deletedRange = scalarFrom to scalarTo
2503
- editText.applyUpdateJSON(renderUpdateJson("Hello "), notifyListener = false)
2504
- }
2505
-
2506
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2507
- assertNotNull(inputConnection)
2508
- assertTrue(inputConnection!!.setComposingText("", 1))
2509
-
2510
- assertTrue(editText.prepareForExternalEditorUpdate())
2511
-
2512
- assertEquals(6 to 11, deletedRange)
2513
- assertEquals(0, editText.reconciliationCount)
2514
- }
2515
-
2516
- @Test
2517
- fun `commit text after input connection recreation uses persisted composition range`() {
2518
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2519
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2520
- editText.setSelection(6)
2521
- editText.editorId = 1
2522
-
2523
- var insertedText: String? = null
2524
- var insertedScalar: Int? = null
2525
- editText.onInsertTextInRustForTesting = { text, scalar ->
2526
- insertedText = text
2527
- insertedScalar = scalar
2528
- }
2529
-
2530
- val firstConnection = editText.onCreateInputConnection(EditorInfo())
2531
- assertNotNull(firstConnection)
2532
- assertTrue(firstConnection!!.setComposingText("brave ", 1))
2533
- assertEquals("Hello brave world", editText.text?.toString())
2534
-
2535
- val recreatedConnection = editText.onCreateInputConnection(EditorInfo())
2536
- assertNotNull(recreatedConnection)
2537
- assertTrue(recreatedConnection!!.commitText("brave ", 1))
2538
-
2539
- assertEquals("brave ", insertedText)
2540
- assertEquals(6, insertedScalar)
2541
- }
2542
-
2543
- @Test
2544
- fun `composing text uses rendered paragraph font size before Samsung space commit`() {
2545
- val context = RuntimeEnvironment.getApplication()
2546
- val density = context.resources.displayMetrics.density
2547
- val editText = EditorEditText(context)
2548
- editText.setBaseStyle(24f * density, Color.BLACK, Color.WHITE)
2549
- editText.applyTheme(
2550
- EditorTheme.fromJson(
2551
- """
2552
- {
2553
- "text": { "fontSize": 12, "color": "#112233" }
2554
- }
2555
- """.trimIndent()
2556
- )
2557
- )
2558
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
2559
- editText.setSelection(0)
2560
- editText.editorId = 1
2561
-
2562
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2563
- assertNotNull(inputConnection)
2564
- assertTrue(inputConnection!!.setComposingText("word", 1))
2565
-
2566
- val sizeSpans = editText.text!!.getSpans(0, 4, AbsoluteSizeSpan::class.java)
2567
- assertTrue(sizeSpans.any { it.size == (12f * density).toInt() })
2568
- }
2569
-
2570
- @Test
2571
- fun `finish composing defers render so pending Samsung space commit uses same connection`() {
2572
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2573
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
2574
- editText.setSelection(0)
2575
- editText.editorId = 1
2576
-
2577
- val updates = mutableListOf<String>()
2578
- editText.editorListener = object : EditorEditText.EditorListener {
2579
- override fun onSelectionChanged(anchor: Int, head: Int) = Unit
2580
- override fun onEditorUpdate(updateJSON: String) {
2581
- updates.add(updateJSON)
2582
- }
2583
- }
2584
-
2585
- val inserted = mutableListOf<Pair<String, Int>>()
2586
- editText.onInsertTextInRustForTesting = { text, scalar ->
2587
- inserted.add(text to scalar)
2588
- val renderedText = when (text) {
2589
- "word" -> "word"
2590
- " " -> "word "
2591
- else -> text
2592
- }
2593
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(renderedText))
2594
- }
2595
-
2596
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2597
- assertNotNull(inputConnection)
2598
- assertTrue(inputConnection!!.setComposingText("word", 1))
2599
-
2600
- assertTrue(inputConnection.finishComposingText())
2601
-
2602
- assertEquals(listOf("word" to 0), inserted)
2603
- assertTrue(editText.hasDeferredRustUpdateApplicationForTesting())
2604
- assertTrue(updates.isEmpty())
2605
-
2606
- assertTrue(inputConnection.commitText(" ", 1))
2607
-
2608
- assertEquals(listOf("word" to 0, " " to 4), inserted)
2609
- assertEquals("word ", editText.text?.toString())
2610
- assertEquals(1, updates.size)
2611
-
2612
- shadowOf(Looper.getMainLooper()).idle()
2613
-
2614
- assertFalse(editText.hasDeferredRustUpdateApplicationForTesting())
2615
- assertEquals("word ", editText.text?.toString())
2616
- assertEquals(1, updates.size)
2617
- }
2618
-
2619
- @Test
2620
- fun `finish composing deferred render applies on next loop without pending commit`() {
2621
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2622
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
2623
- editText.setSelection(0)
2624
- editText.editorId = 1
2625
-
2626
- val updates = mutableListOf<String>()
2627
- editText.editorListener = object : EditorEditText.EditorListener {
2628
- override fun onSelectionChanged(anchor: Int, head: Int) = Unit
2629
- override fun onEditorUpdate(updateJSON: String) {
2630
- updates.add(updateJSON)
2631
- }
2632
- }
2633
- editText.onInsertTextInRustForTesting = { text, _ ->
2634
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(text))
2635
- }
2636
-
2637
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2638
- assertNotNull(inputConnection)
2639
- assertTrue(inputConnection!!.setComposingText("word", 1))
2640
-
2641
- assertTrue(inputConnection.finishComposingText())
2642
-
2643
- assertTrue(editText.hasDeferredRustUpdateApplicationForTesting())
2644
- assertTrue(updates.isEmpty())
2645
-
2646
- shadowOf(Looper.getMainLooper()).idle()
2647
-
2648
- assertFalse(editText.hasDeferredRustUpdateApplicationForTesting())
2649
- assertEquals("word", editText.text?.toString())
2650
- assertEquals(1, updates.size)
2651
- }
2652
-
2653
- @Test
2654
- fun `composition commit defers render so Samsung autocorrect space commit survives`() {
2655
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2656
- editText.applyUpdateJSON(renderUpdateJson("teh"), notifyListener = false)
2657
- editText.setSelection(0, 3)
2658
- editText.editorId = 1
2659
-
2660
- val updates = mutableListOf<String>()
2661
- editText.editorListener = object : EditorEditText.EditorListener {
2662
- override fun onSelectionChanged(anchor: Int, head: Int) = Unit
2663
- override fun onEditorUpdate(updateJSON: String) {
2664
- updates.add(updateJSON)
2665
- }
2666
- }
2667
-
2668
- val rendered = StringBuilder("teh")
2669
- val replacements = mutableListOf<Triple<Int, Int, String>>()
2670
- val inserts = mutableListOf<Pair<String, Int>>()
2671
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2672
- replacements.add(Triple(scalarFrom, scalarTo, text))
2673
- rendered.replace(scalarFrom, scalarTo, text)
2674
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
2675
- }
2676
- editText.onInsertTextInRustForTesting = { text, scalar ->
2677
- inserts.add(text to scalar)
2678
- rendered.insert(scalar.coerceIn(0, rendered.length), text)
2679
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
2680
- }
2681
-
2682
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2683
- assertNotNull(inputConnection)
2684
- assertTrue(inputConnection!!.setComposingRegion(0, 3))
2685
-
2686
- assertTrue(inputConnection.commitText("the", 1))
2687
-
2688
- assertEquals(listOf(Triple(0, 3, "the")), replacements)
2689
- assertTrue(editText.hasDeferredRustUpdateApplicationForTesting())
2690
- assertTrue(updates.isEmpty())
2691
-
2692
- assertTrue(inputConnection.commitText(" ", 1))
2693
-
2694
- assertEquals(listOf(" " to 3), inserts)
2695
- assertEquals("the ", editText.text?.toString())
2696
- assertFalse(editText.hasDeferredRustUpdateApplicationForTesting())
2697
-
2698
- shadowOf(Looper.getMainLooper()).idle()
2699
-
2700
- assertEquals("the ", editText.text?.toString())
2701
- }
2702
-
2703
- @Test
2704
- fun `composition commit uses composing span when tracked range is collapsed`() {
2705
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2706
- editText.applyUpdateJSON(renderUpdateJson("wouldnt"), notifyListener = false)
2707
- editText.setSelection(7)
2708
- editText.editorId = 1
2709
-
2710
- val rendered = StringBuilder("wouldnt")
2711
- val replacements = mutableListOf<Triple<Int, Int, String>>()
2712
- val inserts = mutableListOf<Pair<String, Int>>()
2713
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2714
- replacements.add(Triple(scalarFrom, scalarTo, text))
2715
- rendered.replace(scalarFrom, scalarTo, text)
2716
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
2717
- }
2718
- editText.onInsertTextInRustForTesting = { text, scalar ->
2719
- inserts.add(text to scalar)
2720
- rendered.insert(scalar.coerceIn(0, rendered.length), text)
2721
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
2722
- }
2723
-
2724
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2725
- assertNotNull(inputConnection)
2726
- assertTrue(inputConnection!!.setComposingRegion(0, 7))
2727
- editText.setCompositionReplacementRange(7, 7)
2728
-
2729
- assertTrue(inputConnection.commitText("wouldn't", 1))
2730
-
2731
- assertEquals(listOf(Triple(0, 7, "wouldn't")), replacements)
2732
- assertTrue(editText.hasDeferredRustUpdateApplicationForTesting())
2733
-
2734
- assertTrue(inputConnection.commitText(" ", 1))
2735
-
2736
- assertEquals(listOf(" " to 8), inserts)
2737
- assertEquals("wouldn't ", editText.text?.toString())
2738
-
2739
- shadowOf(Looper.getMainLooper()).idle()
2740
-
2741
- assertEquals("wouldn't ", editText.text?.toString())
2742
- }
2743
-
2744
- @Test
2745
- fun `composition commit adopts already visible correction instead of inserting duplicate word`() {
2746
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2747
- editText.applyUpdateJSON(renderUpdateJson("wouldnt"), notifyListener = false)
2748
- editText.setSelection(7)
2749
- editText.editorId = 1
2750
-
2751
- val rendered = StringBuilder("wouldnt")
2752
- val replacements = mutableListOf<Triple<Int, Int, String>>()
2753
- val inserts = mutableListOf<Pair<String, Int>>()
2754
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2755
- replacements.add(Triple(scalarFrom, scalarTo, text))
2756
- rendered.replace(scalarFrom, scalarTo, text)
2757
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
2758
- }
2759
- editText.onInsertTextInRustForTesting = { text, scalar ->
2760
- inserts.add(text to scalar)
2761
- rendered.insert(scalar.coerceIn(0, rendered.length), text)
2762
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
2763
- }
2764
-
2765
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2766
- assertNotNull(inputConnection)
2767
- editText.setCompositionReplacementRange(7, 7)
2768
- editText.runWithTransientInputMutationGuard {
2769
- editText.text!!.replace(0, 7, "wouldn't")
2770
- Selection.setSelection(editText.text!!, 8, 8)
2771
- true
2772
- }
2773
-
2774
- assertTrue(inputConnection!!.commitText("wouldn't", 1))
2775
-
2776
- assertTrue(replacements.isEmpty())
2777
- assertEquals(listOf("'" to 6), inserts)
2778
- assertEquals("wouldn't", editText.text?.toString())
2779
- assertTrue(editText.hasDeferredRustUpdateApplicationForTesting())
2780
-
2781
- assertTrue(inputConnection.commitText(" ", 1))
2782
-
2783
- assertEquals(listOf("'" to 6, " " to 8), inserts)
2784
- assertEquals("wouldn't ", editText.text?.toString())
2785
-
2786
- shadowOf(Looper.getMainLooper()).idle()
2787
-
2788
- assertEquals("wouldn't ", editText.text?.toString())
2789
- }
2790
-
2791
- @Test
2792
- fun `already visible multi typo correction uses visible replacement range`() {
2793
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2794
- editText.applyUpdateJSON(renderUpdateJson("woudlnt"), notifyListener = false)
2795
- editText.setSelection(7)
2796
- editText.editorId = 1
2797
-
2798
- val rendered = StringBuilder("woudlnt")
2799
- val replacements = mutableListOf<Triple<Int, Int, String>>()
2800
- val inserts = mutableListOf<Pair<String, Int>>()
2801
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2802
- replacements.add(Triple(scalarFrom, scalarTo, text))
2803
- rendered.replace(scalarFrom, scalarTo, text)
2804
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
2805
- }
2806
- editText.onInsertTextInRustForTesting = { text, scalar ->
2807
- inserts.add(text to scalar)
2808
- rendered.insert(scalar.coerceIn(0, rendered.length), text)
2809
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
2810
- }
2811
-
2812
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2813
- assertNotNull(inputConnection)
2814
- editText.setCompositionReplacementRange(7, 7)
2815
- editText.runWithTransientInputMutationGuard {
2816
- editText.text!!.replace(0, 7, "wouldn't")
2817
- Selection.setSelection(editText.text!!, 8, 8)
2818
- true
2819
- }
2820
-
2821
- assertTrue(inputConnection!!.commitText("wouldn't", 1))
2822
-
2823
- assertEquals(listOf(Triple(3, 6, "ldn'")), replacements)
2824
- assertTrue(inserts.isEmpty())
2825
- assertEquals("wouldn't", editText.text?.toString())
2826
- }
2827
-
2828
- @Test
2829
- fun `empty commit text over composing range deletes original range`() {
2830
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2831
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2832
- editText.setSelection(6, 11)
2833
- editText.editorId = 1
2834
-
2835
- var deletedRange: Pair<Int, Int>? = null
2836
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
2837
- deletedRange = scalarFrom to scalarTo
2838
- }
2839
-
2840
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2841
- assertNotNull(inputConnection)
2842
-
2843
- assertTrue(inputConnection!!.setComposingRegion(6, 11))
2844
- assertTrue(inputConnection.commitText("", 1))
2845
-
2846
- assertEquals(6 to 11, deletedRange)
2847
- }
2848
-
2849
- @Test
2850
- fun `commit text honors requested cursor position`() {
2851
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2852
- editText.applyUpdateJSON(renderUpdateJson("Hello "), notifyListener = false)
2853
- editText.setSelection(6)
2854
- editText.editorId = 1
2855
-
2856
- var insertedText: String? = null
2857
- var selectedScalar: Pair<Int, Int>? = null
2858
- editText.onInsertTextInRustForTesting = { text, _ ->
2859
- insertedText = text
2860
- }
2861
- editText.onSetSelectionScalarInRustForTesting = { anchor, head ->
2862
- selectedScalar = anchor to head
2863
- }
2864
-
2865
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2866
- assertNotNull(inputConnection)
2867
-
2868
- assertTrue(inputConnection!!.commitText("()", 0))
2869
-
2870
- assertEquals("()", insertedText)
2871
- assertEquals(6 to 6, selectedScalar)
2872
- }
2873
-
2874
- @Test
2875
- fun `no-op composition commit honors requested cursor position`() {
2876
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2877
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
2878
- editText.setSelection(3)
2879
- editText.editorId = 1
2880
-
2881
- var replacement: Triple<Int, Int, String>? = null
2882
- var selectedScalar: Pair<Int, Int>? = null
2883
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
2884
- replacement = Triple(scalarFrom, scalarTo, text)
2885
- }
2886
- editText.onSetSelectionScalarInRustForTesting = { anchor, head ->
2887
- selectedScalar = anchor to head
2888
- }
2889
-
2890
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2891
- assertNotNull(inputConnection)
2892
-
2893
- assertTrue(inputConnection!!.setComposingRegion(0, 3))
2894
- assertTrue(inputConnection.commitText("abc", 0))
2895
-
2896
- assertNull(replacement)
2897
- assertEquals(0 to 0, selectedScalar)
2898
- }
2899
-
2900
- @Test
2901
- fun `command preflight flushes visible composing text before toolbar commands`() {
2902
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2903
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
2904
- editText.setSelection(0)
2905
- editText.editorId = 1
2906
-
2907
- var insertedText: String? = null
2908
- var insertedScalar: Int? = null
2909
- editText.onInsertTextInRustForTesting = { text, scalar ->
2910
- insertedText = text
2911
- insertedScalar = scalar
2912
- editText.applyUpdateJSON(renderUpdateJson(text), notifyListener = false)
2913
- }
2914
-
2915
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2916
- assertNotNull(inputConnection)
2917
- inputConnection!!.setComposingText("abc", 1)
2918
-
2919
- val ready = editText.prepareForExternalEditorUpdate()
2920
-
2921
- assertTrue(ready)
2922
- assertEquals("abc", insertedText)
2923
- assertEquals(0, insertedScalar)
2924
- assertEquals(0, editText.reconciliationCount)
2925
- }
2926
-
2927
- @Test
2928
- fun `command preflight blocks and restores cancelled empty composing text`() {
2929
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2930
- editText.applyUpdateJSON(renderUpdateJson(""), notifyListener = false)
2931
- editText.setSelection(0)
2932
- editText.editorId = 1
2933
-
2934
- var insertedText: String? = null
2935
- editText.onInsertTextInRustForTesting = { text, _ ->
2936
- insertedText = text
2937
- }
2938
-
2939
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
2940
- assertNotNull(inputConnection)
2941
- assertTrue(inputConnection!!.setComposingText("", 1))
2942
- assertEquals("", editText.text?.toString())
2943
-
2944
- val ready = editText.prepareForExternalEditorUpdate()
2945
-
2946
- assertFalse(ready)
2947
- assertEquals("", editText.text?.toString())
2948
- assertEquals(null, insertedText)
2949
- assertEquals(0, editText.reconciliationCount)
2950
- }
2951
-
2952
- @Test
2953
- fun `focused native insertion mutation commits to rust instead of reconciliation`() {
2954
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2955
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2956
- assertTrue(editText.requestFocus())
2957
- editText.editorId = 1
2958
-
2959
- var insertedText: String? = null
2960
- var insertedScalar: Int? = null
2961
- editText.onInsertTextInRustForTesting = { text, scalar ->
2962
- insertedText = text
2963
- insertedScalar = scalar
2964
- }
2965
-
2966
- editText.text!!.insert(6, "brave ")
2967
-
2968
- assertEquals("brave ", insertedText)
2969
- assertEquals(6, insertedScalar)
2970
- assertEquals(0, editText.reconciliationCount)
2971
- }
2972
-
2973
- @Test
2974
- fun `focused native multiline insertion uses structured content insertion`() {
2975
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2976
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
2977
- assertTrue(editText.requestFocus())
2978
- editText.editorId = 1
2979
-
2980
- var insertedContent: Triple<Int, Int, String>? = null
2981
- editText.onInsertContentJsonAtSelectionScalarForTesting = { scalarFrom, scalarTo, json ->
2982
- insertedContent = Triple(scalarFrom, scalarTo, json)
2983
- }
2984
-
2985
- editText.text!!.insert(6, "one\ntwo")
2986
-
2987
- val (scalarFrom, scalarTo, json) = insertedContent!!
2988
- assertEquals(6, scalarFrom)
2989
- assertEquals(6, scalarTo)
2990
- val content = JSONObject(json).getJSONArray("content")
2991
- assertEquals("one", content.getJSONObject(0).getJSONArray("content").getJSONObject(0).getString("text"))
2992
- assertEquals("two", content.getJSONObject(1).getJSONArray("content").getJSONObject(0).getString("text"))
2993
- assertEquals(0, editText.reconciliationCount)
2994
- }
2995
-
2996
- @Test
2997
- fun `focused native replacement mutation commits to rust instead of reconciliation`() {
2998
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
2999
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3000
- assertTrue(editText.requestFocus())
3001
- editText.editorId = 1
3002
-
3003
- var replacement: Triple<Int, Int, String>? = null
3004
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3005
- replacement = Triple(scalarFrom, scalarTo, text)
3006
- }
3007
-
3008
- editText.text!!.replace(6, 11, "there")
3009
-
3010
- assertEquals(Triple(6, 11, "there"), replacement)
3011
- assertEquals(0, editText.reconciliationCount)
3012
- }
3013
-
3014
- @Test
3015
- fun `focused native deletion mutation commits to rust instead of reconciliation`() {
3016
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3017
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3018
- assertTrue(editText.requestFocus())
3019
- editText.editorId = 1
3020
-
3021
- var deletedRange: Pair<Int, Int>? = null
3022
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
3023
- deletedRange = scalarFrom to scalarTo
3024
- }
3025
-
3026
- editText.text!!.delete(5, 6)
3027
-
3028
- assertEquals(5 to 6, deletedRange)
3029
- assertEquals(0, editText.reconciliationCount)
3030
- }
3031
-
3032
- @Test
3033
- fun `native emoji replacement snaps diff to scalar boundaries`() {
3034
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3035
- editText.applyUpdateJSON(renderUpdateJson("😀 ok"), notifyListener = false)
3036
- assertTrue(editText.requestFocus())
3037
- editText.editorId = 1
3038
-
3039
- var replacement: Triple<Int, Int, String>? = null
3040
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3041
- replacement = Triple(scalarFrom, scalarTo, text)
3042
- }
3043
-
3044
- editText.text!!.replace(0, 2, "😁")
3045
-
3046
- assertEquals(Triple(0, 1, "😁"), replacement)
3047
- assertEquals(0, editText.reconciliationCount)
3048
- }
3049
-
3050
- @Test
3051
- fun `native autocorrect immediately after blur commits during blur grace window`() {
3052
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3053
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
3054
- assertTrue(editText.requestFocus())
3055
- editText.editorId = 1
3056
-
3057
- var replacement: Triple<Int, Int, String>? = null
3058
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3059
- replacement = Triple(scalarFrom, scalarTo, text)
3060
- }
3061
-
3062
- editText.clearFocus()
3063
- editText.text!!.replace(0, 3, "the")
3064
-
3065
- assertEquals(Triple(1, 3, "he"), replacement)
3066
- assertEquals(0, editText.reconciliationCount)
3067
- }
3068
-
3069
- @Test
3070
- fun `native autocorrect after blur commits even when ime leaves composing span`() {
3071
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3072
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
3073
- assertTrue(editText.requestFocus())
3074
- editText.editorId = 1
3075
-
3076
- var replacement: Triple<Int, Int, String>? = null
3077
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3078
- replacement = Triple(scalarFrom, scalarTo, text)
3079
- }
3080
-
3081
- editText.clearFocus()
3082
- editText.runWithTransientInputMutationGuard {
3083
- editText.text!!.replace(0, 3, "the")
3084
- BaseInputConnection.setComposingSpans(editText.text!!)
3085
- true
3086
- }
3087
-
3088
- assertTrue(editText.prepareForExternalEditorUpdate())
3089
- assertEquals(Triple(1, 3, "he"), replacement)
3090
- assertEquals(0, editText.reconciliationCount)
3091
- }
3092
-
3093
- @Test
3094
- fun `native composing diff after blur is not adopted as final mutation`() {
3095
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3096
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3097
- assertTrue(editText.requestFocus())
3098
- editText.setSelection(6)
3099
- editText.editorId = 1
3100
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
3101
-
3102
- var insertedText: String? = null
3103
- var replacement: Triple<Int, Int, String>? = null
3104
- editText.onInsertTextInRustForTesting = { text, _ ->
3105
- insertedText = text
3106
- }
3107
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3108
- replacement = Triple(scalarFrom, scalarTo, text)
3109
- }
3110
-
3111
- editText.setCompositionReplacementRange(6, 6)
3112
- editText.setComposingTextForEditor("brave ")
3113
- editText.runWithTransientInputMutationGuard {
3114
- editText.text!!.insert(6, "braver ")
3115
- BaseInputConnection.setComposingSpans(editText.text!!)
3116
- true
3117
- }
3118
- editText.clearFocus()
3119
-
3120
- assertFalse(editText.prepareForExternalEditorUpdate())
3121
- assertNull(insertedText)
3122
- assertNull(replacement)
3123
- assertEquals(0, editText.reconciliationCount)
3124
- }
3125
-
3126
- @Test
3127
- fun `input trait change after blur suppresses stale direct native mutation adoption`() {
3128
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3129
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3130
- assertTrue(editText.requestFocus())
3131
- editText.setSelection(6)
3132
- editText.editorId = 1
3133
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
3134
-
3135
- var insertedText: String? = null
3136
- var replacement: Triple<Int, Int, String>? = null
3137
- editText.onInsertTextInRustForTesting = { text, _ ->
3138
- insertedText = text
3139
- }
3140
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3141
- replacement = Triple(scalarFrom, scalarTo, text)
3142
- }
3143
-
3144
- val oldConnection = editText.onCreateInputConnection(EditorInfo())
3145
- assertNotNull(oldConnection)
3146
- assertTrue(oldConnection!!.setComposingText("brave ", 1))
3147
-
3148
- editText.clearFocus()
3149
- editText.setAutoCorrect(false)
3150
- assertEquals("Hello world", editText.text?.toString())
3151
-
3152
- editText.runWithTransientInputMutationGuard {
3153
- editText.text!!.insert(6, "stale ")
3154
- true
3155
- }
3156
-
3157
- assertFalse(editText.prepareForExternalEditorUpdate())
3158
- assertNull(insertedText)
3159
- assertNull(replacement)
3160
- assertEquals(0, editText.reconciliationCount)
3161
- }
3162
-
3163
- @Test
3164
- fun `native mutation after blur is only adopted once`() {
3165
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3166
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
3167
- assertTrue(editText.requestFocus())
3168
- editText.editorId = 1
3169
-
3170
- var replacement: Triple<Int, Int, String>? = null
3171
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3172
- replacement = Triple(scalarFrom, scalarTo, text)
3173
- }
3174
-
3175
- editText.clearFocus()
3176
- editText.runWithTransientInputMutationGuard {
3177
- editText.text!!.replace(0, 3, "the")
3178
- true
3179
- }
3180
-
3181
- assertTrue(editText.prepareForExternalEditorUpdate())
3182
- assertEquals(Triple(1, 3, "he"), replacement)
3183
-
3184
- replacement = null
3185
- editText.runWithTransientInputMutationGuard {
3186
- editText.text!!.replace(0, 3, "tha")
3187
- true
3188
- }
3189
-
3190
- assertFalse(editText.prepareForExternalEditorUpdate())
3191
- assertNull(replacement)
3192
- }
3193
-
3194
- @Test
3195
- fun `native mutation after blur grace window expires is not adopted`() {
3196
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3197
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
3198
- assertTrue(editText.requestFocus())
3199
- editText.editorId = 1
3200
-
3201
- var replacement: Triple<Int, Int, String>? = null
3202
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3203
- replacement = Triple(scalarFrom, scalarTo, text)
3204
- }
3205
-
3206
- editText.clearFocus()
3207
- shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(800))
3208
- editText.runWithTransientInputMutationGuard {
3209
- editText.text!!.replace(0, 3, "the")
3210
- true
3211
- }
3212
-
3213
- assertFalse(editText.prepareForExternalEditorUpdate())
3214
- assertNull(replacement)
3215
- }
3216
-
3217
- @Test
3218
- fun `native mutation after blur is only adopted once after applied update render`() {
3219
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3220
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
3221
- assertTrue(editText.requestFocus())
3222
- editText.editorId = 1
3223
-
3224
- var replacement: Triple<Int, Int, String>? = null
3225
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3226
- replacement = Triple(scalarFrom, scalarTo, text)
3227
- editText.applyUpdateJSON(renderUpdateJson("the "), notifyListener = false)
3228
- }
3229
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
3230
-
3231
- editText.clearFocus()
3232
- editText.runWithTransientInputMutationGuard {
3233
- editText.text!!.replace(0, 3, "the")
3234
- true
3235
- }
3236
-
3237
- assertTrue(editText.prepareForExternalEditorUpdate())
3238
- assertEquals(Triple(1, 3, "he"), replacement)
3239
- assertEquals("the ", editText.text?.toString())
3240
-
3241
- replacement = null
3242
- editText.runWithTransientInputMutationGuard {
3243
- editText.text!!.replace(0, 3, "tha")
3244
- true
3245
- }
3246
-
3247
- assertFalse(editText.prepareForExternalEditorUpdate())
3248
- assertNull(replacement)
3249
- }
3250
-
3251
- @Test
3252
- fun `native mutation after blur window clears after skipped authorized render`() {
3253
- val editText = EditorEditText(RuntimeEnvironment.getApplication()).apply {
3254
- captureApplyUpdateTraceForTesting = true
3255
- }
3256
- editText.applyUpdateJSON(renderUpdateJson("the "), notifyListener = false)
3257
- assertTrue(editText.requestFocus())
3258
- editText.editorId = 1
3259
-
3260
- var replacement: Triple<Int, Int, String>? = null
3261
- var insertedText: String? = null
3262
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3263
- replacement = Triple(scalarFrom, scalarTo, text)
3264
- }
3265
- editText.onInsertTextInRustForTesting = { text, _ ->
3266
- insertedText = text
3267
- }
3268
-
3269
- editText.clearFocus()
3270
- editText.applyUpdateJSON(renderUpdateJson("the "), notifyListener = false)
3271
- assertTrue(editText.lastApplyUpdateTrace()?.skippedRender == true)
3272
-
3273
- editText.runWithTransientInputMutationGuard {
3274
- editText.text!!.replace(0, 3, "tha")
3275
- true
3276
- }
3277
-
3278
- assertFalse(editText.prepareForExternalEditorUpdate())
3279
- assertNull(replacement)
3280
- assertNull(insertedText)
3281
- }
3282
-
3283
- @Test
3284
- fun `native autocorrect preserves final ime selection`() {
3285
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3286
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
3287
- assertTrue(editText.requestFocus())
3288
- editText.setSelection(4)
3289
- editText.editorId = 1
3290
-
3291
- editText.onReplaceTextInRustForTesting = { _, _, _ -> }
3292
-
3293
- editText.text!!.replace(0, 3, "the")
3294
-
3295
- assertEquals(4, editText.selectionStart)
3296
- assertEquals(4, editText.selectionEnd)
3297
- assertEquals(0, editText.reconciliationCount)
3298
- }
3299
-
3300
- @Test
3301
- fun `native autocorrect preserves backward selection direction`() {
3302
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3303
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3304
- assertTrue(editText.requestFocus())
3305
- editText.editorId = 1
3306
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
3307
- Selection.setSelection(editText.text, 11, 6)
3308
-
3309
- var replacement: Triple<Int, Int, String>? = null
3310
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3311
- replacement = Triple(scalarFrom, scalarTo, text)
3312
- }
3313
-
3314
- editText.text!!.replace(0, 5, "Hi")
3315
-
3316
- assertEquals(Triple(1, 5, "i"), replacement)
3317
- assertTrue(editText.selectionStart > editText.selectionEnd)
3318
- assertEquals(0, editText.reconciliationCount)
3319
- }
3320
-
3321
- @Test
3322
- fun `focused native autocorrect with stray composing span commits when no composition is tracked`() {
3323
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3324
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
3325
- assertTrue(editText.requestFocus())
3326
- editText.editorId = 1
3327
-
3328
- var replacement: Triple<Int, Int, String>? = null
3329
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3330
- replacement = Triple(scalarFrom, scalarTo, text)
3331
- }
3332
-
3333
- BaseInputConnection.setComposingSpans(editText.text!!)
3334
- editText.text!!.replace(0, 3, "the")
3335
-
3336
- assertEquals(Triple(1, 3, "he"), replacement)
3337
- assertEquals(0, editText.reconciliationCount)
3338
- }
3339
-
3340
- @Test
3341
- fun `focused native autocorrect with tracked composition commits instead of reconciliation`() {
3342
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3343
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
3344
- assertTrue(editText.requestFocus())
3345
- editText.editorId = 1
3346
-
3347
- var replacement: Triple<Int, Int, String>? = null
3348
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3349
- replacement = Triple(scalarFrom, scalarTo, text)
3350
- }
3351
-
3352
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
3353
- assertNotNull(inputConnection)
3354
- assertTrue(inputConnection!!.setComposingRegion(0, 3))
3355
- BaseInputConnection.setComposingSpans(editText.text!!)
3356
-
3357
- editText.text!!.replace(0, 3, "the")
3358
-
3359
- assertEquals(Triple(1, 3, "he"), replacement)
3360
- assertEquals(0, editText.reconciliationCount)
3361
- }
3362
-
3363
- @Test
3364
- fun `focused native insertion at tracked composition boundary commits as final mutation`() {
3365
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3366
- editText.applyUpdateJSON(renderUpdateJson("word "), notifyListener = false)
3367
- assertTrue(editText.requestFocus())
3368
- editText.editorId = 1
3369
-
3370
- var insertedText: String? = null
3371
- var insertedScalar: Int? = null
3372
- editText.onInsertTextInRustForTesting = { text, scalar ->
3373
- insertedText = text
3374
- insertedScalar = scalar
3375
- }
3376
-
3377
- editText.setCompositionReplacementRange(0, 4)
3378
- editText.text!!.insert(4, "!")
3379
-
3380
- assertEquals("!", insertedText)
3381
- assertEquals(4, insertedScalar)
3382
- assertEquals(0, editText.reconciliationCount)
3383
- }
3384
-
3385
- @Test
3386
- fun `focused native insertion at collapsed tracked composition range commits at caret`() {
3387
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3388
- editText.applyUpdateJSON(renderUpdateJson("abcd"), notifyListener = false)
3389
- assertTrue(editText.requestFocus())
3390
- editText.editorId = 1
3391
-
3392
- var insertedText: String? = null
3393
- var insertedScalar: Int? = null
3394
- editText.onInsertTextInRustForTesting = { text, scalar ->
3395
- insertedText = text
3396
- insertedScalar = scalar
3397
- }
3398
-
3399
- editText.setCompositionReplacementRange(2, 2)
3400
- editText.text!!.insert(2, "X")
3401
-
3402
- assertEquals("X", insertedText)
3403
- assertEquals(2, insertedScalar)
3404
- assertEquals(0, editText.reconciliationCount)
3405
- }
3406
-
3407
- @Test
3408
- fun `focused native mutation outside tracked composition range is not adopted`() {
3409
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3410
- editText.applyUpdateJSON(renderUpdateJson("teh word"), notifyListener = false)
3411
- assertTrue(editText.requestFocus())
3412
- editText.editorId = 1
3413
-
3414
- var insertedText: String? = null
3415
- var replacement: Triple<Int, Int, String>? = null
3416
- editText.onInsertTextInRustForTesting = { text, _ ->
3417
- insertedText = text
3418
- }
3419
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3420
- replacement = Triple(scalarFrom, scalarTo, text)
3421
- }
3422
-
3423
- editText.setCompositionReplacementRange(0, 3)
3424
- editText.runWithTransientInputMutationGuard {
3425
- editText.text!!.insert(4, "!")
3426
- true
3427
- }
3428
-
3429
- assertFalse(editText.prepareForExternalEditorUpdate())
3430
- assertNull(insertedText)
3431
- assertNull(replacement)
3432
- assertEquals(0, editText.reconciliationCount)
3433
- }
3434
-
3435
- @Test
3436
- fun `focused native composing diff with tracked composing text is not adopted as final mutation`() {
3437
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3438
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3439
- assertTrue(editText.requestFocus())
3440
- editText.setSelection(6)
3441
- editText.editorId = 1
3442
-
3443
- var insertedText: String? = null
3444
- var replacement: Triple<Int, Int, String>? = null
3445
- editText.onInsertTextInRustForTesting = { text, _ ->
3446
- insertedText = text
3447
- }
3448
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3449
- replacement = Triple(scalarFrom, scalarTo, text)
3450
- }
3451
-
3452
- editText.setCompositionReplacementRange(6, 6)
3453
- editText.setComposingTextForEditor("brave ")
3454
- editText.runWithTransientInputMutationGuard {
3455
- editText.text!!.insert(6, "braver ")
3456
- true
3457
- }
3458
-
3459
- assertFalse(editText.prepareForExternalEditorUpdate())
3460
- assertNull(insertedText)
3461
- assertNull(replacement)
3462
- assertEquals(0, editText.reconciliationCount)
3463
- }
3464
-
3465
- @Test
3466
- fun `native autocorrect retires old input connection before late commit`() {
3467
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3468
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
3469
- assertTrue(editText.requestFocus())
3470
- editText.editorId = 1
3471
-
3472
- val replacements = mutableListOf<Triple<Int, Int, String>>()
3473
- var insertedText: String? = null
3474
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3475
- replacements.add(Triple(scalarFrom, scalarTo, text))
3476
- }
3477
- editText.onInsertTextInRustForTesting = { text, _ ->
3478
- insertedText = text
3479
- }
3480
-
3481
- val oldConnection = editText.onCreateInputConnection(EditorInfo())
3482
- assertNotNull(oldConnection)
3483
- assertTrue(oldConnection!!.setComposingRegion(0, 3))
3484
-
3485
- editText.text!!.replace(0, 3, "the")
3486
-
3487
- assertEquals(listOf(Triple(1, 3, "he")), replacements)
3488
-
3489
- assertTrue(oldConnection.commitText("the", 1))
3490
- assertTrue(oldConnection.finishComposingText())
3491
-
3492
- assertEquals(listOf(Triple(1, 3, "he")), replacements)
3493
- assertNull(insertedText)
3494
- }
3495
-
3496
- @Test
3497
- fun `fresh input connection after native autocorrect accepts new commit`() {
3498
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3499
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
3500
- assertTrue(editText.requestFocus())
3501
- editText.setSelection(4)
3502
- editText.editorId = 1
3503
- editText.onSetSelectionScalarInRustForTesting = { _, _ -> }
3504
-
3505
- var replacement: Triple<Int, Int, String>? = null
3506
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3507
- replacement = Triple(scalarFrom, scalarTo, text)
3508
- editText.applyUpdateJSON(renderUpdateJson("the "), notifyListener = false)
3509
- }
3510
-
3511
- val oldConnection = editText.onCreateInputConnection(EditorInfo())
3512
- assertNotNull(oldConnection)
3513
-
3514
- editText.text!!.replace(0, 3, "the")
3515
-
3516
- assertEquals(Triple(1, 3, "he"), replacement)
3517
-
3518
- var insertedText: String? = null
3519
- editText.onInsertTextInRustForTesting = { text, _ ->
3520
- insertedText = text
3521
- }
3522
-
3523
- val freshConnection = editText.onCreateInputConnection(EditorInfo())
3524
- assertNotNull(freshConnection)
3525
- assertTrue(freshConnection!!.commitText("!", 1))
3526
-
3527
- assertEquals("!", insertedText)
3528
- }
3529
-
3530
- @Test
3531
- fun `text commit replaces normalized backward selection range`() {
3532
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3533
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3534
- Selection.setSelection(editText.text, 11, 6)
3535
- editText.editorId = 1
3536
-
3537
- var replacement: Triple<Int, Int, String>? = null
3538
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3539
- replacement = Triple(scalarFrom, scalarTo, text)
3540
- }
3541
-
3542
- editText.handleTextCommit("there")
3543
-
3544
- assertEquals(Triple(6, 11, "there"), replacement)
3545
- }
3546
-
3547
- @Test
3548
- fun `text replacement commit does not optimistically mutate visible text`() {
3549
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3550
- editText.applyUpdateJSON(renderUpdateJson("teh "), notifyListener = false)
3551
- editText.setSelection(0, 3)
3552
- editText.editorId = 1
3553
-
3554
- var replacement: Triple<Int, Int, String>? = null
3555
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3556
- replacement = Triple(scalarFrom, scalarTo, text)
3557
- }
3558
-
3559
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
3560
- assertNotNull(inputConnection)
3561
-
3562
- assertTrue(inputConnection!!.commitText("the", 1))
3563
-
3564
- assertEquals(Triple(0, 3, "the"), replacement)
3565
- assertEquals("teh ", editText.text?.toString())
3566
- assertFalse(
3567
- editText.imeTraceSnapshotForTesting().any {
3568
- it.contains("optimisticVisibleTextCommit")
3569
- }
3570
- )
3571
- }
3572
-
3573
- @Test
3574
- fun `bulk surrounding delete defers render so autocorrect replacement commit survives`() {
3575
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3576
- editText.applyUpdateJSON(renderUpdateJson("teh"), notifyListener = false)
3577
- editText.setSelection(3)
3578
- editText.editorId = 1
3579
-
3580
- val rendered = StringBuilder("teh")
3581
- val deletes = mutableListOf<Pair<Int, Int>>()
3582
- val inserts = mutableListOf<Pair<String, Int>>()
3583
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
3584
- deletes.add(scalarFrom to scalarTo)
3585
- rendered.delete(scalarFrom, scalarTo)
3586
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
3587
- }
3588
- editText.onInsertTextInRustForTesting = { text, scalar ->
3589
- inserts.add(text to scalar)
3590
- rendered.insert(scalar.coerceIn(0, rendered.length), text)
3591
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
3592
- }
3593
-
3594
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
3595
- assertNotNull(inputConnection)
3596
-
3597
- assertTrue(inputConnection!!.deleteSurroundingText(3, 0))
3598
-
3599
- assertEquals(listOf(0 to 3), deletes)
3600
- assertEquals("", editText.text?.toString())
3601
- assertTrue(editText.hasDeferredRustUpdateApplicationForTesting())
3602
-
3603
- assertTrue(inputConnection.commitText("the", 1))
3604
-
3605
- assertEquals(listOf("the" to 0), inserts)
3606
- assertEquals("the", editText.text?.toString())
3607
- assertFalse(editText.hasDeferredRustUpdateApplicationForTesting())
3608
-
3609
- shadowOf(Looper.getMainLooper()).idle()
3610
-
3611
- assertEquals("the", editText.text?.toString())
3612
- }
3613
-
3614
- @Test
3615
- fun `single character surrounding delete defers render so case replacement commit survives`() {
3616
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3617
- editText.applyUpdateJSON(renderUpdateJson("i"), notifyListener = false)
3618
- editText.setSelection(1)
3619
- editText.editorId = 1
3620
-
3621
- val updates = mutableListOf<String>()
3622
- editText.editorListener = object : EditorEditText.EditorListener {
3623
- override fun onSelectionChanged(anchor: Int, head: Int) = Unit
3624
- override fun onEditorUpdate(updateJSON: String) {
3625
- updates.add(updateJSON)
3626
- }
3627
- }
3628
-
3629
- val rendered = StringBuilder("i")
3630
- val deletes = mutableListOf<Pair<Int, Int>>()
3631
- val inserts = mutableListOf<Pair<String, Int>>()
3632
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
3633
- deletes.add(scalarFrom to scalarTo)
3634
- rendered.delete(scalarFrom, scalarTo)
3635
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
3636
- }
3637
- editText.onInsertTextInRustForTesting = { text, scalar ->
3638
- inserts.add(text to scalar)
3639
- rendered.insert(scalar.coerceIn(0, rendered.length), text)
3640
- editText.applyRustUpdateJSONForTesting(renderUpdateJson(rendered.toString()))
3641
- }
3642
-
3643
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
3644
- assertNotNull(inputConnection)
3645
-
3646
- assertTrue(inputConnection!!.deleteSurroundingText(1, 0))
3647
-
3648
- assertEquals(listOf(0 to 1), deletes)
3649
- assertEquals("", editText.text?.toString())
3650
- assertTrue(editText.hasDeferredRustUpdateApplicationForTesting())
3651
- assertTrue(updates.isEmpty())
3652
-
3653
- assertTrue(inputConnection.commitText("I", 1))
3654
-
3655
- assertEquals(listOf("I" to 0), inserts)
3656
- assertEquals("I", editText.text?.toString())
3657
- assertEquals(1, updates.size)
3658
-
3659
- shadowOf(Looper.getMainLooper()).idle()
3660
-
3661
- assertFalse(editText.hasDeferredRustUpdateApplicationForTesting())
3662
- assertEquals("I", editText.text?.toString())
3663
- assertEquals(1, updates.size)
3664
- }
3665
-
3666
- @Test
3667
- fun `bulk surrounding delete no-op does not queue rust delete`() {
3668
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3669
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
3670
- editText.setSelection(0)
3671
- editText.editorId = 1
3672
-
3673
- val deletes = mutableListOf<Pair<Int, Int>>()
3674
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
3675
- deletes.add(scalarFrom to scalarTo)
3676
- }
3677
-
3678
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
3679
- assertNotNull(inputConnection)
3680
-
3681
- assertTrue(inputConnection!!.deleteSurroundingText(3, 0))
3682
-
3683
- assertTrue(deletes.isEmpty())
3684
- assertEquals("abc", editText.text?.toString())
3685
- assertFalse(editText.hasDeferredRustUpdateApplicationForTesting())
3686
- }
3687
-
3688
- @Test
3689
- fun `text commit snaps split surrogate selection to scalar boundaries`() {
3690
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3691
- editText.applyUpdateJSON(renderUpdateJson("A😀B"), notifyListener = false)
3692
- editText.setSelection(2, 3)
3693
- editText.editorId = 1
3694
-
3695
- var replacement: Triple<Int, Int, String>? = null
3696
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3697
- replacement = Triple(scalarFrom, scalarTo, text)
3698
- }
3699
-
3700
- editText.handleTextCommit("X")
3701
-
3702
- assertEquals(Triple(1, 2, "X"), replacement)
3703
- }
3704
-
3705
- @Test
3706
- fun `selection sync snaps split surrogate selection to scalar boundaries`() {
3707
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3708
- editText.applyUpdateJSON(renderUpdateJson("A😀B"), notifyListener = false)
3709
- editText.editorId = 1
3710
-
3711
- var syncedSelection: Pair<Int, Int>? = null
3712
- editText.onSetSelectionScalarInRustForTesting = { anchor, head ->
3713
- syncedSelection = anchor to head
3714
- }
3715
-
3716
- editText.setSelection(2, 3)
3717
-
3718
- assertEquals(1 to 2, syncedSelection)
3719
- }
3720
-
3721
- @Test
3722
- fun `selection sync preserves backward anchor and head direction`() {
3723
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3724
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3725
- editText.editorId = 1
3726
-
3727
- var syncedSelection: Pair<Int, Int>? = null
3728
- editText.onSetSelectionScalarInRustForTesting = { anchor, head ->
3729
- syncedSelection = anchor to head
3730
- }
3731
-
3732
- Selection.setSelection(editText.text, 11, 6)
3733
-
3734
- assertEquals(11 to 6, syncedSelection)
3735
- }
3736
-
3737
- @Test
3738
- fun `collapsed composition range snaps split surrogate caret to insertion point`() {
3739
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3740
- editText.applyUpdateJSON(renderUpdateJson("A😀B"), notifyListener = false)
3741
-
3742
- editText.setCompositionReplacementRange(2, 2)
3743
-
3744
- assertEquals(3 to 3, editText.compositionReplacementRange())
3745
- }
3746
-
3747
- @Test
3748
- fun `backspace deletes normalized backward selection range`() {
3749
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3750
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3751
- Selection.setSelection(editText.text, 11, 6)
3752
- editText.editorId = 1
3753
-
3754
- var deletedRange: Pair<Int, Int>? = null
3755
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
3756
- deletedRange = scalarFrom to scalarTo
3757
- }
3758
-
3759
- editText.handleBackspace()
3760
-
3761
- assertEquals(6 to 11, deletedRange)
3762
- }
3763
-
3764
- @Test
3765
- fun `backspace snaps split surrogate selection to scalar boundaries`() {
3766
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3767
- editText.applyUpdateJSON(renderUpdateJson("A😀B"), notifyListener = false)
3768
- editText.setSelection(2, 3)
3769
- editText.editorId = 1
3770
-
3771
- var deletedRange: Pair<Int, Int>? = null
3772
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
3773
- deletedRange = scalarFrom to scalarTo
3774
- }
3775
-
3776
- editText.handleBackspace()
3777
-
3778
- assertEquals(1 to 2, deletedRange)
3779
- }
3780
-
3781
- @Test
3782
- fun `delete surrounding text deletes forward selected range`() {
3783
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3784
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3785
- editText.setSelection(6, 11)
3786
- editText.editorId = 1
3787
-
3788
- var deletedRange: Pair<Int, Int>? = null
3789
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
3790
- deletedRange = scalarFrom to scalarTo
3791
- }
3792
-
3793
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
3794
- assertNotNull(inputConnection)
3795
-
3796
- assertTrue(inputConnection!!.deleteSurroundingText(1, 0))
3797
-
3798
- assertEquals(6 to 11, deletedRange)
3799
- }
3800
-
3801
- @Test
3802
- fun `delete surrounding text in code points deletes backward selected range`() {
3803
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3804
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3805
- Selection.setSelection(editText.text, 11, 6)
3806
- editText.editorId = 1
3807
-
3808
- var deletedRange: Pair<Int, Int>? = null
3809
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
3810
- deletedRange = scalarFrom to scalarTo
3811
- }
3812
-
3813
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
3814
- assertNotNull(inputConnection)
3815
-
3816
- assertTrue(inputConnection!!.deleteSurroundingTextInCodePoints(1, 0))
3817
-
3818
- assertEquals(6 to 11, deletedRange)
3819
- }
3820
-
3821
- @Test
3822
- fun `delete surrounding text snaps split surrogate ranges to scalar boundaries`() {
3823
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3824
- editText.applyUpdateJSON(renderUpdateJson("A😀B"), notifyListener = false)
3825
- editText.setSelection(2)
3826
- editText.editorId = 1
3827
-
3828
- var deletedRange: Pair<Int, Int>? = null
3829
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
3830
- deletedRange = scalarFrom to scalarTo
3831
- }
3832
-
3833
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
3834
- assertNotNull(inputConnection)
3835
-
3836
- assertTrue(inputConnection!!.deleteSurroundingText(0, 1))
3837
-
3838
- assertEquals(1 to 2, deletedRange)
3839
- }
3840
-
3841
- @Test
3842
- fun `plain paste replaces selected range`() {
3843
- val context = RuntimeEnvironment.getApplication()
3844
- val editText = EditorEditText(context)
3845
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3846
- editText.setSelection(6, 11)
3847
- editText.editorId = 1
3848
-
3849
- var replacement: Triple<Int, Int, String>? = null
3850
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3851
- replacement = Triple(scalarFrom, scalarTo, text)
3852
- }
3853
-
3854
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
3855
- clipboard.setPrimaryClip(ClipData.newPlainText("plain", "there"))
3856
-
3857
- assertTrue(editText.onTextContextMenuItem(android.R.id.paste))
3858
-
3859
- assertEquals(Triple(6, 11, "there"), replacement)
3860
- }
3861
-
3862
- @Test
3863
- fun `paste as plain text ignores html and routes plain text through rust`() {
3864
- val context = RuntimeEnvironment.getApplication()
3865
- val editText = EditorEditText(context)
3866
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3867
- editText.setSelection(6, 11)
3868
- editText.editorId = 1
3869
-
3870
- var replacement: Triple<Int, Int, String>? = null
3871
- var insertedHtml: String? = null
3872
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3873
- replacement = Triple(scalarFrom, scalarTo, text)
3874
- }
3875
- editText.onInsertContentHtmlInRustForTesting = { html ->
3876
- insertedHtml = html
3877
- }
3878
-
3879
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
3880
- clipboard.setPrimaryClip(
3881
- ClipData.newHtmlText("html", "there", "<strong>there</strong>")
3882
- )
3883
-
3884
- assertTrue(editText.onTextContextMenuItem(android.R.id.pasteAsPlainText))
3885
-
3886
- assertNull(insertedHtml)
3887
- assertEquals(Triple(6, 11, "there"), replacement)
3888
- }
3889
-
3890
- @Test
3891
- fun `plain paste coerces non text clipboard item through rust`() {
3892
- val context = RuntimeEnvironment.getApplication()
3893
- val editText = EditorEditText(context)
3894
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3895
- editText.setSelection(6, 11)
3896
- editText.editorId = 1
3897
-
3898
- var replacement: Triple<Int, Int, String>? = null
3899
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3900
- replacement = Triple(scalarFrom, scalarTo, text)
3901
- }
3902
-
3903
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.test/share"))
3904
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
3905
- clipboard.setPrimaryClip(ClipData.newIntent("intent", intent))
3906
-
3907
- assertTrue(editText.onTextContextMenuItem(android.R.id.paste))
3908
-
3909
- assertEquals(
3910
- Triple(6, 11, intent.toUri(Intent.URI_INTENT_SCHEME)),
3911
- replacement
3912
- )
3913
- }
3914
-
3915
- @Test
3916
- fun `editable cut copies selection and deletes through rust`() {
3917
- val context = RuntimeEnvironment.getApplication()
3918
- val editText = EditorEditText(context)
3919
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3920
- editText.setSelection(6, 11)
3921
- editText.editorId = 1
3922
-
3923
- var deletedRange: Pair<Int, Int>? = null
3924
- editText.onDeleteRangeInRustForTesting = { scalarFrom, scalarTo ->
3925
- deletedRange = scalarFrom to scalarTo
3926
- }
3927
-
3928
- assertTrue(editText.onTextContextMenuItem(android.R.id.cut))
3929
-
3930
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
3931
- assertEquals("world", clipboard.primaryClip?.getItemAt(0)?.text?.toString())
3932
- assertEquals(6 to 11, deletedRange)
3933
- assertEquals("Hello world", editText.text?.toString())
3934
- }
3935
-
3936
- @Test
3937
- fun `read only cut and paste as plain text are consumed without mutating text`() {
3938
- val context = RuntimeEnvironment.getApplication()
3939
- val editText = EditorEditText(context)
3940
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
3941
- editText.setSelection(0, 3)
3942
- editText.isEditable = false
3943
-
3944
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
3945
- clipboard.setPrimaryClip(ClipData.newPlainText("plain", "X"))
3946
-
3947
- assertTrue(editText.onTextContextMenuItem(android.R.id.cut))
3948
- assertTrue(editText.onTextContextMenuItem(android.R.id.paste))
3949
- assertTrue(editText.onTextContextMenuItem(android.R.id.pasteAsPlainText))
3950
- assertEquals("abc", editText.text?.toString())
3951
- }
3952
-
3953
- @Test
3954
- fun `editable accessibility set text replaces full document through rust`() {
3955
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3956
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3957
- editText.setSelection(6, 11)
3958
- editText.editorId = 1
3959
-
3960
- var replacement: Triple<Int, Int, String>? = null
3961
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3962
- replacement = Triple(scalarFrom, scalarTo, text)
3963
- }
3964
- val args = Bundle().apply {
3965
- putCharSequence(
3966
- AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
3967
- "there"
3968
- )
3969
- }
3970
-
3971
- assertTrue(
3972
- editText.performAccessibilityAction(
3973
- AccessibilityNodeInfo.ACTION_SET_TEXT,
3974
- args
3975
- )
3976
- )
3977
-
3978
- assertEquals(Triple(0, 11, "there"), replacement)
3979
- assertEquals("Hello world", editText.text?.toString())
3980
- }
3981
-
3982
- @Test
3983
- fun `editable accessibility set text replaces full document when selection is collapsed`() {
3984
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
3985
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
3986
- editText.setSelection(6)
3987
- editText.editorId = 1
3988
-
3989
- var replacement: Triple<Int, Int, String>? = null
3990
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
3991
- replacement = Triple(scalarFrom, scalarTo, text)
3992
- }
3993
- val args = Bundle().apply {
3994
- putCharSequence(
3995
- AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
3996
- "replacement"
3997
- )
3998
- }
3999
-
4000
- assertTrue(
4001
- editText.performAccessibilityAction(
4002
- AccessibilityNodeInfo.ACTION_SET_TEXT,
4003
- args
4004
- )
4005
- )
4006
-
4007
- assertEquals(Triple(0, 11, "replacement"), replacement)
4008
- }
4009
-
4010
- @Test
4011
- fun `read only accessibility text mutations are rejected without mutating text`() {
4012
- val context = RuntimeEnvironment.getApplication()
4013
- val editText = EditorEditText(context)
4014
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
4015
- editText.setSelection(0, 3)
4016
- editText.isEditable = false
4017
-
4018
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
4019
- clipboard.setPrimaryClip(ClipData.newPlainText("plain", "X"))
4020
- val setTextArgs = Bundle().apply {
4021
- putCharSequence(
4022
- AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
4023
- "X"
4024
- )
4025
- }
4026
-
4027
- assertFalse(
4028
- editText.performAccessibilityAction(
4029
- AccessibilityNodeInfo.ACTION_SET_TEXT,
4030
- setTextArgs
4031
- )
4032
- )
4033
- assertFalse(editText.performAccessibilityAction(AccessibilityNodeInfo.ACTION_PASTE, null))
4034
- assertFalse(editText.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CUT, null))
4035
- assertEquals("abc", editText.text?.toString())
4036
- }
4037
-
4038
- @Test
4039
- fun `read only input connection consumes printable and forward delete keys`() {
4040
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
4041
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
4042
- editText.setSelection(1)
4043
- editText.editorId = 1
4044
- editText.isEditable = false
4045
-
4046
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
4047
- assertNotNull(inputConnection)
4048
-
4049
- assertTrue(inputConnection!!.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A)))
4050
- assertTrue(inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SPACE)))
4051
- assertTrue(inputConnection.sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_FORWARD_DEL)))
4052
- assertEquals("abc", editText.text?.toString())
4053
- assertEquals(1, editText.selectionStart)
4054
- assertEquals(1, editText.selectionEnd)
4055
- }
4056
-
4057
- @Test
4058
- fun `read only multiple character key events are consumed without mutating text`() {
4059
- val editText = EditorEditText(RuntimeEnvironment.getApplication())
4060
- editText.applyUpdateJSON(renderUpdateJson("abc"), notifyListener = false)
4061
- editText.setSelection(1)
4062
- editText.editorId = 1
4063
- editText.isEditable = false
4064
-
4065
- val inputConnection = editText.onCreateInputConnection(EditorInfo())
4066
- assertNotNull(inputConnection)
4067
- val multipleCharactersEvent = KeyEvent(100L, "é", 0, 0)
4068
-
4069
- assertTrue(editText.dispatchKeyEvent(multipleCharactersEvent))
4070
- assertTrue(inputConnection!!.sendKeyEvent(multipleCharactersEvent))
4071
- assertEquals("abc", editText.text?.toString())
4072
- assertEquals(1, editText.selectionStart)
4073
- assertEquals(1, editText.selectionEnd)
4074
- }
4075
-
4076
- @Test
4077
- fun `plain paste snaps split surrogate selection to scalar boundaries`() {
4078
- val context = RuntimeEnvironment.getApplication()
4079
- val editText = EditorEditText(context)
4080
- editText.applyUpdateJSON(renderUpdateJson("A😀B"), notifyListener = false)
4081
- editText.setSelection(2, 3)
4082
- editText.editorId = 1
4083
-
4084
- var replacement: Triple<Int, Int, String>? = null
4085
- editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
4086
- replacement = Triple(scalarFrom, scalarTo, text)
4087
- }
4088
-
4089
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
4090
- clipboard.setPrimaryClip(ClipData.newPlainText("plain", "X"))
4091
-
4092
- assertTrue(editText.onTextContextMenuItem(android.R.id.paste))
4093
-
4094
- assertEquals(Triple(1, 2, "X"), replacement)
4095
- }
4096
-
4097
- @Test
4098
- fun `multiline plain paste inserts structured content`() {
4099
- val context = RuntimeEnvironment.getApplication()
4100
- val editText = EditorEditText(context)
4101
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
4102
- editText.setSelection(6, 11)
4103
- editText.editorId = 1
4104
-
4105
- var insertedContent: Triple<Int, Int, String>? = null
4106
- editText.onInsertContentJsonAtSelectionScalarForTesting = { scalarFrom, scalarTo, json ->
4107
- insertedContent = Triple(scalarFrom, scalarTo, json)
4108
- }
4109
-
4110
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
4111
- clipboard.setPrimaryClip(ClipData.newPlainText("plain", "one\ntwo"))
4112
-
4113
- assertTrue(editText.onTextContextMenuItem(android.R.id.paste))
4114
-
4115
- val (scalarFrom, scalarTo, json) = insertedContent!!
4116
- assertEquals(6, scalarFrom)
4117
- assertEquals(11, scalarTo)
4118
- val content = JSONObject(json).getJSONArray("content")
4119
- assertEquals("one", content.getJSONObject(0).getJSONArray("content").getJSONObject(0).getString("text"))
4120
- assertEquals("two", content.getJSONObject(1).getJSONArray("content").getJSONObject(0).getString("text"))
4121
- }
4122
-
4123
- @Test
4124
- fun `html paste syncs current selection before inserting html`() {
4125
- val context = RuntimeEnvironment.getApplication()
4126
- val editText = EditorEditText(context)
4127
- editText.applyUpdateJSON(renderUpdateJson("Hello world"), notifyListener = false)
4128
- editText.setSelection(6, 11)
4129
- editText.editorId = 1
4130
-
4131
- var syncedSelection: Pair<Int, Int>? = null
4132
- editText.onSetSelectionScalarInRustForTesting = { anchor, head ->
4133
- syncedSelection = anchor to head
4134
- }
4135
- var insertedHtml: String? = null
4136
- editText.onInsertContentHtmlInRustForTesting = { html ->
4137
- insertedHtml = html
4138
- }
4139
-
4140
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
4141
- clipboard.setPrimaryClip(
4142
- ClipData.newHtmlText("html", "there", "<strong>there</strong>")
4143
- )
4144
-
4145
- assertTrue(editText.onTextContextMenuItem(android.R.id.paste))
4146
-
4147
- assertEquals(6 to 11, syncedSelection)
4148
- assertEquals("<strong>there</strong>", insertedHtml)
4149
- }
4150
-
4151
- private fun renderUpdateJson(text: String): String =
4152
- renderBlocksUpdateJson(text)
4153
-
4154
- private fun renderBlocksUpdateJson(vararg texts: String): String =
4155
- JSONObject()
4156
- .put(
4157
- "renderBlocks",
4158
- JSONArray().apply {
4159
- texts.forEach { put(paragraphRenderBlock(it)) }
4160
- }
4161
- )
4162
- .toString()
4163
-
4164
- private fun paragraphRenderBlock(text: String): JSONArray =
4165
- JSONArray()
4166
- .put(
4167
- JSONObject()
4168
- .put("type", "blockStart")
4169
- .put("nodeType", "paragraph")
4170
- .put("depth", 0)
4171
- )
4172
- .put(
4173
- JSONObject()
4174
- .put("type", "textRun")
4175
- .put("text", text)
4176
- .put("marks", JSONArray())
4177
- )
4178
- .put(JSONObject().put("type", "blockEnd"))
4179
-
4180
- private fun withDefaultInputMethod(context: Context, inputMethodId: String, block: () -> Unit) {
4181
- val previous = Settings.Secure.getString(
4182
- context.contentResolver,
4183
- Settings.Secure.DEFAULT_INPUT_METHOD
4184
- )
4185
- Settings.Secure.putString(
4186
- context.contentResolver,
4187
- Settings.Secure.DEFAULT_INPUT_METHOD,
4188
- inputMethodId
4189
- )
4190
- try {
4191
- block()
4192
- } finally {
4193
- Settings.Secure.putString(
4194
- context.contentResolver,
4195
- Settings.Secure.DEFAULT_INPUT_METHOD,
4196
- previous
4197
- )
4198
- }
4199
- }
4200
-
4201
- private infix fun Int.hasInputFlag(flag: Int): Boolean = (this and flag) == flag
4202
- }