[gtk/wip/chergert/textundo] wip on text undo



commit 179fb93a6b10a908005519540975e39c776a5c88
Author: Christian Hergert <chergert redhat com>
Date:   Wed Oct 23 19:13:11 2019 -0700

    wip on text undo

 gtk/gtkentrybuffer.c        | 172 ++++++++
 gtk/gtkentrybuffer.h        |  22 +
 gtk/gtktext.c               |  68 ++-
 gtk/gtktextbuffer.c         | 262 +++++++++++-
 gtk/gtktextbuffer.h         |  11 +
 gtk/gtktexthistory.c        | 975 ++++++++++++++++++++++++++++++++++++++++++++
 gtk/gtktexthistoryprivate.h |  81 ++++
 gtk/gtktextprivate.h        |   2 +
 gtk/gtktextview.c           |  63 +++
 gtk/gtktextview.h           |   2 +
 gtk/istring.h               | 168 ++++++++
 gtk/meson.build             |   1 +
 12 files changed, 1819 insertions(+), 8 deletions(-)
---
diff --git a/gtk/gtkentrybuffer.c b/gtk/gtkentrybuffer.c
index 0ac7cf3a0b..5e6723b109 100644
--- a/gtk/gtkentrybuffer.c
+++ b/gtk/gtkentrybuffer.c
@@ -21,6 +21,7 @@
 #include "gtkintl.h"
 #include "gtkmarshalers.h"
 #include "gtkprivate.h"
+#include "gtktexthistoryprivate.h"
 #include "gtkwidget.h"
 
 #include <gdk/gdk.h>
@@ -50,6 +51,8 @@
 
 enum {
   PROP_0,
+  PROP_CAN_REDO,
+  PROP_CAN_UNDO,
   PROP_TEXT,
   PROP_LENGTH,
   PROP_MAX_LENGTH,
@@ -76,6 +79,8 @@ struct _GtkEntryBufferPrivate
   guint  normal_text_chars;
 
   gint   max_length;
+
+  GtkTextHistory *history;
 };
 
 G_DEFINE_TYPE_WITH_PRIVATE (GtkEntryBuffer, gtk_entry_buffer, G_TYPE_OBJECT)
@@ -248,11 +253,66 @@ gtk_entry_buffer_real_deleted_text (GtkEntryBuffer *buffer,
  *
  */
 
+static void
+gtk_entry_buffer_history_state_change (gpointer funcs_data,
+                                       gboolean is_modified,
+                                       gboolean can_undo,
+                                       gboolean can_redo)
+{
+  GtkEntryBuffer *buffer = funcs_data;
+
+  g_object_notify_by_pspec (G_OBJECT (buffer), entry_buffer_props[PROP_CAN_UNDO]);
+  g_object_notify_by_pspec (G_OBJECT (buffer), entry_buffer_props[PROP_CAN_REDO]);
+}
+
+static void
+gtk_entry_buffer_history_insert (gpointer     funcs_data,
+                                 guint        begin,
+                                 guint        end,
+                                 const gchar *text,
+                                 guint        len)
+{
+  GtkEntryBuffer *buffer = funcs_data;
+
+  gtk_entry_buffer_insert_text (buffer,
+                                begin,
+                                text,
+                                g_utf8_strlen (text, len));
+}
+
+static void
+gtk_entry_buffer_history_delete (gpointer     funcs_data,
+                                 guint        begin,
+                                 guint        end,
+                                 const gchar *expected_text,
+                                 guint        len)
+{
+  GtkEntryBuffer *buffer = funcs_data;
+
+  gtk_entry_buffer_delete_text (buffer, begin, end - begin);
+}
+
+static void
+gtk_entry_buffer_history_select (gpointer funcs_data,
+                                 int      selection_insert,
+                                 int      selection_bound)
+{
+}
+
+static const GtkTextHistoryFuncs history_funcs = {
+  gtk_entry_buffer_history_state_change,
+  gtk_entry_buffer_history_insert,
+  gtk_entry_buffer_history_delete,
+  gtk_entry_buffer_history_select,
+};
+
 static void
 gtk_entry_buffer_init (GtkEntryBuffer *buffer)
 {
   GtkEntryBufferPrivate *pv = gtk_entry_buffer_get_instance_private (buffer);
 
+  pv->history = gtk_text_history_new (&history_funcs, buffer);
+
   pv->normal_text = NULL;
   pv->normal_text_chars = 0;
   pv->normal_text_bytes = 0;
@@ -265,6 +325,8 @@ gtk_entry_buffer_finalize (GObject *obj)
   GtkEntryBuffer *buffer = GTK_ENTRY_BUFFER (obj);
   GtkEntryBufferPrivate *pv = gtk_entry_buffer_get_instance_private (buffer);
 
+  g_clear_object (&pv->history);
+
   if (pv->normal_text)
     {
       trash_area (pv->normal_text, pv->normal_text_size);
@@ -309,6 +371,12 @@ gtk_entry_buffer_get_property (GObject    *obj,
 
   switch (prop_id)
     {
+    case PROP_CAN_REDO:
+      g_value_set_boolean (value, gtk_entry_buffer_get_can_redo (buffer));
+      break;
+    case PROP_CAN_UNDO:
+      g_value_set_boolean (value, gtk_entry_buffer_get_can_undo (buffer));
+      break;
     case PROP_TEXT:
       g_value_set_string (value, gtk_entry_buffer_get_text (buffer));
       break;
@@ -341,6 +409,32 @@ gtk_entry_buffer_class_init (GtkEntryBufferClass *klass)
   klass->inserted_text = gtk_entry_buffer_real_inserted_text;
   klass->deleted_text = gtk_entry_buffer_real_deleted_text;
 
+  /**
+   * GtkEntryBuffer:can-redo:
+   *
+   * The :can-redo property denotes that the entry buffer can
+   * redo the last undone operation.
+   */
+  entry_buffer_props[PROP_CAN_REDO] =
+    g_param_spec_boolean ("can-redo",
+                          P_("can-redo"),
+                          P_("If the entry buffer can redo the last undone operation"),
+                          FALSE,
+                          GTK_PARAM_READABLE);
+
+  /**
+   * GtkEntryBuffer:can-undo:
+   *
+   * The :can-undo property denotes that the entry buffer can
+   * undo the last operation.
+   */
+  entry_buffer_props[PROP_CAN_UNDO] =
+    g_param_spec_boolean ("can-undo",
+                          P_("can-undo"),
+                          P_("If the entry buffer can undo the last operation"),
+                          FALSE,
+                          GTK_PARAM_READABLE);
+
   /**
    * GtkEntryBuffer:text:
    *
@@ -648,6 +742,11 @@ gtk_entry_buffer_insert_text (GtkEntryBuffer *buffer,
   klass = GTK_ENTRY_BUFFER_GET_CLASS (buffer);
   g_return_val_if_fail (klass->insert_text != NULL, 0);
 
+  gtk_text_history_text_inserted (pv->history,
+                                  position,
+                                  chars,
+                                  strlen (chars));
+
   return (*klass->insert_text) (buffer, position, chars, n_chars);
 }
 
@@ -673,7 +772,10 @@ gtk_entry_buffer_delete_text (GtkEntryBuffer *buffer,
                               guint           position,
                               gint            n_chars)
 {
+  GtkEntryBufferPrivate *priv = gtk_entry_buffer_get_instance_private (buffer);
   GtkEntryBufferClass *klass;
+  const gchar *text;
+  gchar *removed;
   guint length;
 
   g_return_val_if_fail (GTK_IS_ENTRY_BUFFER (buffer), 0);
@@ -689,6 +791,16 @@ gtk_entry_buffer_delete_text (GtkEntryBuffer *buffer,
   klass = GTK_ENTRY_BUFFER_GET_CLASS (buffer);
   g_return_val_if_fail (klass->delete_text != NULL, 0);
 
+  text = gtk_entry_buffer_get_text (buffer);
+
+  removed = g_utf8_substring (text, position, position + n_chars);
+  gtk_text_history_text_deleted (priv->history,
+                                 position,
+                                 position + n_chars,
+                                 removed,
+                                 -1);
+  g_free (removed);
+
   return (*klass->delete_text) (buffer, position, n_chars);
 }
 
@@ -727,3 +839,63 @@ gtk_entry_buffer_emit_deleted_text (GtkEntryBuffer *buffer,
   g_return_if_fail (GTK_IS_ENTRY_BUFFER (buffer));
   g_signal_emit (buffer, signals[DELETED_TEXT], 0, position, n_chars);
 }
+
+gboolean
+gtk_entry_buffer_get_can_redo (GtkEntryBuffer *buffer)
+{
+  GtkEntryBufferPrivate *priv = gtk_entry_buffer_get_instance_private (buffer);
+
+  g_return_val_if_fail (GTK_IS_ENTRY_BUFFER (buffer), FALSE);
+
+  return gtk_text_history_get_can_redo (priv->history);
+}
+
+gboolean
+gtk_entry_buffer_get_can_undo (GtkEntryBuffer *buffer)
+{
+  GtkEntryBufferPrivate *priv = gtk_entry_buffer_get_instance_private (buffer);
+
+  g_return_val_if_fail (GTK_IS_ENTRY_BUFFER (buffer), FALSE);
+
+  return gtk_text_history_get_can_undo (priv->history);
+}
+
+void
+gtk_entry_buffer_undo (GtkEntryBuffer *buffer)
+{
+  GtkEntryBufferPrivate *priv = gtk_entry_buffer_get_instance_private (buffer);
+
+  g_return_if_fail (GTK_IS_ENTRY_BUFFER (buffer));
+
+  return gtk_text_history_undo (priv->history);
+}
+
+void
+gtk_entry_buffer_redo (GtkEntryBuffer *buffer)
+{
+  GtkEntryBufferPrivate *priv = gtk_entry_buffer_get_instance_private (buffer);
+
+  g_return_if_fail (GTK_IS_ENTRY_BUFFER (buffer));
+
+  return gtk_text_history_redo (priv->history);
+}
+
+void
+gtk_entry_buffer_begin_user_action (GtkEntryBuffer *buffer)
+{
+  GtkEntryBufferPrivate *priv = gtk_entry_buffer_get_instance_private (buffer);
+
+  g_return_if_fail (GTK_IS_ENTRY_BUFFER (buffer));
+
+  return gtk_text_history_begin_user_action (priv->history);
+}
+
+void
+gtk_entry_buffer_end_user_action (GtkEntryBuffer *buffer)
+{
+  GtkEntryBufferPrivate *priv = gtk_entry_buffer_get_instance_private (buffer);
+
+  g_return_if_fail (GTK_IS_ENTRY_BUFFER (buffer));
+
+  return gtk_text_history_end_user_action (priv->history);
+}
diff --git a/gtk/gtkentrybuffer.h b/gtk/gtkentrybuffer.h
index ce270d18a8..9124bb3723 100644
--- a/gtk/gtkentrybuffer.h
+++ b/gtk/gtkentrybuffer.h
@@ -76,6 +76,10 @@ struct _GtkEntryBufferClass
                                           guint           position,
                                           guint           n_chars);
 
+  void         (*undo)                   (GtkEntryBuffer *buffer);
+
+  void         (*redo)                   (GtkEntryBuffer *buffer);
+
   /* Padding for future expansion */
   void (*_gtk_reserved1) (void);
   void (*_gtk_reserved2) (void);
@@ -137,6 +141,24 @@ void                      gtk_entry_buffer_emit_deleted_text      (GtkEntryBuffe
                                                                    guint            position,
                                                                    guint            n_chars);
 
+GDK_AVAILABLE_IN_ALL
+void                      gtk_entry_buffer_begin_user_action      (GtkEntryBuffer  *buffer);
+
+GDK_AVAILABLE_IN_ALL
+void                      gtk_entry_buffer_end_user_action        (GtkEntryBuffer  *buffer);
+
+GDK_AVAILABLE_IN_ALL
+gboolean                  gtk_entry_buffer_get_can_undo           (GtkEntryBuffer  *buffer);
+
+GDK_AVAILABLE_IN_ALL
+gboolean                  gtk_entry_buffer_get_can_redo           (GtkEntryBuffer  *buffer);
+
+GDK_AVAILABLE_IN_ALL
+void                      gtk_entry_buffer_undo                   (GtkEntryBuffer  *buffer);
+
+GDK_AVAILABLE_IN_ALL
+void                      gtk_entry_buffer_redo                   (GtkEntryBuffer  *buffer);
+
 G_END_DECLS
 
 #endif /* __GTK_ENTRY_BUFFER_H__ */
diff --git a/gtk/gtktext.c b/gtk/gtktext.c
index 698b1421eb..33ededdf8c 100644
--- a/gtk/gtktext.c
+++ b/gtk/gtktext.c
@@ -243,6 +243,8 @@ enum {
   TOGGLE_OVERWRITE,
   PREEDIT_CHANGED,
   INSERT_EMOJI,
+  UNDO,
+  REDO,
   LAST_SIGNAL
 };
 
@@ -559,6 +561,8 @@ static void gtk_text_activate_selection_select_all   (GtkWidget  *widget,
 static void gtk_text_activate_misc_insert_emoji      (GtkWidget  *widget,
                                                       const char *action_name,
                                                       GVariant   *parameter);
+static void gtk_text_real_undo                       (GtkText    *text);
+static void gtk_text_real_redo                       (GtkText    *text);
 
 /* GtkTextContent implementation
  */
@@ -719,7 +723,9 @@ gtk_text_class_init (GtkTextClass *class)
   class->toggle_overwrite = gtk_text_toggle_overwrite;
   class->insert_emoji = gtk_text_insert_emoji;
   class->activate = gtk_text_real_activate;
-
+  class->undo = gtk_text_real_undo;
+  class->redo = gtk_text_real_redo;
+ 
   quark_password_hint = g_quark_from_static_string ("gtk-entry-password-hint");
 
   text_props[PROP_BUFFER] =
@@ -1173,6 +1179,40 @@ gtk_text_class_init (GtkTextClass *class)
                   NULL,
                   G_TYPE_NONE, 0);
 
+  /**
+   * GtkText::undo:
+   * @self: the object which received the signal
+   *
+   * The ::undo signal is a
+   * [keybinding signal][GtkBindingSignal]
+   * which gets emitted to undo the last operation.
+   */
+  signals[UNDO] =
+    g_signal_new (I_("undo"),
+                  G_OBJECT_CLASS_TYPE (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (GtkTextClass, undo),
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE, 0);
+
+  /**
+   * GtkText::redo:
+   * @self: the object which received the signal
+   *
+   * The ::redo signal is a
+   * [keybinding signal][GtkBindingSignal]
+   * which gets emitted to redo the last undone operation.
+   */
+  signals[REDO] =
+    g_signal_new (I_("redo"),
+                  G_OBJECT_CLASS_TYPE (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (GtkTextClass, redo),
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE, 0);
+
   /*
    * Key bindings
    */
@@ -1346,6 +1386,12 @@ gtk_text_class_init (GtkTextClass *class)
   gtk_binding_entry_add_signal (binding_set, GDK_KEY_semicolon, GDK_CONTROL_MASK,
                                 "insert-emoji", 0);
 
+  /* Undo/Redo */
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_z, GDK_CONTROL_MASK,
+                                "undo", 0);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_z, GDK_CONTROL_MASK | GDK_SHIFT_MASK,
+                                "redo", 0);
+
   gtk_widget_class_set_accessible_type (widget_class, GTK_TYPE_TEXT_ACCESSIBLE);
   gtk_widget_class_set_css_name (widget_class, I_("text"));
 
@@ -6815,3 +6861,23 @@ gtk_text_get_extra_menu (GtkText *self)
 
   return priv->extra_menu;
 }
+
+static void
+gtk_text_real_undo (GtkText *text)
+{
+  GtkTextPrivate *priv = gtk_text_get_instance_private (text);
+
+  g_return_if_fail (GTK_IS_TEXT (text));
+
+  gtk_entry_buffer_undo (priv->buffer);
+}
+
+static void
+gtk_text_real_redo (GtkText *text)
+{
+  GtkTextPrivate *priv = gtk_text_get_instance_private (text);
+
+  g_return_if_fail (GTK_IS_TEXT (text));
+
+  gtk_entry_buffer_redo (priv->buffer);
+}
diff --git a/gtk/gtktextbuffer.c b/gtk/gtktextbuffer.c
index 5434696670..ffb9ed4f02 100644
--- a/gtk/gtktextbuffer.c
+++ b/gtk/gtktextbuffer.c
@@ -30,6 +30,7 @@
 #include "gtkdnd.h"
 #include "gtkmarshalers.h"
 #include "gtktextbuffer.h"
+#include "gtktexthistoryprivate.h"
 #include "gtktextbufferprivate.h"
 #include "gtktextbtree.h"
 #include "gtktextiterprivate.h"
@@ -62,11 +63,15 @@ struct _GtkTextBufferPrivate
 
   GtkTextLogAttrCache *log_attr_cache;
 
+  GtkTextHistory *history;
+
   guint user_action_count;
 
   /* Whether the buffer has been modified since last save */
   guint modified : 1;
   guint has_selection : 1;
+  guint can_undo : 1;
+  guint can_redo : 1;
 };
 
 typedef struct _ClipboardRequest ClipboardRequest;
@@ -93,6 +98,8 @@ enum {
   BEGIN_USER_ACTION,
   END_USER_ACTION,
   PASTE_DONE,
+  UNDO,
+  REDO,
   LAST_SIGNAL
 };
 
@@ -108,6 +115,8 @@ enum {
   PROP_CURSOR_POSITION,
   PROP_COPY_TARGET_LIST,
   PROP_PASTE_TARGET_LIST,
+  PROP_CAN_UNDO,
+  PROP_CAN_REDO,
   LAST_PROP
 };
 
@@ -138,6 +147,8 @@ static void gtk_text_buffer_real_changed               (GtkTextBuffer     *buffe
 static void gtk_text_buffer_real_mark_set              (GtkTextBuffer     *buffer,
                                                         const GtkTextIter *iter,
                                                         GtkTextMark       *mark);
+static void gtk_text_buffer_real_undo                  (GtkTextBuffer     *buffer);
+static void gtk_text_buffer_real_redo                  (GtkTextBuffer     *buffer);
 
 static GtkTextBTree* get_btree (GtkTextBuffer *buffer);
 static void          free_log_attr_cache (GtkTextLogAttrCache *cache);
@@ -154,6 +165,24 @@ static void gtk_text_buffer_get_property (GObject         *object,
                                          GValue          *value,
                                          GParamSpec      *pspec);
 
+static void gtk_text_buffer_history_change_state (gpointer     funcs_data,
+                                                  gboolean     is_modified,
+                                                  gboolean     can_undo,
+                                                  gboolean     can_redo);
+static void gtk_text_buffer_history_insert       (gpointer     funcs_data,
+                                                  guint        begin,
+                                                  guint        end,
+                                                  const char  *text,
+                                                  guint        len);
+static void gtk_text_buffer_history_delete       (gpointer     funcs_data,
+                                                  guint        begin,
+                                                  guint        end,
+                                                  const char  *expected_text,
+                                                  guint        len);
+static void gtk_text_buffer_history_select       (gpointer     funcs_data,
+                                                  int          selection_insert,
+                                                  int          selection_bound);
+
 static guint signals[LAST_SIGNAL] = { 0 };
 static GParamSpec *text_buffer_props[LAST_PROP];
 
@@ -185,6 +214,13 @@ GType gtk_text_buffer_content_get_type (void) G_GNUC_CONST;
 
 G_DEFINE_TYPE (GtkTextBufferContent, gtk_text_buffer_content, GDK_TYPE_CONTENT_PROVIDER)
 
+static GtkTextHistoryFuncs history_funcs = {
+  gtk_text_buffer_history_change_state,
+  gtk_text_buffer_history_insert,
+  gtk_text_buffer_history_delete,
+  gtk_text_buffer_history_select,
+};
+
 static GdkContentFormats *
 gtk_text_buffer_content_ref_formats (GdkContentProvider *provider)
 {
@@ -403,6 +439,8 @@ gtk_text_buffer_class_init (GtkTextBufferClass *klass)
   klass->remove_tag = gtk_text_buffer_real_remove_tag;
   klass->changed = gtk_text_buffer_real_changed;
   klass->mark_set = gtk_text_buffer_real_mark_set;
+  klass->undo = gtk_text_buffer_real_undo;
+  klass->redo = gtk_text_buffer_real_redo;
 
   /* Construct */
   text_buffer_props[PROP_TAG_TABLE] =
@@ -439,6 +477,32 @@ gtk_text_buffer_class_init (GtkTextBufferClass *klass)
                             FALSE,
                             GTK_PARAM_READABLE);
 
+  /**
+   * GtkTextBuffer:can-undo:
+   *
+   * The "can-undo" property denotes that the buffer can have the
+   * last operation undone by calling gtk_text_buffer_undo().
+   */
+  text_buffer_props[PROP_CAN_UNDO] =
+    g_param_spec_boolean ("can-undo",
+                          P_("Can Undo"),
+                          P_("If the buffer can have the last action undone"),
+                          FALSE,
+                          G_PARAM_READABLE);
+
+  /**
+   * GtkTextBuffer:can-redo:
+   *
+   * The "can-redo" property denotes that the buffer can reapply the
+   * last operation which was undone by calling gtk_text_buffer_redo().
+   */
+  text_buffer_props[PROP_CAN_REDO] =
+    g_param_spec_boolean ("can-redo",
+                          P_("Can Redo"),
+                          P_("If the buffer can have the last undone action reapplied"),
+                          FALSE,
+                          G_PARAM_READABLE);
+
   /**
    * GtkTextBuffer:cursor-position:
    *
@@ -840,6 +904,34 @@ gtk_text_buffer_class_init (GtkTextBufferClass *klass)
                   1,
                   GDK_TYPE_CLIPBOARD);
 
+  /**
+   * GtkTextBuffer::redo:
+   * @buffer: a #GtkTextBuffer
+   *
+   * The "redo" signal is emitted when a request has been made to redo the
+   * previously undone operation.
+   */
+  signals[REDO] =
+    g_signal_new (I_("redo"),
+                  G_OBJECT_CLASS_TYPE (object_class),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (GtkTextBufferClass, redo),
+                  NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  /**
+   * GtkTextBuffer::undo:
+   * @buffer: a #GtkTextBuffer
+   *
+   * The "undo" signal is emitted when a request has been made to undo the
+   * previous operation or set of operations that have been grouped together.
+   */
+  signals[UNDO] =
+    g_signal_new (I_("undo"),
+                  G_OBJECT_CLASS_TYPE (object_class),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (GtkTextBufferClass, undo),
+                  NULL, NULL, NULL, G_TYPE_NONE, 0);
+
   gtk_text_buffer_register_serializers ();
 }
 
@@ -848,6 +940,7 @@ gtk_text_buffer_init (GtkTextBuffer *buffer)
 {
   buffer->priv = gtk_text_buffer_get_instance_private (buffer);
   buffer->priv->tag_table = NULL;
+  buffer->priv->history = gtk_text_history_new (&history_funcs, buffer);
 }
 
 static void
@@ -946,6 +1039,14 @@ gtk_text_buffer_get_property (GObject         *object,
       g_value_set_int (value, gtk_text_iter_get_offset (&iter));
       break;
 
+    case PROP_CAN_UNDO:
+      g_value_set_boolean (value, gtk_text_buffer_get_can_undo (text_buffer));
+      break;
+
+    case PROP_CAN_REDO:
+      g_value_set_boolean (value, gtk_text_buffer_get_can_redo (text_buffer));
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -981,6 +1082,8 @@ gtk_text_buffer_finalize (GObject *object)
 
   remove_all_selection_clipboards (buffer);
 
+  g_clear_object (&buffer->priv->history);
+
   if (priv->tag_table)
     {
       _gtk_text_tag_table_remove_buffer (priv->tag_table, buffer);
@@ -1058,6 +1161,8 @@ gtk_text_buffer_set_text (GtkTextBuffer *buffer,
   if (len < 0)
     len = strlen (text);
 
+  gtk_text_history_begin_irreversible_action (buffer->priv->history);
+
   gtk_text_buffer_get_bounds (buffer, &start, &end);
 
   gtk_text_buffer_delete (buffer, &start, &end);
@@ -1067,6 +1172,8 @@ gtk_text_buffer_set_text (GtkTextBuffer *buffer,
       gtk_text_buffer_get_iter_at_offset (buffer, &start, 0);
       gtk_text_buffer_insert (buffer, &start, text, len);
     }
+
+  gtk_text_history_end_irreversible_action (buffer->priv->history);
 }
 
  
@@ -1084,6 +1191,11 @@ gtk_text_buffer_real_insert_text (GtkTextBuffer *buffer,
   g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
   g_return_if_fail (iter != NULL);
   
+  gtk_text_history_text_inserted (buffer->priv->history,
+                                  gtk_text_iter_get_offset (iter),
+                                  text,
+                                  len);
+
   _gtk_text_btree_insert (iter, text, len);
 
   g_signal_emit (buffer, signals[CHANGED], 0);
@@ -1792,12 +1904,30 @@ gtk_text_buffer_real_delete_range (GtkTextBuffer *buffer,
                                    GtkTextIter   *start,
                                    GtkTextIter   *end)
 {
+  GtkTextIter sel_begin, sel_end;
+  gchar *text;
   gboolean has_selection;
 
   g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
   g_return_if_fail (start != NULL);
   g_return_if_fail (end != NULL);
 
+  if (gtk_text_buffer_get_selection_bounds (buffer, &sel_begin, &sel_end))
+    gtk_text_history_selection_changed (buffer->priv->history,
+                                        gtk_text_iter_get_offset (&sel_begin),
+                                        gtk_text_iter_get_offset (&sel_end));
+  else
+    gtk_text_history_selection_changed (buffer->priv->history,
+                                        gtk_text_iter_get_offset (&sel_begin),
+                                        -1);
+
+  text = gtk_text_iter_get_slice (start, end);
+  gtk_text_history_text_deleted (buffer->priv->history,
+                                 gtk_text_iter_get_offset (start),
+                                 gtk_text_iter_get_offset (end),
+                                 text, -1);
+  g_free (text);
+
   _gtk_text_btree_delete (start, end);
 
   /* may have deleted the selection... */
@@ -1812,6 +1942,8 @@ gtk_text_buffer_real_delete_range (GtkTextBuffer *buffer,
 
   g_signal_emit (buffer, signals[CHANGED], 0);
   g_object_notify_by_pspec (G_OBJECT (buffer), text_buffer_props[PROP_CURSOR_POSITION]);
+  g_object_notify_by_pspec (G_OBJECT (buffer), text_buffer_props[PROP_CAN_UNDO]);
+  g_object_notify_by_pspec (G_OBJECT (buffer), text_buffer_props[PROP_CAN_REDO]);
 }
 
 static void
@@ -3274,17 +3406,14 @@ void
 gtk_text_buffer_set_modified (GtkTextBuffer *buffer,
                               gboolean       setting)
 {
-  gboolean fixed_setting;
-
   g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
 
-  fixed_setting = setting != FALSE;
+  setting = !!setting;
 
-  if (buffer->priv->modified == fixed_setting)
-    return;
-  else
+  if (buffer->priv->modified != setting)
     {
-      buffer->priv->modified = fixed_setting;
+      buffer->priv->modified = setting;
+      gtk_text_history_modified_changed (buffer->priv->history, setting);
       g_signal_emit (buffer, signals[MODIFIED_CHANGED], 0);
     }
 }
@@ -4723,3 +4852,122 @@ gtk_text_buffer_insert_markup (GtkTextBuffer *buffer,
   pango_attr_list_unref (attributes);
   g_free (text); 
 }
+
+static void
+gtk_text_buffer_real_undo (GtkTextBuffer *buffer)
+{
+  if (gtk_text_history_get_can_undo (buffer->priv->history))
+    gtk_text_history_undo (buffer->priv->history);
+}
+
+static void
+gtk_text_buffer_real_redo (GtkTextBuffer *buffer)
+{
+  if (gtk_text_history_get_can_redo (buffer->priv->history))
+    gtk_text_history_redo (buffer->priv->history);
+}
+
+gboolean
+gtk_text_buffer_get_can_undo (GtkTextBuffer *buffer)
+{
+  g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), FALSE);
+
+  return gtk_text_history_get_can_undo (buffer->priv->history);
+}
+
+gboolean
+gtk_text_buffer_get_can_redo (GtkTextBuffer *buffer)
+{
+  g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), FALSE);
+
+  return gtk_text_history_get_can_redo (buffer->priv->history);
+}
+
+static void
+gtk_text_buffer_history_change_state (gpointer funcs_data,
+                                      gboolean is_modified,
+                                      gboolean can_undo,
+                                      gboolean can_redo)
+{
+  GtkTextBuffer *buffer = funcs_data;
+
+  if (buffer->priv->can_undo != can_undo)
+    {
+      buffer->priv->can_undo = can_undo;
+      g_object_notify_by_pspec (G_OBJECT (buffer), text_buffer_props[PROP_CAN_UNDO]);
+    }
+
+  if (buffer->priv->can_redo != can_redo)
+    {
+      buffer->priv->can_redo = can_redo;
+      g_object_notify_by_pspec (G_OBJECT (buffer), text_buffer_props[PROP_CAN_REDO]);
+    }
+
+  if (buffer->priv->modified != is_modified)
+    gtk_text_buffer_set_modified (buffer, is_modified);
+}
+
+static void
+gtk_text_buffer_history_insert (gpointer    funcs_data,
+                                guint       begin,
+                                guint       end,
+                                const char *text,
+                                guint       len)
+{
+  GtkTextBuffer *buffer = funcs_data;
+  GtkTextIter iter;
+
+  gtk_text_buffer_get_iter_at_offset (buffer, &iter, begin);
+  gtk_text_buffer_insert (buffer, &iter, text, len);
+}
+
+static void
+gtk_text_buffer_history_delete (gpointer    funcs_data,
+                                guint       begin,
+                                guint       end,
+                                const char *expected_text,
+                                guint       len)
+{
+  GtkTextBuffer *buffer = funcs_data;
+  GtkTextIter iter;
+  GtkTextIter end_iter;
+
+  gtk_text_buffer_get_iter_at_offset (buffer, &iter, begin);
+  gtk_text_buffer_get_iter_at_offset (buffer, &end_iter, end);
+  gtk_text_buffer_delete (buffer, &iter, &end_iter);
+}
+
+static void
+gtk_text_buffer_history_select (gpointer funcs_data,
+                                int      selection_insert,
+                                int      selection_bound)
+{
+  GtkTextBuffer *buffer = funcs_data;
+  GtkTextIter insert;
+  GtkTextIter bound;
+
+  if (selection_insert == -1 || selection_bound == -1)
+    return;
+
+  gtk_text_buffer_get_iter_at_offset (buffer, &insert, selection_insert);
+  gtk_text_buffer_get_iter_at_offset (buffer, &bound, selection_bound);
+  gtk_text_buffer_select_range (buffer, &insert, &bound);
+
+  //g_print ("Select: %d:%d\n", selection_insert, selection_bound);
+}
+
+void
+gtk_text_buffer_undo (GtkTextBuffer *buffer)
+{
+  g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+
+  g_signal_emit (buffer, signals[UNDO], 0);
+}
+
+void
+gtk_text_buffer_redo (GtkTextBuffer *buffer)
+{
+  g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+
+  g_signal_emit (buffer, signals[REDO], 0);
+}
diff --git a/gtk/gtktextbuffer.h b/gtk/gtktextbuffer.h
index 51668cbb6e..c320c52d92 100644
--- a/gtk/gtktextbuffer.h
+++ b/gtk/gtktextbuffer.h
@@ -146,6 +146,8 @@ struct _GtkTextBufferClass
 
   void (* paste_done)             (GtkTextBuffer      *buffer,
                                    GdkClipboard       *clipboard);
+  void (* undo)                   (GtkTextBuffer      *buffer);
+  void (* redo)                   (GtkTextBuffer      *buffer);
 
   /*< private >*/
 
@@ -451,6 +453,15 @@ gboolean        gtk_text_buffer_delete_selection        (GtkTextBuffer *buffer,
                                                          gboolean       interactive,
                                                          gboolean       default_editable);
 
+GDK_AVAILABLE_IN_ALL
+gboolean        gtk_text_buffer_get_can_undo            (GtkTextBuffer *buffer);
+GDK_AVAILABLE_IN_ALL
+gboolean        gtk_text_buffer_get_can_redo            (GtkTextBuffer *buffer);
+GDK_AVAILABLE_IN_ALL
+void            gtk_text_buffer_undo                    (GtkTextBuffer *buffer);
+GDK_AVAILABLE_IN_ALL
+void            gtk_text_buffer_redo                    (GtkTextBuffer *buffer);
+
 /* Called to specify atomic user actions, used to implement undo */
 GDK_AVAILABLE_IN_ALL
 void            gtk_text_buffer_begin_user_action       (GtkTextBuffer *buffer);
diff --git a/gtk/gtktexthistory.c b/gtk/gtktexthistory.c
new file mode 100644
index 0000000000..27964b5ff8
--- /dev/null
+++ b/gtk/gtktexthistory.c
@@ -0,0 +1,975 @@
+/* Copyright (C) 2019 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "gtktexthistoryprivate.h"
+#include "istring.h"
+
+/*
+ * The GtkTextHistory works in a way that allows text widgets to deliver
+ * information about changes to the underlying text at given offsets within
+ * their text. The GtkTextHistory object uses a series of callback functions
+ * (see GtkTextHistoryFuncs) to apply changes as undo/redo is performed.
+ *
+ * The GtkTextHistory object is careful to avoid tracking changes while
+ * applying specific undo/redo actions.
+ *
+ * Changes are tracked within a series of actions, contained in groups.  The
+ * group may be coalesced when gtk_text_history_end_user_action() is
+ * called.
+ *
+ * Calling gtk_text_history_begin_irreversible_action() and
+ * gtk_text_history_end_irreversible_action() can be used to denote a
+ * section of operations that cannot be undone. This will cause all previous
+ * changes tracked by the GtkTextHistory to be discared.
+ */
+
+typedef struct _Action     Action;
+typedef enum   _ActionKind ActionKind;
+
+enum _ActionKind
+{
+  ACTION_KIND_BARRIER             = 1,
+  ACTION_KIND_DELETE_BACKSPACE    = 2,
+  ACTION_KIND_DELETE_KEY          = 3,
+  ACTION_KIND_DELETE_PROGRAMMATIC = 4,
+  ACTION_KIND_DELETE_SELECTION    = 5,
+  ACTION_KIND_GROUP               = 6,
+  ACTION_KIND_INSERT              = 7,
+};
+
+struct _Action
+{
+  ActionKind kind;
+  GList link;
+  guint is_modified : 1;
+  guint is_modified_set : 1;
+  union {
+    struct {
+      IString istr;
+      guint begin;
+      guint end;
+    } insert;
+    struct {
+      IString istr;
+      guint begin;
+      guint end;
+      struct {
+        int insert;
+        int bound;
+      } selection;
+    } delete;
+    struct {
+      GQueue actions;
+      guint  depth;
+    } group;
+  } u;
+};
+
+struct _GtkTextHistory
+{
+  GObject             parent_instance;
+
+  GtkTextHistoryFuncs funcs;
+  gpointer            funcs_data;
+
+  GQueue              undo_queue;
+  GQueue              redo_queue;
+
+  struct {
+    int insert;
+    int bound;
+  } selection;
+
+  guint               irreversible;
+  guint               in_user;
+
+  guint               can_undo : 1;
+  guint               can_redo : 1;
+  guint               is_modified : 1;
+  guint               is_modified_set : 1;
+  guint               applying : 1;
+  guint               enabled : 1;
+};
+
+static void action_free (Action *action);
+
+G_DEFINE_TYPE (GtkTextHistory, gtk_text_history, G_TYPE_OBJECT)
+
+#define return_if_applying(instance)     \
+  G_STMT_START {                         \
+    if ((instance)->applying)            \
+      return;                            \
+  } G_STMT_END
+#define return_if_irreversible(instance) \
+  G_STMT_START {                         \
+    if ((instance)->irreversible)        \
+      return;                            \
+  } G_STMT_END
+#define return_if_not_enabled(instance)  \
+  G_STMT_START {                         \
+    if (!(instance)->enabled)            \
+      return;                            \
+  } G_STMT_END
+
+static inline void
+uint_order (guint *a,
+            guint *b)
+{
+  if (*a > *b)
+    {
+      guint tmp = *a;
+      *a = *b;
+      *b = tmp;
+    }
+}
+
+static void
+clear_action_queue (GQueue *queue)
+{
+  g_assert (queue != NULL);
+
+  while (queue->length > 0)
+    {
+      Action *action = g_queue_peek_head (queue);
+      g_queue_unlink (queue, &action->link);
+      action_free (action);
+    }
+}
+
+static Action *
+action_new (ActionKind kind)
+{
+  Action *action;
+
+  action = g_slice_new0 (Action);
+  action->kind = kind;
+  action->link.data = action;
+
+  return action;
+}
+
+static void
+action_free (Action *action)
+{
+  if (action->kind == ACTION_KIND_INSERT)
+    istring_clear (&action->u.insert.istr);
+  else if (action->kind == ACTION_KIND_DELETE_BACKSPACE ||
+           action->kind == ACTION_KIND_DELETE_KEY ||
+           action->kind == ACTION_KIND_DELETE_PROGRAMMATIC ||
+           action->kind == ACTION_KIND_DELETE_SELECTION)
+    istring_clear (&action->u.delete.istr);
+  else if (action->kind == ACTION_KIND_GROUP)
+    clear_action_queue (&action->u.group.actions);
+
+  g_slice_free (Action, action);
+}
+
+static gboolean
+action_group_is_empty (const Action *action)
+{
+  const GList *iter;
+
+  g_assert (action->kind == ACTION_KIND_GROUP);
+
+  for (iter = action->u.group.actions.head; iter; iter = iter->next)
+    {
+      const Action *child = iter->data;
+
+      if (child->kind == ACTION_KIND_BARRIER)
+        continue;
+
+      if (child->kind == ACTION_KIND_GROUP && action_group_is_empty (child))
+        continue;
+
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+static gboolean
+action_chain (Action   *action,
+              Action   *other,
+              gboolean  in_user_action)
+{
+  g_assert (action != NULL);
+  g_assert (other != NULL);
+
+  if (action->kind == ACTION_KIND_GROUP)
+    {
+      /* Always push new items onto a group, so that we can coalesce
+       * items when gtk_text_history_end_user_action() is called.
+       *
+       * But we don't care if this is a barrier since we will always
+       * apply things as a group anyway.
+       */
+
+      if (other->kind == ACTION_KIND_BARRIER)
+        action_free (other);
+      else
+        g_queue_push_tail_link (&action->u.group.actions, &other->link);
+
+      return TRUE;
+    }
+
+  /* The rest can only be merged to themselves */
+  if (action->kind != other->kind)
+    return FALSE;
+
+  switch (action->kind)
+    {
+    case ACTION_KIND_INSERT: {
+
+      /* Make sure the new insert is at the end of the previous */
+      if (action->u.insert.end != other->u.insert.begin)
+        return FALSE;
+
+      /* If we are not within a user action, be more selective */
+      if (!in_user_action)
+        {
+          /* Avoid pathological cases */
+          if (istring_is_too_large (&other->u.insert.istr))
+            return FALSE;
+
+          /* Chain space to items that ended in space. This is generally
+           * just at the start of a line where we could have indentation
+           * space.
+           */
+          if (istring_ends_with_space (&action->u.insert.istr) &&
+              istring_only_contains_space (&other->u.insert.istr))
+            goto do_chain;
+
+          /* Starting a new word, don't chain this */
+          if (istring_starts_with_space (&other->u.insert.istr))
+            return FALSE;
+
+          /* Check for possible paste (multi-character input) or word input that
+           * has spaces in it (and should treat as one operation).
+           */
+          if (other->u.insert.istr.n_chars > 1 &&
+              istring_contains_space (&other->u.insert.istr))
+            return FALSE;
+        }
+
+    do_chain:
+
+      istring_append (&action->u.insert.istr, &other->u.insert.istr);
+      action->u.insert.end += other->u.insert.end - other->u.insert.begin;
+      action_free (other);
+
+      return TRUE;
+    }
+
+    case ACTION_KIND_DELETE_PROGRAMMATIC:
+      /* We can't tell if this should be chained because we don't
+       * have a group to coalesce. But unless each action deletes
+       * a single character, the overhead isn't too bad as we embed
+       * the strings in the action.
+       */
+      return FALSE;
+
+    case ACTION_KIND_DELETE_SELECTION:
+      /* Don't join selection deletes as they should appear as a single
+       * operation and have selection reinstanted when performing undo.
+       */
+      return FALSE;
+
+    case ACTION_KIND_DELETE_BACKSPACE:
+      if (other->u.delete.end == action->u.delete.begin)
+        {
+          istring_prepend (&action->u.delete.istr,
+                           &other->u.delete.istr);
+          action->u.delete.begin = other->u.delete.begin;
+          action_free (other);
+          return TRUE;
+        }
+
+      return FALSE;
+
+    case ACTION_KIND_DELETE_KEY:
+      if (action->u.delete.begin == other->u.delete.begin)
+        {
+          if (!istring_contains_space (&other->u.delete.istr) ||
+              istring_only_contains_space (&action->u.delete.istr))
+            {
+              istring_append (&action->u.delete.istr, &other->u.delete.istr);
+              action->u.delete.end += other->u.delete.istr.n_chars;
+              action_free (other);
+              return TRUE;
+            }
+        }
+
+      return FALSE;
+
+    case ACTION_KIND_BARRIER:
+      /* Only allow a single barrier to be added. */
+      action_free (other);
+      return TRUE;
+
+    case ACTION_KIND_GROUP:
+    default:
+      g_return_val_if_reached (FALSE);
+    }
+}
+
+static void
+gtk_text_history_do_change_state (GtkTextHistory *self,
+                                  gboolean        is_modified,
+                                  gboolean        can_undo,
+                                  gboolean        can_redo)
+{
+  g_assert (GTK_IS_TEXT_HISTORY (self));
+
+  self->funcs.change_state (self->funcs_data, is_modified, can_undo, can_redo);
+}
+
+static void
+gtk_text_history_do_insert (GtkTextHistory *self,
+                            guint           begin,
+                            guint           end,
+                            const char     *text,
+                            guint           len)
+{
+  g_assert (GTK_IS_TEXT_HISTORY (self));
+  g_assert (text != NULL);
+
+  uint_order (&begin, &end);
+
+  self->funcs.insert (self->funcs_data, begin, end, text, len);
+}
+
+static void
+gtk_text_history_do_delete (GtkTextHistory *self,
+                            guint           begin,
+                            guint           end,
+                            const gchar    *expected_text,
+                            guint           len)
+{
+  g_assert (GTK_IS_TEXT_HISTORY (self));
+
+  uint_order (&begin, &end);
+
+  self->funcs.delete (self->funcs_data, begin, end, expected_text, len);
+}
+
+static void
+gtk_text_history_do_select (GtkTextHistory *self,
+                            guint           selection_insert,
+                            guint           selection_bound)
+{
+  g_assert (GTK_IS_TEXT_HISTORY (self));
+
+  self->funcs.select (self->funcs_data, selection_insert, selection_bound);
+}
+
+static void
+gtk_text_history_finalize (GObject *object)
+{
+  GtkTextHistory *self = (GtkTextHistory *)object;
+
+  clear_action_queue (&self->undo_queue);
+  clear_action_queue (&self->redo_queue);
+
+  G_OBJECT_CLASS (gtk_text_history_parent_class)->finalize (object);
+}
+
+static void
+gtk_text_history_class_init (GtkTextHistoryClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gtk_text_history_finalize;
+}
+
+static void
+gtk_text_history_init (GtkTextHistory *self)
+{
+  self->enabled = TRUE;
+  self->selection.insert = -1;
+  self->selection.bound = -1;
+}
+
+static gboolean
+has_actionable (const GQueue *queue)
+{
+  const GList *iter;
+
+  for (iter = queue->head; iter; iter = iter->next)
+    {
+      const Action *action = iter->data;
+
+      if (action->kind == ACTION_KIND_BARRIER)
+        continue;
+
+      if (action->kind == ACTION_KIND_GROUP)
+        {
+          if (has_actionable (&action->u.group.actions))
+            return TRUE;
+        }
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+gtk_text_history_update_state (GtkTextHistory *self)
+{
+  g_assert (GTK_IS_TEXT_HISTORY (self));
+
+  if (self->irreversible || self->in_user)
+    {
+      self->can_undo = FALSE;
+      self->can_redo = FALSE;
+    }
+  else
+    {
+      self->can_undo = has_actionable (&self->undo_queue);
+      self->can_redo = has_actionable (&self->redo_queue);
+    }
+
+  gtk_text_history_do_change_state (self, self->is_modified, self->can_undo, self->can_redo);
+}
+
+static void
+gtk_text_history_push (GtkTextHistory *self,
+                       Action         *action)
+{
+  Action *peek;
+  gboolean in_user_action;
+
+  g_assert (GTK_IS_TEXT_HISTORY (self));
+  g_assert (self->enabled);
+  g_assert (action != NULL);
+
+  while (self->redo_queue.length > 0)
+    {
+      peek = g_queue_peek_head (&self->redo_queue);
+      g_queue_unlink (&self->redo_queue, &peek->link);
+      action_free (peek);
+    }
+
+  peek = g_queue_peek_tail (&self->undo_queue);
+  in_user_action = self->in_user > 0;
+
+  if (peek == NULL || !action_chain (peek, action, in_user_action))
+    g_queue_push_tail_link (&self->undo_queue, &action->link);
+
+  gtk_text_history_update_state (self);
+}
+
+GtkTextHistory *
+gtk_text_history_new (const GtkTextHistoryFuncs *funcs,
+                      gpointer                   funcs_data)
+{
+  GtkTextHistory *self;
+
+  g_return_val_if_fail (funcs != NULL, NULL);
+
+  self = g_object_new (GTK_TYPE_TEXT_HISTORY, NULL);
+  self->funcs = *funcs;
+  self->funcs_data = funcs_data;
+
+  return g_steal_pointer (&self);
+}
+
+gboolean
+gtk_text_history_get_can_undo (GtkTextHistory *self)
+{
+  g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), FALSE);
+
+  return self->can_undo;
+}
+
+gboolean
+gtk_text_history_get_can_redo (GtkTextHistory *self)
+{
+  g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), FALSE);
+
+  return self->can_redo;
+}
+
+static void
+gtk_text_history_apply (GtkTextHistory *self,
+                        Action         *action)
+{
+  g_assert (GTK_IS_TEXT_HISTORY (self));
+  g_assert (action != NULL);
+
+  switch (action->kind)
+    {
+    case ACTION_KIND_INSERT:
+      gtk_text_history_do_insert (self,
+                                  action->u.insert.begin,
+                                  action->u.insert.end,
+                                  istring_str (&action->u.insert.istr),
+                                  action->u.insert.istr.n_bytes);
+      break;
+
+    case ACTION_KIND_DELETE_BACKSPACE:
+    case ACTION_KIND_DELETE_KEY:
+    case ACTION_KIND_DELETE_PROGRAMMATIC:
+    case ACTION_KIND_DELETE_SELECTION:
+      gtk_text_history_do_delete (self,
+                                  action->u.delete.begin,
+                                  action->u.delete.end,
+                                  istring_str (&action->u.delete.istr),
+                                  action->u.delete.istr.n_bytes);
+      break;
+
+    case ACTION_KIND_GROUP: {
+      const GList *actions = action->u.group.actions.head;
+
+      for (const GList *iter = actions; iter; iter = iter->next)
+        gtk_text_history_apply (self, iter->data);
+
+      break;
+    }
+
+    case ACTION_KIND_BARRIER:
+      break;
+
+    default:
+      g_assert_not_reached ();
+    }
+
+  if (action->is_modified_set)
+    self->is_modified = action->is_modified;
+}
+
+static void
+gtk_text_history_reverse (GtkTextHistory *self,
+                          Action         *action)
+{
+  g_assert (GTK_IS_TEXT_HISTORY (self));
+  g_assert (action != NULL);
+
+  switch (action->kind)
+    {
+    case ACTION_KIND_INSERT:
+      gtk_text_history_do_delete (self,
+                                  action->u.insert.begin,
+                                  action->u.insert.end,
+                                  istring_str (&action->u.insert.istr),
+                                  action->u.insert.istr.n_bytes);
+      break;
+
+    case ACTION_KIND_DELETE_BACKSPACE:
+    case ACTION_KIND_DELETE_KEY:
+    case ACTION_KIND_DELETE_PROGRAMMATIC:
+    case ACTION_KIND_DELETE_SELECTION:
+      gtk_text_history_do_insert (self,
+                                  action->u.delete.begin,
+                                  action->u.delete.end,
+                                  istring_str (&action->u.delete.istr),
+                                  action->u.delete.istr.n_bytes);
+      if (action->u.delete.selection.insert != -1 &&
+          action->u.delete.selection.bound != -1)
+        gtk_text_history_do_select (self,
+                                    action->u.delete.selection.insert,
+                                    action->u.delete.selection.bound);
+      else if (action->u.delete.selection.insert != -1)
+        gtk_text_history_do_select (self,
+                                    action->u.delete.selection.insert,
+                                    action->u.delete.selection.insert);
+      break;
+
+    case ACTION_KIND_GROUP: {
+      const GList *actions = action->u.group.actions.tail;
+
+      for (const GList *iter = actions; iter; iter = iter->prev)
+        gtk_text_history_reverse (self, iter->data);
+
+      break;
+    }
+
+    case ACTION_KIND_BARRIER:
+      break;
+
+    default:
+      g_assert_not_reached ();
+    }
+
+  if (action->is_modified_set)
+    self->is_modified = !action->is_modified;
+}
+
+static void
+move_barrier (GQueue   *from_queue,
+              Action   *action,
+              GQueue   *to_queue,
+              gboolean  head)
+{
+  g_queue_unlink (from_queue, &action->link);
+
+  if (head)
+    g_queue_push_head_link (to_queue, &action->link);
+  else
+    g_queue_push_tail_link (to_queue, &action->link);
+}
+
+void
+gtk_text_history_undo (GtkTextHistory *self)
+{
+  g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
+
+  return_if_not_enabled (self);
+  return_if_applying (self);
+  return_if_irreversible (self);
+
+  if (gtk_text_history_get_can_undo (self))
+    {
+      Action *action;
+
+      self->applying = TRUE;
+
+      action = g_queue_peek_tail (&self->undo_queue);
+
+      if (action->kind == ACTION_KIND_BARRIER)
+        {
+          move_barrier (&self->undo_queue, action, &self->redo_queue, TRUE);
+          action = g_queue_peek_tail (&self->undo_queue);
+        }
+
+      g_queue_unlink (&self->undo_queue, &action->link);
+      g_queue_push_head_link (&self->redo_queue, &action->link);
+      gtk_text_history_reverse (self, action);
+      gtk_text_history_update_state (self);
+
+      self->applying = FALSE;
+    }
+}
+
+void
+gtk_text_history_redo (GtkTextHistory *self)
+{
+  g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
+
+  return_if_not_enabled (self);
+  return_if_applying (self);
+  return_if_irreversible (self);
+
+  if (gtk_text_history_get_can_redo (self))
+    {
+      Action *action;
+
+      self->applying = TRUE;
+
+      action = g_queue_peek_head (&self->redo_queue);
+
+      if (action->kind == ACTION_KIND_BARRIER)
+        {
+          move_barrier (&self->redo_queue, action, &self->undo_queue, FALSE);
+          action = g_queue_peek_head (&self->redo_queue);
+        }
+
+      g_queue_unlink (&self->redo_queue, &action->link);
+      g_queue_push_tail_link (&self->undo_queue, &action->link);
+      gtk_text_history_apply (self, action);
+      gtk_text_history_update_state (self);
+
+      self->applying = FALSE;
+    }
+}
+
+void
+gtk_text_history_begin_user_action (GtkTextHistory *self)
+{
+  Action *group;
+
+  g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
+
+  return_if_not_enabled (self);
+  return_if_applying (self);
+  return_if_irreversible (self);
+
+  self->in_user++;
+
+  group = g_queue_peek_tail (&self->undo_queue);
+
+  if (group == NULL || group->kind != ACTION_KIND_GROUP)
+    {
+      group = action_new (ACTION_KIND_GROUP);
+      gtk_text_history_push (self, group);
+    }
+
+  group->u.group.depth++;
+
+  gtk_text_history_update_state (self);
+}
+
+void
+gtk_text_history_end_user_action (GtkTextHistory *self)
+{
+  Action *peek;
+
+  g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
+
+  return_if_not_enabled (self);
+  return_if_applying (self);
+  return_if_irreversible (self);
+
+  clear_action_queue (&self->redo_queue);
+
+  peek = g_queue_peek_tail (&self->undo_queue);
+
+  if (peek->kind != ACTION_KIND_GROUP)
+    {
+      g_warning ("miss-matched %s end_user_action. Expected group, got %d",
+                 G_OBJECT_TYPE_NAME (self),
+                 peek->kind);
+      return;
+    }
+
+  self->in_user--;
+  peek->u.group.depth--;
+
+  /* Unless this is the last user action, short-circuit */
+  if (peek->u.group.depth > 0)
+    return;
+
+  /* Unlikely, but if the group is empty, just remove it */
+  if (action_group_is_empty (peek))
+    {
+      g_queue_unlink (&self->undo_queue, &peek->link);
+      action_free (peek);
+      goto update_state;
+    }
+
+  /* Now insert a barrier action so we don't allow
+   * joining items to this node in the future.
+   */
+  gtk_text_history_push (self, action_new (ACTION_KIND_BARRIER));
+
+update_state:
+  gtk_text_history_update_state (self);
+}
+
+void
+gtk_text_history_begin_irreversible_action (GtkTextHistory *self)
+{
+  g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
+
+  return_if_not_enabled (self);
+  return_if_applying (self);
+
+  if (self->in_user)
+    {
+      g_warning ("Cannot begin irreversible action while in user action");
+      return;
+    }
+
+  self->irreversible++;
+
+  clear_action_queue (&self->undo_queue);
+  clear_action_queue (&self->redo_queue);
+
+  gtk_text_history_update_state (self);
+}
+
+void
+gtk_text_history_end_irreversible_action (GtkTextHistory *self)
+{
+  g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
+
+  return_if_not_enabled (self);
+  return_if_applying (self);
+
+  if (self->in_user)
+    {
+      g_warning ("Cannot end irreversible action while in user action");
+      return;
+    }
+
+  self->irreversible--;
+
+  clear_action_queue (&self->undo_queue);
+  clear_action_queue (&self->redo_queue);
+
+  gtk_text_history_update_state (self);
+}
+
+static void
+gtk_text_history_clear_modified (GtkTextHistory *self)
+{
+  const GList *iter;
+
+  for (iter = self->undo_queue.head; iter; iter = iter->next)
+    {
+      Action *action = iter->data;
+
+      action->is_modified = FALSE;
+      action->is_modified_set = FALSE;
+    }
+
+  for (iter = self->redo_queue.head; iter; iter = iter->next)
+    {
+      Action *action = iter->data;
+
+      action->is_modified = FALSE;
+      action->is_modified_set = FALSE;
+    }
+}
+
+void
+gtk_text_history_modified_changed (GtkTextHistory *self,
+                                   gboolean        modified)
+{
+  Action *peek;
+
+  g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
+
+  return_if_not_enabled (self);
+  return_if_applying (self);
+  return_if_irreversible (self);
+
+  /* If we have a new save point, clear all previous modified states. */
+  gtk_text_history_clear_modified (self);
+
+  if ((peek = g_queue_peek_tail (&self->undo_queue)))
+    {
+      if (peek->kind == ACTION_KIND_BARRIER)
+        {
+          if (!(peek = peek->link.prev->data))
+            return;
+        }
+
+      peek->is_modified = !!modified;
+      peek->is_modified_set = TRUE;
+    }
+
+  self->is_modified = !!modified;
+  self->is_modified_set = TRUE;
+
+  gtk_text_history_update_state (self);
+}
+
+void
+gtk_text_history_selection_changed (GtkTextHistory *self,
+                                    int             selection_insert,
+                                    int             selection_bound)
+{
+  g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
+
+  return_if_not_enabled (self);
+  return_if_applying (self);
+  return_if_irreversible (self);
+
+  if (self->in_user == 0 && self->irreversible == 0)
+    {
+      self->selection.insert = CLAMP (selection_insert, -1, G_MAXINT);
+      self->selection.bound = CLAMP (selection_bound, -1, G_MAXINT);
+    }
+}
+
+void
+gtk_text_history_text_inserted (GtkTextHistory *self,
+                                guint           position,
+                                const char     *text,
+                                int             len)
+{
+  Action *action;
+
+  g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
+
+  return_if_not_enabled (self);
+  return_if_applying (self);
+  return_if_irreversible (self);
+
+  if (len < 0)
+    len = strlen (text);
+
+  action = action_new (ACTION_KIND_INSERT);
+  action->u.insert.begin = position;
+  action->u.insert.end = position + g_utf8_strlen (text, len);
+  istring_set (&action->u.insert.istr,
+               text,
+               len,
+               action->u.insert.end);
+
+  gtk_text_history_push (self, action);
+}
+
+void
+gtk_text_history_text_deleted (GtkTextHistory *self,
+                               guint           begin,
+                               guint           end,
+                               const char     *text,
+                               int             len)
+{
+  Action *action;
+  ActionKind kind;
+
+  g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
+
+  return_if_not_enabled (self);
+  return_if_applying (self);
+  return_if_irreversible (self);
+
+  if (len < 0)
+    len = strlen (text);
+
+  if (self->selection.insert == -1 && self->selection.bound == -1)
+    kind = ACTION_KIND_DELETE_PROGRAMMATIC;
+  else if (self->selection.insert == end && self->selection.bound == -1)
+    kind = ACTION_KIND_DELETE_BACKSPACE;
+  else if (self->selection.insert == begin && self->selection.bound == -1)
+    kind = ACTION_KIND_DELETE_KEY;
+  else
+    kind = ACTION_KIND_DELETE_SELECTION;
+
+  action = action_new (kind);
+  action->u.delete.begin = begin;
+  action->u.delete.end = end;
+  action->u.delete.selection.insert = self->selection.insert;
+  action->u.delete.selection.bound = self->selection.bound;
+  istring_set (&action->u.delete.istr, text, len, ABS (end - begin));
+
+  gtk_text_history_push (self, action);
+}
+
+gboolean
+gtk_text_history_get_enabled (GtkTextHistory *self)
+{
+  g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), FALSE);
+
+  return self->enabled;
+}
+
+void
+gtk_text_history_set_enabled (GtkTextHistory *self,
+                              gboolean        enabled)
+{
+  g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
+
+  enabled = !!enabled;
+
+  if (self->enabled != enabled)
+    {
+      self->enabled = enabled;
+
+      if (!self->enabled)
+        {
+          self->irreversible = 0;
+          self->in_user = 0;
+          clear_action_queue (&self->undo_queue);
+          clear_action_queue (&self->redo_queue);
+        }
+    }
+}
diff --git a/gtk/gtktexthistoryprivate.h b/gtk/gtktexthistoryprivate.h
new file mode 100644
index 0000000000..5e7c986397
--- /dev/null
+++ b/gtk/gtktexthistoryprivate.h
@@ -0,0 +1,81 @@
+/* Copyright (C) 2019 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __GTK_TEXT_HISTORY_PRIVATE_H__
+#define __GTK_TEXT_HISTORY_PRIVATE_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_TEXT_HISTORY (gtk_text_history_get_type())
+
+typedef struct _GtkTextHistoryFuncs GtkTextHistoryFuncs;
+
+G_DECLARE_FINAL_TYPE (GtkTextHistory, gtk_text_history, GTK, TEXT_HISTORY, GObject)
+
+struct _GtkTextHistoryFuncs
+{
+  void (*change_state) (gpointer     funcs_data,
+                        gboolean     is_modified,
+                        gboolean     can_undo,
+                        gboolean     can_redo);
+  void (*insert)       (gpointer     funcs_data,
+                        guint        begin,
+                        guint        end,
+                        const char  *text,
+                        guint        len);
+  void (*delete)       (gpointer     funcs_data,
+                        guint        begin,
+                        guint        end,
+                        const gchar *expected_text,
+                        guint        len);
+  void (*select)       (gpointer     funcs_data,
+                        int          selection_insert,
+                        int          selection_bound);
+};
+
+GtkTextHistory *gtk_text_history_new                       (const GtkTextHistoryFuncs *funcs,
+                                                            gpointer                   funcs_data);
+void            gtk_text_history_begin_user_action         (GtkTextHistory            *self);
+void            gtk_text_history_end_user_action           (GtkTextHistory            *self);
+void            gtk_text_history_begin_irreversible_action (GtkTextHistory            *self);
+void            gtk_text_history_end_irreversible_action   (GtkTextHistory            *self);
+gboolean        gtk_text_history_get_can_undo              (GtkTextHistory            *self);
+gboolean        gtk_text_history_get_can_redo              (GtkTextHistory            *self);
+void            gtk_text_history_undo                      (GtkTextHistory            *self);
+void            gtk_text_history_redo                      (GtkTextHistory            *self);
+void            gtk_text_history_modified_changed          (GtkTextHistory            *self,
+                                                            gboolean                   modified);
+void            gtk_text_history_selection_changed         (GtkTextHistory            *self,
+                                                            int                        selection_insert,
+                                                            int                        selection_bound);
+void            gtk_text_history_text_inserted             (GtkTextHistory            *self,
+                                                            guint                      position,
+                                                            const char                *text,
+                                                            int                        len);
+void            gtk_text_history_text_deleted              (GtkTextHistory            *self,
+                                                            guint                      begin,
+                                                            guint                      end,
+                                                            const char                *text,
+                                                            int                        len);
+gboolean        gtk_text_history_get_enabled               (GtkTextHistory            *self);
+void            gtk_text_history_set_enabled               (GtkTextHistory            *self,
+                                                            gboolean                   enabled);
+
+G_END_DECLS
+
+#endif /* __GTK_TEXT_HISTORY_PRIVATE_H__ */
diff --git a/gtk/gtktextprivate.h b/gtk/gtktextprivate.h
index daeed71bce..500389824d 100644
--- a/gtk/gtktextprivate.h
+++ b/gtk/gtktextprivate.h
@@ -85,6 +85,8 @@ struct _GtkTextClass
   void (* paste_clipboard)    (GtkText         *self);
   void (* toggle_overwrite)   (GtkText         *self);
   void (* insert_emoji)       (GtkText         *self);
+  void (* undo)               (GtkText         *self);
+  void (* redo)               (GtkText         *self);
 };
 
 char *              gtk_text_get_display_text   (GtkText    *entry,
diff --git a/gtk/gtktextview.c b/gtk/gtktextview.c
index c82fcf792e..f489356d4b 100644
--- a/gtk/gtktextview.c
+++ b/gtk/gtktextview.c
@@ -314,6 +314,8 @@ enum
   PREEDIT_CHANGED,
   EXTEND_SELECTION,
   INSERT_EMOJI,
+  UNDO,
+  REDO,
   LAST_SIGNAL
 };
 
@@ -619,6 +621,9 @@ static void gtk_text_view_activate_misc_insert_emoji    (GtkWidget  *widget,
                                                          const char *action_name,
                                                          GVariant   *parameter);
 
+static void gtk_text_view_real_undo (GtkTextView *text_view);
+static void gtk_text_view_real_redo (GtkTextView *text_view);
+
 
 /* FIXME probably need the focus methods. */
 
@@ -734,6 +739,8 @@ gtk_text_view_class_init (GtkTextViewClass *klass)
   klass->create_buffer = gtk_text_view_create_buffer;
   klass->extend_selection = gtk_text_view_extend_selection;
   klass->insert_emoji = gtk_text_view_insert_emoji;
+  klass->undo = gtk_text_view_real_undo;
+  klass->redo = gtk_text_view_real_redo;
 
   /*
    * Properties
@@ -1358,6 +1365,40 @@ gtk_text_view_class_init (GtkTextViewClass *klass)
                   NULL,
                   G_TYPE_NONE, 0);
 
+  /**
+   * GtkTextView::undo:
+   * @text_view: the object which received the signal
+   *
+   * The ::undo signal is a
+   * [keybinding signal][GtkBindingSignal]
+   * which gets emitted to undo the last operation in the @text_view.
+   *
+   * The default binding for this signal is Ctrl-z.
+   */
+  signals[UNDO] =
+    g_signal_new (I_("undo"),
+                  G_OBJECT_CLASS_TYPE (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (GtkTextViewClass, undo),
+                  NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  /**
+   * GtkTextView::redo:
+   * @text_view: the object which received the signal
+   *
+   * The ::redo signal is a
+   * [keybinding signal][GtkBindingSignal]
+   * which gets emitted to redo the last undone operation in the @text_view.
+   *
+   * The default binding for this signal is Ctrl-Shift-z.
+   */
+  signals[REDO] =
+    g_signal_new (I_("redo"),
+                  G_OBJECT_CLASS_TYPE (gobject_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (GtkTextViewClass, redo),
+                  NULL, NULL, NULL, G_TYPE_NONE, 0);
+
   /*
    * Key bindings
    */
@@ -1550,6 +1591,14 @@ gtk_text_view_class_init (GtkTextViewClass *klass)
   gtk_binding_entry_add_signal (binding_set, GDK_KEY_Insert, GDK_SHIFT_MASK,
                                 "paste-clipboard", 0);
 
+  /* Undo/Redo */
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_z,
+                                GDK_CONTROL_MASK,
+                                "undo", 0);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_z,
+                                GDK_CONTROL_MASK | GDK_SHIFT_MASK,
+                                "redo", 0);
+
   /* Overwrite */
   gtk_binding_entry_add_signal (binding_set, GDK_KEY_Insert, 0,
                                "toggle-overwrite", 0);
@@ -9835,3 +9884,17 @@ gtk_text_view_get_extra_menu (GtkTextView *text_view)
 
   return priv->extra_menu;
 }
+
+static void
+gtk_text_view_real_undo (GtkTextView *text_view)
+{
+  if (text_view->priv->buffer)
+    gtk_text_buffer_undo (text_view->priv->buffer);
+}
+
+static void
+gtk_text_view_real_redo (GtkTextView *text_view)
+{
+  if (text_view->priv->buffer)
+    gtk_text_buffer_redo (text_view->priv->buffer);
+}
diff --git a/gtk/gtktextview.h b/gtk/gtktextview.h
index d386aebf70..91a156ddc4 100644
--- a/gtk/gtktextview.h
+++ b/gtk/gtktextview.h
@@ -179,6 +179,8 @@ struct _GtkTextViewClass
                                   GtkTextIter            *start,
                                   GtkTextIter            *end);
   void (* insert_emoji)          (GtkTextView      *text_view);
+  void (* undo)                  (GtkTextView      *text_view);
+  void (* redo)                  (GtkTextView      *text_view);
 
   /*< private >*/
 
diff --git a/gtk/istring.h b/gtk/istring.h
new file mode 100644
index 0000000000..66813e0101
--- /dev/null
+++ b/gtk/istring.h
@@ -0,0 +1,168 @@
+#ifndef __ISTRING_H__
+#define __ISTRING_H__
+
+#include <glib.h>
+#include <string.h>
+
+typedef struct
+{
+  guint n_bytes;
+  guint n_chars;
+  union {
+    char  buf[24];
+    char *str;
+  } u;
+} IString;
+
+static inline gboolean
+istring_is_inline (const IString *str)
+{
+  return str->n_bytes <= (sizeof str->u.buf - 1);
+}
+
+static inline char *
+istring_str (IString *str)
+{
+  if (istring_is_inline (str))
+    return str->u.buf;
+  else
+    return str->u.str;
+}
+
+static inline void
+istring_clear (IString *str)
+{
+  if (istring_is_inline (str))
+    str->u.buf[0] = 0;
+  else
+    g_clear_pointer (&str->u.str, g_free);
+
+  str->n_bytes = 0;
+  str->n_chars = 0;
+}
+
+static inline void
+istring_set (IString    *str,
+             const char *text,
+             guint       n_bytes,
+             guint       n_chars)
+{
+  if G_LIKELY (n_bytes <= (sizeof str->u.buf - 1))
+    {
+      memcpy (str->u.buf, text, n_bytes);
+      str->u.buf[n_bytes] = 0;
+    }
+  else
+    {
+      str->u.str = g_strndup (text, n_bytes);
+    }
+
+  str->n_bytes = n_bytes;
+  str->n_chars = n_chars;
+}
+
+static inline gboolean
+istring_ends_with_space (IString *str)
+{
+  if (str->n_bytes == 0)
+    return TRUE;
+  else
+    return g_ascii_isspace (istring_str (str)[str->n_bytes - 1]);
+}
+
+static inline gboolean
+istring_starts_with_space (IString *str)
+{
+  return g_unichar_isspace (g_utf8_get_char (istring_str (str)));
+}
+
+static inline gboolean
+istring_only_contains_space (IString *str)
+{
+  const char *iter;
+
+  for (iter = istring_str (str); *iter; iter = g_utf8_next_char (iter))
+    {
+      if (!g_unichar_isspace (g_utf8_get_char (iter)))
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+static inline gboolean
+istring_contains_space (IString *str)
+{
+  const char *iter;
+
+  for (iter = istring_str (str); *iter; iter = g_utf8_next_char (iter))
+    {
+      if (g_unichar_isspace (g_utf8_get_char (iter)))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static inline gboolean
+istring_is_too_large (const IString *str)
+{
+  return str->n_chars > 1000;
+}
+
+static inline void
+istring_prepend (IString *str,
+                 IString *other)
+{
+  if G_LIKELY (str->n_bytes + other->n_bytes < sizeof str->u.buf - 1)
+    {
+      memmove (str->u.buf + other->n_bytes, str->u.buf, str->n_bytes);
+      memcpy (str->u.buf, other->u.buf, other->n_bytes);
+      str->n_bytes += other->n_bytes;
+      str->n_chars += other->n_chars;
+      str->u.buf[str->n_bytes] = 0;
+    }
+  else
+    {
+      gchar *old = NULL;
+
+      if (!istring_is_inline (str))
+        old = str->u.str;
+
+      str->u.str = g_strconcat (istring_str (str), istring_str (other), NULL);
+      str->n_bytes += other->n_bytes;
+      str->n_chars += other->n_chars;
+
+      g_free (old);
+    }
+}
+
+static inline void
+istring_append (IString *str,
+                IString *other)
+{
+  const gchar *text = istring_str (other);
+  guint n_bytes = other->n_bytes;
+  guint n_chars = other->n_chars;
+
+  if G_LIKELY (istring_is_inline (str))
+    {
+      if G_LIKELY (str->n_bytes + n_bytes <= (sizeof str->u.buf - 1))
+        memcpy (str->u.buf + str->n_bytes, text, n_bytes);
+      else
+        str->u.str = g_strconcat (str->u.buf, text, NULL);
+    }
+  else
+    {
+      str->u.str = g_realloc (str->u.str, str->n_bytes + n_bytes + 1);
+      memcpy (str->u.str + str->n_bytes, text, n_bytes);
+    }
+
+  str->n_bytes += n_bytes;
+  str->n_chars += n_chars;
+
+  istring_str (str)[str->n_bytes] = 0;
+}
+
+
+#endif /* __ISTRING_H__ */
diff --git a/gtk/meson.build b/gtk/meson.build
index 3119e05934..65576f92c6 100644
--- a/gtk/meson.build
+++ b/gtk/meson.build
@@ -145,6 +145,7 @@ gtk_private_sources = files([
   'gtkstylecascade.c',
   'gtkstyleproperty.c',
   'gtktextbtree.c',
+  'gtktexthistory.c',
   'gtktextviewchild.c',
   'gtktrashmonitor.c',
   'gtktreedatalist.c',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]