1 /*
2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.foundation.text.input.internal.undo
18 
19 import androidx.compose.foundation.text.input.TextFieldState
20 import androidx.compose.foundation.text.input.setSelectionCoerced
21 import androidx.compose.foundation.text.timeNowMillis
22 import androidx.compose.runtime.saveable.Saver
23 import androidx.compose.runtime.saveable.SaverScope
24 import androidx.compose.ui.text.TextRange
25 
26 /**
27  * An undo identifier designed for text editors. Defines a single atomic change that can be applied
28  * directly or in reverse to modify the contents of a text editor.
29  *
30  * @param index Start point of [preText] and [postText].
31  * @param preText Previously written text that's deleted starting from [index].
32  * @param postText New text that's inserted at [index]
33  * @param preSelection Previous selection before changes are applied
34  * @param postSelection New selection after changes are applied
35  * @param timeInMillis When did this change was first committed
36  * @param canMerge Whether this change can be merged with the next or previous change in an undo
37  *   stack. There are many other rules that affect the merging strategy between two
38  *   [TextUndoOperation]s but this flag is a sure way to force a non-mergeable property.
39  */
40 internal class TextUndoOperation(
41     val index: Int,
42     val preText: String,
43     val postText: String,
44     val preSelection: TextRange,
45     val postSelection: TextRange,
46     val timeInMillis: Long = timeNowMillis(),
47     val canMerge: Boolean = true
48 ) {
49 
50     /**
51      * What kind of edit operation is defined by this change. Edit type is decided by forward the
52      * behavior of this change in forward direction (pre -> post).
53      */
54     val textEditType: TextEditType =
55         when {
56             preText.isEmpty() && postText.isEmpty() ->
57                 throw IllegalArgumentException("Either pre or post text must not be empty")
58             preText.isEmpty() && postText.isNotEmpty() -> TextEditType.Insert
59             preText.isNotEmpty() && postText.isEmpty() -> TextEditType.Delete
60             else -> TextEditType.Replace
61         }
62 
63     /** Only required while deciding whether to merge two deletion type undo operations. */
64     val deletionType: TextDeleteType
65         get() {
66             if (textEditType != TextEditType.Delete) return TextDeleteType.NotByUser
67             if (!postSelection.collapsed) return TextDeleteType.NotByUser
68             if (preSelection.collapsed) {
69                 return if (preSelection.start > postSelection.start) {
70                     TextDeleteType.Start
71                 } else {
72                     TextDeleteType.End
73                 }
74             } else if (preSelection.start == postSelection.start && preSelection.start == index) {
75                 return TextDeleteType.Inner
76             }
77             return TextDeleteType.NotByUser
78         }
79 
80     companion object {
81 
82         val Saver =
83             object : Saver<TextUndoOperation, Any> {
savenull84                 override fun SaverScope.save(value: TextUndoOperation): Any =
85                     listOf(
86                         value.index,
87                         value.preText,
88                         value.postText,
89                         value.preSelection.start,
90                         value.preSelection.end,
91                         value.postSelection.start,
92                         value.postSelection.end,
93                         value.timeInMillis
94                     )
95 
96                 override fun restore(value: Any): TextUndoOperation {
97                     return with((value as List<*>)) {
98                         TextUndoOperation(
99                             index = get(0) as Int,
100                             preText = get(1) as String,
101                             postText = get(2) as String,
102                             preSelection = TextRange(get(3) as Int, get(4) as Int),
103                             postSelection = TextRange(get(5) as Int, get(6) as Int),
104                             timeInMillis = get(7) as Long,
105                         )
106                     }
107                 }
108             }
109     }
110 }
111 
112 /** Apply a given [TextUndoOperation] in reverse to undo this [TextFieldState]. */
undonull113 internal fun TextFieldState.undo(op: TextUndoOperation) {
114     editWithNoSideEffects {
115         replace(op.index, op.index + op.postText.length, op.preText)
116         setSelectionCoerced(op.preSelection.start, op.preSelection.end)
117     }
118 }
119 
120 /** Apply a given [TextUndoOperation] in forward direction to redo this [TextFieldState]. */
redonull121 internal fun TextFieldState.redo(op: TextUndoOperation) {
122     editWithNoSideEffects {
123         replace(op.index, op.index + op.preText.length, op.postText)
124         setSelectionCoerced(op.postSelection.start, op.postSelection.end)
125     }
126 }
127 
128 /**
129  * Possible types of a text operation.
130  * 1. Insert; if the edited range has 0 length, and the new text is longer than 0 length
131  * 2. Delete: if the edited range is longer than 0, and the new text has 0 length
132  * 3. Replace: All other changes.
133  */
134 internal enum class TextEditType {
135     Insert,
136     Delete,
137     Replace
138 }
139 
140 /**
141  * When a delete occurs during text editing, it can happen in various shapes.
142  * 1. Start; When a single character is removed to the start (towards 0) of the cursor, backspace
143  *    key behavior. "abcd|efg" -> "abc|efg"
144  * 2. End; When a single character is removed to the end (towards length) of the cursor, delete key
145  *    behavior. "abcd|efg" -> "abcd|fg"
146  * 3. Inner; When a selection of characters are removed, directionless. Both backspace and delete
147  *    express the same behavior in this case. "ab|cde|fg" -> "ab|fg"
148  * 4. NotByUser; A text editing operation that cannot be executed via a hardware or software
149  *    keyboard. For example when a portion of text is removed but it's not next to a cursor or
150  *    selection, or selection remains after removal. "abcd|efg" -> "bcd|efg" "abc|def|g" -> "a|bc|g"
151  */
152 internal enum class TextDeleteType {
153     Start,
154     End,
155     Inner,
156     NotByUser
157 }
158 
159 /**
160  * There are multiple strategies while deciding how to add certain edit operations to undo stack.
161  * - Normally, merge is decided by UndoOperation's own merge logic, comparing itself to the latest
162  *   operation in the Undo stack.
163  * - Programmatic updates should clear the history completely.
164  * - Some atomic actions like cut, and paste shouldn't merge to previous or next actions.
165  */
166 internal enum class TextFieldEditUndoBehavior {
167     MergeIfPossible,
168     ClearHistory,
169     NeverMerge
170 }
171