@openeditor/react-native-prose-editor 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +35 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
- package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
- package/dist/EditorToolbar.d.ts +1 -1
- package/dist/EditorToolbar.js +19 -0
- package/dist/NativeEditorBridge.d.ts +62 -35
- package/dist/NativeEditorBridge.js +122 -79
- package/dist/NativeProseViewer.d.ts +13 -34
- package/dist/NativeProseViewer.js +40 -465
- package/dist/NativeRichTextEditor.d.ts +10 -1
- package/dist/NativeRichTextEditor.js +72 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/schemas.d.ts +11 -0
- package/dist/schemas.js +149 -0
- package/dist/useNativeEditor.d.ts +3 -3
- package/dist/useNativeEditor.js +3 -3
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/Generated_editor_core.swift +97 -2
- package/ios/NativeBlockEditorSurface.swift +1393 -0
- package/ios/NativeEditorExpoView.swift +3 -3
- package/ios/NativeEditorModule.swift +30 -45
- package/ios/NativeInputSupport.swift +396 -0
- package/ios/PositionBridge.swift +4 -4
- package/ios/RichTextEditorView.swift +333 -786
- package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
- package/package.json +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2087 -1399
- package/android/src/androidTest/AndroidManifest.xml +0 -8
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
- package/ios/EditorLayoutManager.swift +0 -774
- package/ios/NativeProseViewerExpoView.swift +0 -276
- package/ios/RenderBridge.swift +0 -1708
- package/ios/Tests/NativePerformanceTests.swift +0 -687
- package/ios/Tests/PositionBridgeTests.swift +0 -706
- package/ios/Tests/RenderBridgeTests.swift +0 -2236
- package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
-
<application>
|
|
3
|
-
<activity
|
|
4
|
-
android:name="com.openeditor.editor.NativeEditorOutsideTapActivity"
|
|
5
|
-
android:exported="false"
|
|
6
|
-
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
|
|
7
|
-
</application>
|
|
8
|
-
</manifest>
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
package com.openeditor.editor
|
|
2
|
-
|
|
3
|
-
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
4
|
-
import org.junit.Assert.assertTrue
|
|
5
|
-
import org.junit.Test
|
|
6
|
-
import org.junit.runner.RunWith
|
|
7
|
-
|
|
8
|
-
@RunWith(AndroidJUnit4::class)
|
|
9
|
-
class MeasureHeightInstrumentedTest {
|
|
10
|
-
|
|
11
|
-
@Test
|
|
12
|
-
fun measureHeight_narrowerWidthProducesTallerResult() {
|
|
13
|
-
val longText = "word ".repeat(100)
|
|
14
|
-
val renderJSON = """[{"type":"blockStart","nodeType":"paragraph","depth":0},{"type":"textRun","text":"$longText"},{"type":"blockEnd"}]"""
|
|
15
|
-
val narrowHeight = RenderBridge.measureHeight(
|
|
16
|
-
json = renderJSON,
|
|
17
|
-
themeJson = null,
|
|
18
|
-
width = 100f,
|
|
19
|
-
density = 1f
|
|
20
|
-
)
|
|
21
|
-
val wideHeight = RenderBridge.measureHeight(
|
|
22
|
-
json = renderJSON,
|
|
23
|
-
themeJson = null,
|
|
24
|
-
width = 1000f,
|
|
25
|
-
density = 1f
|
|
26
|
-
)
|
|
27
|
-
assertTrue(
|
|
28
|
-
"Narrower width ($narrowHeight) should be taller than wider ($wideHeight)",
|
|
29
|
-
narrowHeight > wideHeight
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@Test
|
|
34
|
-
fun measureHeight_largerFontProducesTallerResult() {
|
|
35
|
-
val renderJSON = """[{"type":"blockStart","nodeType":"paragraph","depth":0},{"type":"textRun","text":"Hello world"},{"type":"blockEnd"}]"""
|
|
36
|
-
val smallHeight = RenderBridge.measureHeight(
|
|
37
|
-
json = renderJSON,
|
|
38
|
-
themeJson = """{"text":{"fontSize":12}}""",
|
|
39
|
-
width = 375f,
|
|
40
|
-
density = 1f
|
|
41
|
-
)
|
|
42
|
-
val largeHeight = RenderBridge.measureHeight(
|
|
43
|
-
json = renderJSON,
|
|
44
|
-
themeJson = """{"text":{"fontSize":32}}""",
|
|
45
|
-
width = 375f,
|
|
46
|
-
density = 1f
|
|
47
|
-
)
|
|
48
|
-
assertTrue(
|
|
49
|
-
"Larger font ($largeHeight) should be taller than smaller ($smallHeight)",
|
|
50
|
-
largeHeight > smallHeight
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
}
|
package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
package com.openeditor.editor
|
|
2
|
-
|
|
3
|
-
import android.app.Activity
|
|
4
|
-
import android.app.Instrumentation
|
|
5
|
-
import android.content.Context
|
|
6
|
-
import android.graphics.Color
|
|
7
|
-
import android.os.SystemClock
|
|
8
|
-
import android.view.ViewGroup
|
|
9
|
-
import android.widget.FrameLayout
|
|
10
|
-
import androidx.test.core.app.ActivityScenario
|
|
11
|
-
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
12
|
-
import androidx.test.filters.LargeTest
|
|
13
|
-
import androidx.test.platform.app.InstrumentationRegistry
|
|
14
|
-
import expo.modules.core.ModuleRegistry
|
|
15
|
-
import expo.modules.kotlin.AppContext
|
|
16
|
-
import expo.modules.kotlin.ModulesProvider
|
|
17
|
-
import expo.modules.kotlin.modules.Module
|
|
18
|
-
import java.lang.ref.WeakReference
|
|
19
|
-
import java.util.concurrent.atomic.AtomicReference
|
|
20
|
-
import org.json.JSONArray
|
|
21
|
-
import org.json.JSONObject
|
|
22
|
-
import org.junit.Assert.assertTrue
|
|
23
|
-
import org.junit.Test
|
|
24
|
-
import org.junit.runner.RunWith
|
|
25
|
-
import uniffi.editor_core.editorCreate
|
|
26
|
-
import uniffi.editor_core.editorDestroy
|
|
27
|
-
import uniffi.editor_core.editorGetCurrentState
|
|
28
|
-
import uniffi.editor_core.editorReplaceJson
|
|
29
|
-
import uniffi.editor_core.editorSetJson
|
|
30
|
-
|
|
31
|
-
@RunWith(AndroidJUnit4::class)
|
|
32
|
-
@LargeTest
|
|
33
|
-
class NativeDeviceCollaborationInitialSyncTest {
|
|
34
|
-
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
|
|
35
|
-
|
|
36
|
-
@Test
|
|
37
|
-
fun propDrivenReplaceUpdateDisplaysRemoteDocumentWithoutTyping() {
|
|
38
|
-
ActivityScenario.launch(NativeEditorOutsideTapActivity::class.java).use { scenario ->
|
|
39
|
-
val editorRef = AtomicReference<NativeEditorExpoView>()
|
|
40
|
-
val editorId = editorCreate("{}").toLong()
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
scenario.onActivity { activity ->
|
|
44
|
-
editorRef.set(createMountedEditor(activity, editorId))
|
|
45
|
-
}
|
|
46
|
-
instrumentation.waitForIdleSync()
|
|
47
|
-
waitUntil("editor should bind initial id") {
|
|
48
|
-
editorRef.get().richTextView.editorEditText.editorId == editorId
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
val updateJson = AtomicReference<String>()
|
|
52
|
-
scenario.onActivity {
|
|
53
|
-
val update = editorReplaceJson(
|
|
54
|
-
editorId.toULong(),
|
|
55
|
-
documentJson("Remote replace sync")
|
|
56
|
-
)
|
|
57
|
-
updateJson.set(update)
|
|
58
|
-
editorRef.get().setPendingEditorUpdateJson(update)
|
|
59
|
-
editorRef.get().setPendingEditorUpdateEditorId(editorId)
|
|
60
|
-
editorRef.get().setPendingEditorUpdateRevision(1)
|
|
61
|
-
editorRef.get().applyPendingEditorUpdateIfNeeded()
|
|
62
|
-
}
|
|
63
|
-
instrumentation.waitForIdleSync()
|
|
64
|
-
|
|
65
|
-
waitUntil(
|
|
66
|
-
"replace update should render remote document",
|
|
67
|
-
detail = {
|
|
68
|
-
"text=${editorRef.get().richTextView.editorEditText.text} " +
|
|
69
|
-
"trace=${editorRef.get().richTextView.editorEditText.imeTraceSnapshotForTesting().joinToString("|")} " +
|
|
70
|
-
"update=${updateJson.get()}"
|
|
71
|
-
}
|
|
72
|
-
) {
|
|
73
|
-
editorRef.get().richTextView.editorEditText.text.toString() ==
|
|
74
|
-
"Remote replace sync"
|
|
75
|
-
}
|
|
76
|
-
} finally {
|
|
77
|
-
editorDestroy(editorId.toULong())
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
@Test
|
|
83
|
-
fun propDrivenResetUpdateDisplaysRemoteDocumentWithoutTyping() {
|
|
84
|
-
ActivityScenario.launch(NativeEditorOutsideTapActivity::class.java).use { scenario ->
|
|
85
|
-
val editorRef = AtomicReference<NativeEditorExpoView>()
|
|
86
|
-
val editorId = editorCreate("{}").toLong()
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
scenario.onActivity { activity ->
|
|
90
|
-
editorRef.set(createMountedEditor(activity, editorId))
|
|
91
|
-
}
|
|
92
|
-
instrumentation.waitForIdleSync()
|
|
93
|
-
waitUntil("editor should bind initial id") {
|
|
94
|
-
editorRef.get().richTextView.editorEditText.editorId == editorId
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
val updateJson = AtomicReference<String>()
|
|
98
|
-
scenario.onActivity {
|
|
99
|
-
editorSetJson(editorId.toULong(), documentJson("Remote reset sync"))
|
|
100
|
-
val update = editorGetCurrentState(editorId.toULong())
|
|
101
|
-
updateJson.set(update)
|
|
102
|
-
editorRef.get().setPendingEditorResetUpdateJson(update)
|
|
103
|
-
editorRef.get().setPendingEditorResetUpdateEditorId(editorId)
|
|
104
|
-
editorRef.get().setPendingEditorResetUpdateRevision(1)
|
|
105
|
-
editorRef.get().applyPendingEditorResetUpdateIfNeeded()
|
|
106
|
-
}
|
|
107
|
-
instrumentation.waitForIdleSync()
|
|
108
|
-
|
|
109
|
-
waitUntil(
|
|
110
|
-
"reset update should render remote document",
|
|
111
|
-
detail = {
|
|
112
|
-
"text=${editorRef.get().richTextView.editorEditText.text} " +
|
|
113
|
-
"trace=${editorRef.get().richTextView.editorEditText.imeTraceSnapshotForTesting().joinToString("|")} " +
|
|
114
|
-
"update=${updateJson.get()}"
|
|
115
|
-
}
|
|
116
|
-
) {
|
|
117
|
-
editorRef.get().richTextView.editorEditText.text.toString() ==
|
|
118
|
-
"Remote reset sync"
|
|
119
|
-
}
|
|
120
|
-
} finally {
|
|
121
|
-
editorDestroy(editorId.toULong())
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private fun createMountedEditor(
|
|
127
|
-
activity: Activity,
|
|
128
|
-
editorId: Long
|
|
129
|
-
): NativeEditorExpoView {
|
|
130
|
-
initializeSoLoaderIfAvailable(activity)
|
|
131
|
-
val root = FrameLayout(activity).apply {
|
|
132
|
-
setBackgroundColor(Color.WHITE)
|
|
133
|
-
}
|
|
134
|
-
val expoContext = testExpoContext(activity)
|
|
135
|
-
val editor = NativeEditorExpoView(expoContext.context, expoContext.appContext).apply {
|
|
136
|
-
clipToPadding = false
|
|
137
|
-
setShowToolbar(false)
|
|
138
|
-
onFocusChangeForTesting = {}
|
|
139
|
-
onAddonEventForTesting = {}
|
|
140
|
-
onEditorUpdateForTesting = {}
|
|
141
|
-
onEditorReadyForTesting = {}
|
|
142
|
-
}
|
|
143
|
-
root.addView(
|
|
144
|
-
editor,
|
|
145
|
-
FrameLayout.LayoutParams(
|
|
146
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
147
|
-
dp(activity, 240)
|
|
148
|
-
).apply {
|
|
149
|
-
topMargin = dp(activity, 48)
|
|
150
|
-
leftMargin = dp(activity, 16)
|
|
151
|
-
rightMargin = dp(activity, 16)
|
|
152
|
-
}
|
|
153
|
-
)
|
|
154
|
-
activity.setContentView(root)
|
|
155
|
-
editor.setEditorId(editorId)
|
|
156
|
-
return editor
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private fun documentJson(text: String): String =
|
|
160
|
-
JSONObject()
|
|
161
|
-
.put("type", "doc")
|
|
162
|
-
.put(
|
|
163
|
-
"content",
|
|
164
|
-
JSONArray().put(
|
|
165
|
-
JSONObject()
|
|
166
|
-
.put("type", "paragraph")
|
|
167
|
-
.put(
|
|
168
|
-
"content",
|
|
169
|
-
JSONArray().put(
|
|
170
|
-
JSONObject()
|
|
171
|
-
.put("type", "text")
|
|
172
|
-
.put("text", text)
|
|
173
|
-
)
|
|
174
|
-
)
|
|
175
|
-
)
|
|
176
|
-
)
|
|
177
|
-
.toString()
|
|
178
|
-
|
|
179
|
-
private fun waitUntil(
|
|
180
|
-
description: String,
|
|
181
|
-
timeoutMs: Long = 4_000,
|
|
182
|
-
detail: () -> String = { "" },
|
|
183
|
-
condition: () -> Boolean
|
|
184
|
-
) {
|
|
185
|
-
val start = SystemClock.uptimeMillis()
|
|
186
|
-
while (SystemClock.uptimeMillis() - start < timeoutMs) {
|
|
187
|
-
instrumentation.waitForIdleSync()
|
|
188
|
-
if (condition()) return
|
|
189
|
-
SystemClock.sleep(50)
|
|
190
|
-
}
|
|
191
|
-
assertTrue("$description\n${detail()}", condition())
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
private fun dp(context: Context, value: Int): Int =
|
|
195
|
-
(value * context.resources.displayMetrics.density).toInt()
|
|
196
|
-
|
|
197
|
-
private fun initializeSoLoaderIfAvailable(context: Context) {
|
|
198
|
-
try {
|
|
199
|
-
Class
|
|
200
|
-
.forName("com.facebook.soloader.SoLoader")
|
|
201
|
-
.getMethod(
|
|
202
|
-
"init",
|
|
203
|
-
Context::class.java,
|
|
204
|
-
Boolean::class.javaPrimitiveType
|
|
205
|
-
)
|
|
206
|
-
.invoke(null, context, false)
|
|
207
|
-
} catch (_: Throwable) {
|
|
208
|
-
// Some test classpaths do not expose SoLoader directly; in that case the view can
|
|
209
|
-
// still be exercised as long as the React Native draw path does not require it.
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
private fun testExpoContext(activity: Activity): TestExpoContext {
|
|
214
|
-
val reactContext = Class
|
|
215
|
-
.forName("com.facebook.react.bridge.BridgeReactContext")
|
|
216
|
-
.getConstructor(Context::class.java)
|
|
217
|
-
.newInstance(activity) as Context
|
|
218
|
-
|
|
219
|
-
reactContext.javaClass
|
|
220
|
-
.getMethod("onHostResume", Activity::class.java)
|
|
221
|
-
.invoke(reactContext, activity)
|
|
222
|
-
|
|
223
|
-
val modulesProvider = object : ModulesProvider {
|
|
224
|
-
override fun getModulesList(): List<Class<out Module>> = emptyList()
|
|
225
|
-
}
|
|
226
|
-
val constructor = AppContext::class.java.constructors.first { constructor ->
|
|
227
|
-
constructor.parameterTypes.size == 3
|
|
228
|
-
}
|
|
229
|
-
val appContext = constructor.newInstance(
|
|
230
|
-
modulesProvider,
|
|
231
|
-
ModuleRegistry(emptyList(), emptyList()),
|
|
232
|
-
WeakReference(reactContext)
|
|
233
|
-
) as AppContext
|
|
234
|
-
return TestExpoContext(reactContext, appContext)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
private data class TestExpoContext(
|
|
238
|
-
val context: Context,
|
|
239
|
-
val appContext: AppContext
|
|
240
|
-
)
|
|
241
|
-
}
|
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
package com.openeditor.editor
|
|
2
|
-
|
|
3
|
-
import android.app.Instrumentation
|
|
4
|
-
import android.content.Context
|
|
5
|
-
import android.text.Selection
|
|
6
|
-
import android.view.inputmethod.BaseInputConnection
|
|
7
|
-
import android.view.inputmethod.CompletionInfo
|
|
8
|
-
import android.view.inputmethod.CorrectionInfo
|
|
9
|
-
import android.view.inputmethod.EditorInfo
|
|
10
|
-
import androidx.test.core.app.ApplicationProvider
|
|
11
|
-
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
12
|
-
import androidx.test.filters.LargeTest
|
|
13
|
-
import androidx.test.platform.app.InstrumentationRegistry
|
|
14
|
-
import java.util.concurrent.atomic.AtomicReference
|
|
15
|
-
import org.json.JSONArray
|
|
16
|
-
import org.json.JSONObject
|
|
17
|
-
import org.junit.Assert.assertEquals
|
|
18
|
-
import org.junit.Assert.assertNull
|
|
19
|
-
import org.junit.Assert.assertTrue
|
|
20
|
-
import org.junit.Test
|
|
21
|
-
import org.junit.runner.RunWith
|
|
22
|
-
|
|
23
|
-
@RunWith(AndroidJUnit4::class)
|
|
24
|
-
@LargeTest
|
|
25
|
-
class NativeDeviceImeRegressionTest {
|
|
26
|
-
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
|
|
27
|
-
private val context = ApplicationProvider.getApplicationContext<Context>()
|
|
28
|
-
|
|
29
|
-
@Test
|
|
30
|
-
fun commitCompletionReplacesSelectedTextThroughRust() {
|
|
31
|
-
val result = runOnMainSyncWithResult {
|
|
32
|
-
val editText = createEditor("hel", selectionStart = 0, selectionEnd = 3)
|
|
33
|
-
var replacement: Replacement? = null
|
|
34
|
-
editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
|
|
35
|
-
replacement = Replacement(scalarFrom, scalarTo, text)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
val inputConnection = createInputConnection(editText)
|
|
39
|
-
assertTrue(inputConnection.commitCompletion(CompletionInfo(1L, 0, "hello")))
|
|
40
|
-
|
|
41
|
-
ImeResult(
|
|
42
|
-
replacement = replacement,
|
|
43
|
-
trace = editText.imeTraceSnapshotForTesting()
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
assertEquals(Replacement(0, 3, "hello"), result.replacement)
|
|
48
|
-
assertTraceContains(result.trace, "commitCompletion")
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
@Test
|
|
52
|
-
fun commitCorrectionCoversOldTextAndInferredTokenBoundaries() {
|
|
53
|
-
val explicit = runCorrectionScenario("teh", offset = 0, oldText = "teh", newText = "the")
|
|
54
|
-
assertEquals(Replacement(0, 3, "the"), explicit.replacement)
|
|
55
|
-
assertNull(explicit.inserted)
|
|
56
|
-
assertTraceContains(explicit.trace, "correctionExplicitApply")
|
|
57
|
-
|
|
58
|
-
val trailingPeriod = runCorrectionScenario("teh.", offset = 0, oldText = null, newText = "the")
|
|
59
|
-
assertEquals(Replacement(0, 3, "the"), trailingPeriod.replacement)
|
|
60
|
-
assertNull(trailingPeriod.inserted)
|
|
61
|
-
assertTraceContains(trailingPeriod.trace, "correctionInferredApply")
|
|
62
|
-
|
|
63
|
-
val punctuationOffset = runCorrectionScenario("teh.", offset = 3, oldText = null, newText = "the")
|
|
64
|
-
assertNull(punctuationOffset.replacement)
|
|
65
|
-
assertNull(punctuationOffset.inserted)
|
|
66
|
-
assertTraceContains(punctuationOffset.trace, "correctionInferredNoop")
|
|
67
|
-
|
|
68
|
-
val whitespaceOffset = runCorrectionScenario("teh ", offset = 3, oldText = null, newText = "the")
|
|
69
|
-
assertNull(whitespaceOffset.replacement)
|
|
70
|
-
assertNull(whitespaceOffset.inserted)
|
|
71
|
-
assertTraceContains(whitespaceOffset.trace, "correctionInferredNoop")
|
|
72
|
-
|
|
73
|
-
val hyphenated = runCorrectionScenario(
|
|
74
|
-
"dont-stop ",
|
|
75
|
-
offset = 4,
|
|
76
|
-
oldText = null,
|
|
77
|
-
newText = "don't-stop"
|
|
78
|
-
)
|
|
79
|
-
assertEquals(Replacement(0, 9, "don't-stop"), hyphenated.replacement)
|
|
80
|
-
|
|
81
|
-
val apostrophe = runCorrectionScenario("cant's ", offset = 4, oldText = null, newText = "can't")
|
|
82
|
-
assertEquals(Replacement(0, 6, "can't"), apostrophe.replacement)
|
|
83
|
-
|
|
84
|
-
val surrogate = runCorrectionScenario("te😀h ", offset = 3, oldText = null, newText = "term")
|
|
85
|
-
assertEquals(Replacement(0, 4, "term"), surrogate.replacement)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
@Test
|
|
89
|
-
fun visibleCompositionCorrectionCommitDeleteAndPreflightAreRouted() {
|
|
90
|
-
val correction = runOnMainSyncWithResult {
|
|
91
|
-
val editText = createEditor("", selectionStart = 0, selectionEnd = 0)
|
|
92
|
-
var inserted: Inserted? = null
|
|
93
|
-
editText.onInsertTextInRustForTesting = { text, scalar ->
|
|
94
|
-
inserted = Inserted(text, scalar)
|
|
95
|
-
editText.applyUpdateJSON(renderUpdateJson(text), notifyListener = false)
|
|
96
|
-
editText.setSelection(text.length)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
val inputConnection = createInputConnection(editText)
|
|
100
|
-
assertTrue(inputConnection.setComposingText("teh", 1))
|
|
101
|
-
assertTrue(inputConnection.commitCorrection(CorrectionInfo(0, "teh", "the")))
|
|
102
|
-
|
|
103
|
-
ImeResult(
|
|
104
|
-
inserted = inserted,
|
|
105
|
-
trace = editText.imeTraceSnapshotForTesting()
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
assertEquals(Inserted("the", 0), correction.inserted)
|
|
110
|
-
assertTraceContains(correction.trace, "setComposingText")
|
|
111
|
-
assertTraceContains(correction.trace, "commitCorrectionComposition")
|
|
112
|
-
|
|
113
|
-
val deletion = runOnMainSyncWithResult {
|
|
114
|
-
val editText = createEditor("", selectionStart = 0, selectionEnd = 0)
|
|
115
|
-
var inserted: Inserted? = null
|
|
116
|
-
editText.onInsertTextInRustForTesting = { text, scalar ->
|
|
117
|
-
inserted = Inserted(text, scalar)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
val inputConnection = createInputConnection(editText)
|
|
121
|
-
assertTrue(inputConnection.setComposingText("abcd", 1))
|
|
122
|
-
assertTrue(inputConnection.deleteSurroundingText(1, 0))
|
|
123
|
-
assertTrue(inputConnection.finishComposingText())
|
|
124
|
-
|
|
125
|
-
ImeResult(
|
|
126
|
-
inserted = inserted,
|
|
127
|
-
trace = editText.imeTraceSnapshotForTesting()
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
assertEquals(Inserted("abc", 0), deletion.inserted)
|
|
132
|
-
assertTraceContains(deletion.trace, "finishComposingText")
|
|
133
|
-
|
|
134
|
-
val preflight = runOnMainSyncWithResult {
|
|
135
|
-
val editText = createEditor("", selectionStart = 0, selectionEnd = 0)
|
|
136
|
-
var inserted: Inserted? = null
|
|
137
|
-
editText.onInsertTextInRustForTesting = { text, scalar ->
|
|
138
|
-
inserted = Inserted(text, scalar)
|
|
139
|
-
editText.applyUpdateJSON(renderUpdateJson(text), notifyListener = false)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
val inputConnection = createInputConnection(editText)
|
|
143
|
-
assertTrue(inputConnection.setComposingText("abc", 1))
|
|
144
|
-
val ready = editText.prepareForExternalEditorUpdate()
|
|
145
|
-
|
|
146
|
-
ImeResult(
|
|
147
|
-
inserted = inserted,
|
|
148
|
-
trace = editText.imeTraceSnapshotForTesting(),
|
|
149
|
-
ready = ready
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
assertTrue(preflight.ready)
|
|
154
|
-
assertEquals(Inserted("abc", 0), preflight.inserted)
|
|
155
|
-
assertTraceContains(preflight.trace, "finishComposingText")
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
@Test
|
|
159
|
-
fun nativeEditableAutocorrectBeforeAndAfterBlurIsAdoptedAndSelectionIsPreserved() {
|
|
160
|
-
val focused = runOnMainSyncWithResult {
|
|
161
|
-
val editText = createEditor("teh ", selectionStart = 4, selectionEnd = 4)
|
|
162
|
-
assertTrue(editText.requestFocus())
|
|
163
|
-
var replacement: Replacement? = null
|
|
164
|
-
editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
|
|
165
|
-
replacement = Replacement(scalarFrom, scalarTo, text)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
editText.text!!.replace(0, 3, "the")
|
|
169
|
-
|
|
170
|
-
ImeResult(
|
|
171
|
-
replacement = replacement,
|
|
172
|
-
selection = editText.selectionStart to editText.selectionEnd,
|
|
173
|
-
trace = editText.imeTraceSnapshotForTesting()
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
assertEquals(Replacement(1, 3, "he"), focused.replacement)
|
|
178
|
-
assertEquals(4 to 4, focused.selection)
|
|
179
|
-
assertTraceContains(focused.trace, "nativeMutationApply")
|
|
180
|
-
|
|
181
|
-
val afterBlur = runOnMainSyncWithResult {
|
|
182
|
-
val editText = createEditor("teh ", selectionStart = 4, selectionEnd = 4)
|
|
183
|
-
assertTrue(editText.requestFocus())
|
|
184
|
-
var replacement: Replacement? = null
|
|
185
|
-
editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, text ->
|
|
186
|
-
replacement = Replacement(scalarFrom, scalarTo, text)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
editText.clearFocus()
|
|
190
|
-
editText.runWithTransientInputMutationGuard {
|
|
191
|
-
editText.text!!.replace(0, 3, "the")
|
|
192
|
-
BaseInputConnection.setComposingSpans(editText.text!!)
|
|
193
|
-
true
|
|
194
|
-
}
|
|
195
|
-
val ready = editText.prepareForExternalEditorUpdate()
|
|
196
|
-
|
|
197
|
-
ImeResult(
|
|
198
|
-
replacement = replacement,
|
|
199
|
-
selection = editText.selectionStart to editText.selectionEnd,
|
|
200
|
-
trace = editText.imeTraceSnapshotForTesting(),
|
|
201
|
-
ready = ready
|
|
202
|
-
)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
assertTrue(afterBlur.ready)
|
|
206
|
-
assertEquals(Replacement(1, 3, "he"), afterBlur.replacement)
|
|
207
|
-
assertEquals(4 to 4, afterBlur.selection)
|
|
208
|
-
assertTraceContains(afterBlur.trace, "nativeMutationApply")
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
@Test
|
|
212
|
-
fun staleAndInvalidCorrectionsDoNotFallBackToInsertion() {
|
|
213
|
-
val stale = runCorrectionScenario("tah ", offset = 0, oldText = "teh", newText = "the")
|
|
214
|
-
assertNull(stale.replacement)
|
|
215
|
-
assertNull(stale.inserted)
|
|
216
|
-
assertTraceContains(stale.trace, "correctionExplicitNoop")
|
|
217
|
-
|
|
218
|
-
val invalidExplicit = runCorrectionScenario("teh ", offset = -1, oldText = "teh", newText = "the")
|
|
219
|
-
assertNull(invalidExplicit.replacement)
|
|
220
|
-
assertNull(invalidExplicit.inserted)
|
|
221
|
-
assertTraceContains(invalidExplicit.trace, "commitCorrectionResult")
|
|
222
|
-
|
|
223
|
-
val invalidInferred = runCorrectionScenario("teh ", offset = -1, oldText = null, newText = "the")
|
|
224
|
-
assertNull(invalidInferred.replacement)
|
|
225
|
-
assertNull(invalidInferred.inserted)
|
|
226
|
-
assertTraceContains(invalidInferred.trace, "correctionInferredNoop")
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private fun runCorrectionScenario(
|
|
230
|
-
text: String,
|
|
231
|
-
offset: Int,
|
|
232
|
-
oldText: String?,
|
|
233
|
-
newText: String
|
|
234
|
-
): ImeResult =
|
|
235
|
-
runOnMainSyncWithResult {
|
|
236
|
-
val editText = createEditor(text, selectionStart = text.length, selectionEnd = text.length)
|
|
237
|
-
var replacement: Replacement? = null
|
|
238
|
-
var inserted: Inserted? = null
|
|
239
|
-
editText.onReplaceTextInRustForTesting = { scalarFrom, scalarTo, replacementText ->
|
|
240
|
-
replacement = Replacement(scalarFrom, scalarTo, replacementText)
|
|
241
|
-
}
|
|
242
|
-
editText.onInsertTextInRustForTesting = { insertedText, scalar ->
|
|
243
|
-
inserted = Inserted(insertedText, scalar)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
val inputConnection = createInputConnection(editText)
|
|
247
|
-
assertTrue(inputConnection.commitCorrection(CorrectionInfo(offset, oldText, newText)))
|
|
248
|
-
|
|
249
|
-
ImeResult(
|
|
250
|
-
replacement = replacement,
|
|
251
|
-
inserted = inserted,
|
|
252
|
-
trace = editText.imeTraceSnapshotForTesting()
|
|
253
|
-
)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private fun createEditor(
|
|
257
|
-
text: String,
|
|
258
|
-
selectionStart: Int,
|
|
259
|
-
selectionEnd: Int
|
|
260
|
-
): EditorEditText =
|
|
261
|
-
EditorEditText(context).apply {
|
|
262
|
-
applyUpdateJSON(renderUpdateJson(text), notifyListener = false)
|
|
263
|
-
Selection.setSelection(this.text, selectionStart, selectionEnd)
|
|
264
|
-
onSetSelectionScalarInRustForTesting = { _, _ -> }
|
|
265
|
-
editorId = 1
|
|
266
|
-
clearImeTraceForTesting()
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
private fun createInputConnection(editText: EditorEditText): EditorInputConnection {
|
|
270
|
-
val inputConnection = editText.onCreateInputConnection(EditorInfo())
|
|
271
|
-
assertTrue(inputConnection is EditorInputConnection)
|
|
272
|
-
return inputConnection as EditorInputConnection
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
private fun assertTraceContains(trace: List<String>, event: String) {
|
|
276
|
-
assertTrue(
|
|
277
|
-
"expected IME trace to contain $event but was $trace",
|
|
278
|
-
trace.any { it.startsWith(event) }
|
|
279
|
-
)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
private fun renderUpdateJson(text: String): String =
|
|
283
|
-
JSONObject()
|
|
284
|
-
.put(
|
|
285
|
-
"renderBlocks",
|
|
286
|
-
JSONArray().put(
|
|
287
|
-
JSONArray()
|
|
288
|
-
.put(
|
|
289
|
-
JSONObject()
|
|
290
|
-
.put("type", "blockStart")
|
|
291
|
-
.put("nodeType", "paragraph")
|
|
292
|
-
.put("depth", 0)
|
|
293
|
-
)
|
|
294
|
-
.put(
|
|
295
|
-
JSONObject()
|
|
296
|
-
.put("type", "textRun")
|
|
297
|
-
.put("text", text)
|
|
298
|
-
.put("marks", JSONArray())
|
|
299
|
-
)
|
|
300
|
-
.put(JSONObject().put("type", "blockEnd"))
|
|
301
|
-
)
|
|
302
|
-
)
|
|
303
|
-
.toString()
|
|
304
|
-
|
|
305
|
-
@Suppress("UNCHECKED_CAST")
|
|
306
|
-
private fun <T> runOnMainSyncWithResult(block: () -> T): T {
|
|
307
|
-
val result = AtomicReference<Any?>()
|
|
308
|
-
val error = AtomicReference<Throwable?>()
|
|
309
|
-
instrumentation.runOnMainSync {
|
|
310
|
-
try {
|
|
311
|
-
result.set(block())
|
|
312
|
-
} catch (throwable: Throwable) {
|
|
313
|
-
error.set(throwable)
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
error.get()?.let { throw it }
|
|
317
|
-
return result.get() as T
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
private data class Replacement(
|
|
321
|
-
val scalarFrom: Int,
|
|
322
|
-
val scalarTo: Int,
|
|
323
|
-
val text: String
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
private data class Inserted(
|
|
327
|
-
val text: String,
|
|
328
|
-
val scalar: Int
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
private data class ImeResult(
|
|
332
|
-
val replacement: Replacement? = null,
|
|
333
|
-
val inserted: Inserted? = null,
|
|
334
|
-
val selection: Pair<Int, Int> = 0 to 0,
|
|
335
|
-
val trace: List<String> = emptyList(),
|
|
336
|
-
val ready: Boolean = false
|
|
337
|
-
)
|
|
338
|
-
}
|