Skip to content

Commit 6e500ea

Browse files
authored
Merge pull request #673 from MohamedRejeb/feature/undo-redo
feat: implement undo/redo history system for rich text editor
2 parents a2db683 + 245218f commit 6e500ea

29 files changed

Lines changed: 2253 additions & 108 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ venv/
6262
.claude
6363

6464
CLAUDE.md
65+
AGENTS.md
6566

6667
kotlin-js-store
6768

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ A rich text editor library for both Jetpack Compose and Compose Multiplatform, f
1515
- **Multiplatform**: Compose Rich Editor supports Compose Multiplatform (Android, iOS, Desktop, Web).
1616
- **Easy to use**: Compose Rich Editor's API leverages Kotlin's language features for simplicity and minimal boilerplate.
1717
- **WYSIWYG**: Compose Rich Editor is a WYSIWYG editor that supports the most common text styling features.
18+
- **Undo / Redo**: Built-in rich-text-aware undo/redo (`state.history`) that respects formatting, overriding `BasicTextField`'s default.
1819

1920
## Screenshots
2021

docs/rich_text_state.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,57 @@ richTextState.setHtml(savedHtml)
115115
richTextState.setMarkdown(savedMarkdown)
116116
```
117117

118+
## Undo / Redo
119+
120+
`RichTextState` ships its own undo/redo stack that snapshots the full rich-text
121+
tree (paragraphs, spans, list prefixes, link/image/token spans, selection, and
122+
pending styles). It overrides `BasicTextField`'s built-in undo so rich content
123+
never gets out of sync with plain text.
124+
125+
```kotlin
126+
val state = rememberRichTextState(
127+
historyLimit = 100,
128+
coalesceWindowMs = 500L,
129+
)
130+
131+
IconButton(onClick = { state.history.undo() }, enabled = state.history.canUndo) {
132+
Icon(Icons.AutoMirrored.Filled.Undo, contentDescription = "Undo")
133+
}
134+
IconButton(onClick = { state.history.redo() }, enabled = state.history.canRedo) {
135+
Icon(Icons.AutoMirrored.Filled.Redo, contentDescription = "Redo")
136+
}
137+
```
138+
139+
### Keyboard shortcuts (hardware keyboard)
140+
141+
- Undo: `Ctrl+Z` (Windows/Linux) / `Cmd+Z` (macOS)
142+
- Redo: `Ctrl+Shift+Z` (Windows/Linux) / `Cmd+Shift+Z` (macOS)
143+
144+
### Coalescing
145+
146+
Consecutive typing / deletion within `coalesceWindowMs` (default 500ms) collapses
147+
into a single undo step. The following always start a new group:
148+
149+
- Line breaks (Enter)
150+
- Formatting toggles (bold, color, link, list, paragraph style, rich span style)
151+
- Structural changes (image/token insert, list level changes)
152+
- Paste operations
153+
- Programmatic replacements (`setHtml`, `setMarkdown`, `setConfig` — these also
154+
clear the stacks entirely since they typically mean "load a new document")
155+
- Caret moves (do not push a snapshot but seal the pending group, so the next
156+
typed character starts a fresh undo step)
157+
158+
### Opt out
159+
160+
Pass `undoBehavior = UndoBehavior.Disabled` to any editor composable to fall
161+
back to `BasicTextField`'s native shortcuts. You can still call
162+
`state.history.undo()` directly from your own UI.
163+
164+
### Limits
165+
166+
`state.history.limit` caps the undo stack (default 100 entries; oldest are
167+
evicted FIFO). `state.history.clear()` empties both stacks.
168+
118169
## Related Documentation
119170

120171
- For styling text spans, see [Span Style](span_style.md)

richeditor-compose/api/android/richeditor-compose.api

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ public final class com/mohamedrejeb/richeditor/model/RichTextConfig {
151151
public final class com/mohamedrejeb/richeditor/model/RichTextState {
152152
public static final field $stable I
153153
public static final field Companion Lcom/mohamedrejeb/richeditor/model/RichTextState$Companion;
154-
public fun <init> ()V
154+
public fun <init> (IJ)V
155+
public synthetic fun <init> (IJILkotlin/jvm/internal/DefaultConstructorMarker;)V
155156
public final fun addCode ()V
156157
public final fun addCodeSpan ()V
157158
public final fun addLink (Ljava/lang/String;Ljava/lang/String;)V
@@ -183,6 +184,7 @@ public final class com/mohamedrejeb/richeditor/model/RichTextState {
183184
public final fun getCurrentParagraphStyle ()Landroidx/compose/ui/text/ParagraphStyle;
184185
public final fun getCurrentRichSpanStyle ()Lcom/mohamedrejeb/richeditor/model/RichSpanStyle;
185186
public final fun getCurrentSpanStyle ()Landroidx/compose/ui/text/SpanStyle;
187+
public final fun getHistory ()Lcom/mohamedrejeb/richeditor/model/history/RichTextHistory;
186188
public final fun getParagraphStyle-5zc-tL8 (J)Landroidx/compose/ui/text/ParagraphStyle;
187189
public final fun getRichSpanStyle-5zc-tL8 (J)Lcom/mohamedrejeb/richeditor/model/RichSpanStyle;
188190
public final fun getSelectedLinkText ()Ljava/lang/String;
@@ -248,7 +250,7 @@ public final class com/mohamedrejeb/richeditor/model/RichTextState$Companion {
248250
}
249251

250252
public final class com/mohamedrejeb/richeditor/model/RichTextStateKt {
251-
public static final fun rememberRichTextState (Landroidx/compose/runtime/Composer;I)Lcom/mohamedrejeb/richeditor/model/RichTextState;
253+
public static final fun rememberRichTextState (IJLandroidx/compose/runtime/Composer;II)Lcom/mohamedrejeb/richeditor/model/RichTextState;
252254
}
253255

254256
public final class com/mohamedrejeb/richeditor/model/TextPaddingValues {
@@ -266,6 +268,19 @@ public final class com/mohamedrejeb/richeditor/model/TextPaddingValues {
266268
public fun toString ()Ljava/lang/String;
267269
}
268270

271+
public final class com/mohamedrejeb/richeditor/model/history/RichTextHistory {
272+
public static final field $stable I
273+
public final fun clear ()V
274+
public final fun getCanRedo ()Z
275+
public final fun getCanUndo ()Z
276+
public final fun getCoalesceWindowMs ()J
277+
public final fun getLimit ()I
278+
public final fun redo ()Z
279+
public final fun setCoalesceWindowMs (J)V
280+
public final fun setLimit (I)V
281+
public final fun undo ()Z
282+
}
283+
269284
public final class com/mohamedrejeb/richeditor/model/trigger/Trigger {
270285
public static final field $stable I
271286
public fun <init> (Ljava/lang/String;CLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function6;Ljava/util/Set;ZI)V
@@ -389,8 +404,8 @@ public final class com/mohamedrejeb/richeditor/paragraph/type/UnorderedListStyle
389404
}
390405

391406
public final class com/mohamedrejeb/richeditor/ui/BasicRichTextEditorKt {
392-
public static final fun BasicRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
393-
public static final fun BasicRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lkotlin/jvm/functions/Function3;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;III)V
407+
public static final fun BasicRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lcom/mohamedrejeb/richeditor/ui/UndoBehavior;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
408+
public static final fun BasicRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Brush;Lcom/mohamedrejeb/richeditor/ui/UndoBehavior;Lkotlin/jvm/functions/Function3;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;III)V
394409
}
395410

396411
public final class com/mohamedrejeb/richeditor/ui/BasicRichTextKt {
@@ -400,24 +415,32 @@ public final class com/mohamedrejeb/richeditor/ui/BasicRichTextKt {
400415
public final class com/mohamedrejeb/richeditor/ui/ComposableSingletons$BasicRichTextEditorKt {
401416
public static final field INSTANCE Lcom/mohamedrejeb/richeditor/ui/ComposableSingletons$BasicRichTextEditorKt;
402417
public fun <init> ()V
403-
public final fun getLambda$-340365259$richeditor_compose_release ()Lkotlin/jvm/functions/Function3;
404-
public final fun getLambda$2112343050$richeditor_compose_release ()Lkotlin/jvm/functions/Function3;
418+
public final fun getLambda$58895838$richeditor_compose_release ()Lkotlin/jvm/functions/Function3;
419+
public final fun getLambda$626927937$richeditor_compose_release ()Lkotlin/jvm/functions/Function3;
420+
}
421+
422+
public final class com/mohamedrejeb/richeditor/ui/UndoBehavior : java/lang/Enum {
423+
public static final field Disabled Lcom/mohamedrejeb/richeditor/ui/UndoBehavior;
424+
public static final field Enabled Lcom/mohamedrejeb/richeditor/ui/UndoBehavior;
425+
public static fun getEntries ()Lkotlin/enums/EnumEntries;
426+
public static fun valueOf (Ljava/lang/String;)Lcom/mohamedrejeb/richeditor/ui/UndoBehavior;
427+
public static fun values ()[Lcom/mohamedrejeb/richeditor/ui/UndoBehavior;
405428
}
406429

407430
public final class com/mohamedrejeb/richeditor/ui/material/OutlinedRichTextEditorKt {
408-
public static final fun OutlinedRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILandroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/material/TextFieldColors;Landroidx/compose/runtime/Composer;III)V
431+
public static final fun OutlinedRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILandroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/material/TextFieldColors;Lcom/mohamedrejeb/richeditor/ui/UndoBehavior;Landroidx/compose/runtime/Composer;III)V
409432
}
410433

411434
public final class com/mohamedrejeb/richeditor/ui/material/RichTextEditorKt {
412-
public static final fun RichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/material/TextFieldColors;Landroidx/compose/runtime/Composer;III)V
435+
public static final fun RichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Landroidx/compose/material/TextFieldColors;Lcom/mohamedrejeb/richeditor/ui/UndoBehavior;Landroidx/compose/runtime/Composer;IIII)V
413436
}
414437

415438
public final class com/mohamedrejeb/richeditor/ui/material/RichTextKt {
416439
public static final fun RichText-a0LXGaU (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontFamily;JLandroidx/compose/ui/text/style/TextDecoration;IJIZIILjava/util/Map;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/text/TextStyle;Lcom/mohamedrejeb/richeditor/model/ImageLoader;Landroidx/compose/runtime/Composer;III)V
417440
}
418441

419442
public final class com/mohamedrejeb/richeditor/ui/material3/OutlinedRichTextEditorKt {
420-
public static final fun OutlinedRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Lcom/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;IIII)V
443+
public static final fun OutlinedRichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Lcom/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors;Landroidx/compose/foundation/layout/PaddingValues;Lcom/mohamedrejeb/richeditor/ui/UndoBehavior;Landroidx/compose/runtime/Composer;IIII)V
421444
}
422445

423446
public final class com/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors {
@@ -452,7 +475,7 @@ public final class com/mohamedrejeb/richeditor/ui/material3/RichTextEditorDefaul
452475
}
453476

454477
public final class com/mohamedrejeb/richeditor/ui/material3/RichTextEditorKt {
455-
public static final fun RichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Lcom/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;IIII)V
478+
public static final fun RichTextEditor (Lcom/mohamedrejeb/richeditor/model/RichTextState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ZIIILkotlin/jvm/functions/Function1;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/graphics/Shape;Lcom/mohamedrejeb/richeditor/ui/material3/RichTextEditorColors;Landroidx/compose/foundation/layout/PaddingValues;Lcom/mohamedrejeb/richeditor/ui/UndoBehavior;Landroidx/compose/runtime/Composer;IIII)V
456479
}
457480

458481
public final class com/mohamedrejeb/richeditor/ui/material3/RichTextKt {

0 commit comments

Comments
 (0)