← Back to team overview

xpad-hackers team mailing list archive

undo for xpad

 

Hi Michael,

I've added undo/redo for xpad. I am attaching the diff (to apply it use
'patch -p1 -i /patch/to/xpad_undo.diff' in xpad/src directory).

Is there a chance for this code to appear in xpad? There is even a bug for
this: https://bugs.launchpad.net/xpad/+bug/370919. Well, actually I
submitted it in the first place.

PS I've also found out that neither xpad-pad.c dispose/finalize (nor
actually any xpad_pad_dispose/xpad_pad_finalize or even xpad_pad_quit()) are
called after pressing Quit from the context menu. Something seems to be
broken with the cleanup but this has nothing to do with undo/redo, I'm just
mentioning it since now I have no chance of running dispose/finalize in my
code, anyway there is a comment for that in the diff and I will take a look
at this issue later.

PPS I also planned to add search functionality to xpad a bit later (also a
bug, also submitted by me: https://bugs.launchpad.net/xpad/+bug/452226)

Anyway I am attaching the patch for undo, maybe this can appear in trunk and
then in stable version later? Yes I know that all this stuff is in say gnote
but I am kind of used to xpad  and lack of undo/redo and searching confuses
me.

Thanks!
diff -Ndru o/xpad-pad.c p/xpad-pad.c
--- o/xpad-pad.c	2010-03-28 08:43:51.000000000 +0300
+++ p/xpad-pad.c	2010-03-31 02:47:15.000000000 +0300
@@ -163,13 +163,16 @@
 		
 		pad = GTK_WIDGET (g_object_new (XPAD_TYPE_PAD, "group", group, NULL));
 		buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (XPAD_PAD (pad)->priv->textview));
-		
+
+		xpad_text_buffer_freeze_undo (XPAD_TEXT_BUFFER (buffer));
 		g_signal_handlers_block_by_func (buffer, xpad_pad_text_changed, pad);
 		
 		xpad_text_buffer_set_text_with_tags (XPAD_TEXT_BUFFER (buffer), content ? content : "");
 		g_free (content);
 		
 		g_signal_handlers_unblock_by_func (buffer, xpad_pad_text_changed, pad);
+		xpad_text_buffer_thaw_undo (XPAD_TEXT_BUFFER (buffer));
+
 		xpad_pad_text_changed(XPAD_PAD(pad), buffer);
 	}
 	
@@ -1062,12 +1065,15 @@
 	
 	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (pad->priv->textview));
 	
+	xpad_text_buffer_freeze_undo (XPAD_TEXT_BUFFER (buffer));
 	g_signal_handlers_block_by_func (buffer, xpad_pad_text_changed, pad);
 	
 	xpad_text_buffer_set_text_with_tags (XPAD_TEXT_BUFFER (buffer), content ? content : "");
 	g_free (content);
 	
 	g_signal_handlers_unblock_by_func (buffer, xpad_pad_text_changed, pad);
+	xpad_text_buffer_thaw_undo (XPAD_TEXT_BUFFER (buffer));
+
 	xpad_pad_text_changed(pad, buffer);
 }
 
@@ -1305,6 +1311,26 @@
 }
 
 static void
+menu_undo (XpadPad *pad)
+{
+	g_return_if_fail (pad->priv->textview);
+	XpadTextBuffer *buffer = NULL;
+	buffer = XPAD_TEXT_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (pad->priv->textview)));
+	g_return_if_fail (buffer);
+	xpad_text_buffer_undo (buffer);
+}
+
+static void
+menu_redo (XpadPad *pad)
+{
+	g_return_if_fail (pad->priv->textview);
+	XpadTextBuffer *buffer = NULL;
+	buffer = XPAD_TEXT_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (pad->priv->textview)));
+	g_return_if_fail (buffer);
+	xpad_text_buffer_redo (buffer);
+}
+
+static void
 menu_show_all (XpadPad *pad)
 {
 	GSList *pads, *i;
@@ -1476,6 +1502,15 @@
 	gtk_widget_show (item);\
 	}
 
+#define MENU_ADD_STOCK_WITH_ACCEL(stock, callback, key, mask) {\
+	item = gtk_image_menu_item_new_from_stock (stock, accel_group);\
+	g_signal_connect_swapped (item, "activate", G_CALLBACK (callback), pad);\
+	if (key)\
+		gtk_widget_add_accelerator(item, "activate", accel_group, key, mask, GTK_ACCEL_VISIBLE);\
+	gtk_container_add (GTK_CONTAINER (menu), item);\
+	gtk_widget_show (item);\
+	}
+
 #define MENU_ADD_CHECK(mnemonic, active, callback) {\
 	item = gtk_check_menu_item_new_with_mnemonic (mnemonic);\
 	gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), active);\
@@ -1521,10 +1556,20 @@
 	
 	menu = gtk_menu_new ();
 	gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), menu);
+
+	MENU_ADD_STOCK_WITH_ACCEL (GTK_STOCK_UNDO, menu_undo, GDK_Z, GDK_CONTROL_MASK);
+	g_object_set_data (G_OBJECT (uppermenu), "undo", item);
+    
+	MENU_ADD_STOCK_WITH_ACCEL (GTK_STOCK_REDO, menu_redo, GDK_R, GDK_CONTROL_MASK);
+	g_object_set_data (G_OBJECT (uppermenu), "redo", item);
+
+	MENU_ADD_SEP();
 	
 	MENU_ADD_STOCK (GTK_STOCK_PASTE, menu_paste);
 	g_object_set_data (G_OBJECT (uppermenu), "paste", item);
+
 	MENU_ADD_SEP ();
+
 	MENU_ADD_STOCK (GTK_STOCK_PREFERENCES, xpad_pad_open_preferences);
 	
 	
diff -Ndru o/xpad-text-buffer.c p/xpad-text-buffer.c
--- o/xpad-text-buffer.c	2008-09-28 04:44:10.000000000 +0300
+++ p/xpad-text-buffer.c	2010-03-31 02:44:02.000000000 +0300
@@ -17,8 +17,16 @@
  */
 
 #include "xpad-text-buffer.h"
+#include "xpad-undo.h"
 
 G_DEFINE_TYPE(XpadTextBuffer, xpad_text_buffer, GTK_TYPE_TEXT_BUFFER)
+#define XPAD_TEXT_BUFFER_GET_PRIVATE(object) (G_TYPE_INSTANCE_GET_PRIVATE ((object), XPAD_TYPE_TEXT_BUFFER, XpadTextBufferPrivate))
+
+struct XpadTextBufferPrivate 
+{
+	/* undo */
+	XpadUndo *undo;
+};
 
 /* Unicode chars in the Private Use Area. */
 static gunichar TAG_CHAR = 0xe000;
@@ -37,13 +45,31 @@
 }
 
 static void
+xpad_text_buffer_finalize (GObject *object)
+{
+	XpadTextBuffer *text_buffer = XPAD_TEXT_BUFFER (object);
+	
+	g_free (text_buffer->priv->undo);
+	
+	G_OBJECT_CLASS (xpad_text_buffer_parent_class)->finalize (object);
+}
+
+static void
 xpad_text_buffer_class_init (XpadTextBufferClass *klass)
 {
+	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+	
+	gobject_class->finalize = xpad_text_buffer_finalize;
+	
+	g_type_class_add_private (gobject_class, sizeof (XpadTextBufferPrivate));
 }
 
 static void
 xpad_text_buffer_init (XpadTextBuffer *buffer)
 {
+	buffer->priv = XPAD_TEXT_BUFFER_GET_PRIVATE (buffer);
+
+	buffer->priv->undo = xpad_undo_new (buffer);
 }
 
 void
@@ -164,6 +190,33 @@
 	return text;
 }
 
+void
+xpad_text_buffer_insert_text (XpadTextBuffer *buffer, gint pos, const gchar *text, gint len)
+{
+    GtkTextBuffer *parent = (GtkTextBuffer*) buffer;
+	GtkTextIter iter;
+	gtk_text_buffer_get_iter_at_offset (parent, &iter, pos);
+	gtk_text_buffer_insert (parent, &iter, text, len);
+}
+
+void
+xpad_text_buffer_delete_range (XpadTextBuffer *buffer, gint start, gint end)
+{
+    GtkTextBuffer *parent = (GtkTextBuffer*) buffer;
+
+	GtkTextIter start_iter;
+	GtkTextIter end_iter;
+
+	gtk_text_buffer_get_iter_at_offset (parent, &start_iter, start);
+
+	if (end < 0)
+		gtk_text_buffer_get_end_iter (parent, &end_iter);
+	else
+		gtk_text_buffer_get_iter_at_offset (parent, &end_iter, end);
+
+	gtk_text_buffer_delete (parent, &start_iter, &end_iter);
+}
+
 static GtkTextTagTable *
 create_tag_table (void)
 {
@@ -218,3 +271,26 @@
 	
 	return table;
 }
+
+void
+xpad_text_buffer_undo (XpadTextBuffer *buffer)
+{
+	xpad_undo_exec_undo (buffer->priv->undo);
+}
+
+void
+xpad_text_buffer_redo (XpadTextBuffer *buffer)
+{
+	xpad_undo_exec_redo (buffer->priv->undo);
+}
+
+void xpad_text_buffer_freeze_undo (XpadTextBuffer *buffer)
+{
+	xpad_undo_freeze (buffer->priv->undo);
+}
+
+void xpad_text_buffer_thaw_undo (XpadTextBuffer *buffer)
+{
+	xpad_undo_thaw (buffer->priv->undo);
+}
+
diff -Ndru o/xpad-text-buffer.h p/xpad-text-buffer.h
--- o/xpad-text-buffer.h	2008-09-28 04:44:10.000000000 +0300
+++ p/xpad-text-buffer.h	2010-03-31 02:43:22.000000000 +0300
@@ -31,11 +31,15 @@
 #define XPAD_TEXT_BUFFER_GET_CLASS(o)  (G_TYPE_INSTANCE_GET_CLASS ((o), XPAD_TYPE_TEXT_BUFFER, XpadTextBufferClass))
 
 typedef struct XpadTextBufferClass XpadTextBufferClass;
+typedef struct XpadTextBufferPrivate XpadTextBufferPrivate;
 typedef struct XpadTextBuffer XpadTextBuffer;
 
 struct XpadTextBuffer
 {
 	GtkTextBuffer parent;
+
+	/* private */
+	XpadTextBufferPrivate *priv;
 };
 
 struct XpadTextBufferClass
@@ -50,6 +54,14 @@
 void xpad_text_buffer_set_text_with_tags (XpadTextBuffer *buffer, const gchar *text);
 gchar *xpad_text_buffer_get_text_with_tags (XpadTextBuffer *buffer);
 
+void xpad_text_buffer_insert_text (XpadTextBuffer *buffer, gint pos, const gchar *text, gint len);
+void xpad_text_buffer_delete_range (XpadTextBuffer *buffer, gint start, gint end);
+
+void xpad_text_buffer_undo (XpadTextBuffer *buffer);
+void xpad_text_buffer_redo (XpadTextBuffer *buffer);
+void xpad_text_buffer_freeze_undo (XpadTextBuffer *buffer);
+void xpad_text_buffer_thaw_undo (XpadTextBuffer *buffer);
+
 G_END_DECLS
 
 #endif /* __XPAD_TEXT_BUFFER_H__ */
diff -Ndru o/xpad-undo.c p/xpad-undo.c
--- o/xpad-undo.c	1970-01-01 03:00:00.000000000 +0300
+++ p/xpad-undo.c	2010-03-31 02:49:00.000000000 +0300
@@ -0,0 +1,452 @@
+/*
+
+Copyright (c) 2001-2007 Michael Terry
+Copyright (c) 2010 Sergei Riaguzov
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3 of the License, or
+(at your option) any later version.
+
+This program 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+*/
+
+
+#include "../config.h"
+#include <stdlib.h>
+#include <glib.h>
+#include <glib/glist.h>
+#include "xpad-undo.h"
+#include "xpad-text-buffer.h"
+
+G_DEFINE_TYPE(XpadUndo, xpad_undo, G_TYPE_OBJECT)
+#define XPAD_UNDO_GET_PRIVATE(object) (G_TYPE_INSTANCE_GET_PRIVATE ((object), XPAD_TYPE_UNDO, XpadUndoPrivate))
+
+static GObject* xpad_undo_constructor(GType gtype, guint n_properties, GObjectConstructParam *properties);
+static void xpad_undo_dispose (GObject *object);
+static void xpad_undo_finalize (GObject *object);
+static void xpad_undo_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);
+static void xpad_undo_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec);
+
+enum UserActionType
+{
+	USER_ACTION_INSERT_TEXT,
+	USER_ACTION_DELETE_RANGE
+};
+
+typedef struct
+{
+	enum UserActionType action_type;
+	gint start;
+	gint end;
+	gchar *text;
+	gint len_in_bytes;
+	gint n_utf8_chars;
+	gboolean merged;
+} UserAction;
+
+static GList* xpad_undo_remove_action_elem (GList *curr);
+static void xpad_undo_clear_redo_history (XpadUndo *undo);
+static void xpad_undo_clear_history (XpadUndo *undo);
+static void xpad_undo_begin_user_action (GtkTextBuffer *buffer, XpadUndo *undo);
+static void xpad_undo_end_user_action (GtkTextBuffer *buffer, XpadUndo *undo);
+static void xpad_undo_insert_text (GtkTextBuffer *buffer, GtkTextIter *location, gchar *text, gint len, XpadUndo *undo);
+static void xpad_undo_delete_range (GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end, XpadUndo *undo);
+
+struct XpadUndoPrivate
+{
+	XpadTextBuffer *buffer;
+	// We always redo the next element in the list
+	// but undo the current one. We insert this guard
+	// with NULL data in the beginning to ease
+	// coding all of this
+	GList *history_start;
+	GList *history_curr;
+	guint user_action;
+	gboolean frozen;
+};
+
+enum
+{
+	PROP_0,
+	PROP_BUFFER,
+	LAST_PROP
+};
+
+XpadUndo*
+xpad_undo_new (XpadTextBuffer *buffer)
+{
+	return XPAD_UNDO (g_object_new (XPAD_TYPE_UNDO, "buffer", buffer, NULL));
+}
+
+static void
+xpad_undo_class_init (XpadUndoClass *klass)
+{
+	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+	// TODO: neither dispose nor finalize
+	// neither in here nor in xpad-pad.c are
+	// actually called, have to fix cleaning up
+	gobject_class->dispose = xpad_undo_dispose;
+	gobject_class->finalize = xpad_undo_finalize;
+	gobject_class->set_property = xpad_undo_set_property;
+	gobject_class->get_property = xpad_undo_get_property;
+	gobject_class->constructor = xpad_undo_constructor;
+
+	g_object_class_install_property (gobject_class,
+					 PROP_BUFFER,
+					 g_param_spec_pointer ("buffer",
+								   "Pad Buffer",
+								   "Pad buffer connected to this undo",
+								   G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+	g_type_class_add_private (gobject_class, sizeof (XpadUndoPrivate));
+}
+
+
+static void
+xpad_undo_init (XpadUndo *undo)
+{
+	undo->priv = XPAD_UNDO_GET_PRIVATE (undo);
+
+	undo->priv->buffer = NULL;
+	undo->priv->history_start = g_list_append (NULL, NULL);
+	undo->priv->history_curr = undo->priv->history_start;
+	undo->priv->user_action = 0;
+	undo->priv->frozen = FALSE;
+}
+
+static GObject*
+xpad_undo_constructor(GType gtype, guint n_properties, GObjectConstructParam *properties)
+{
+	GObjectClass *parent_class = G_OBJECT_CLASS (xpad_undo_parent_class);
+	GObject *obj = parent_class->constructor (gtype, n_properties, properties);
+
+	XpadUndo *undo = XPAD_UNDO (obj);
+
+	// Assert user passed buffer as construct parameter
+	gint i;
+	gboolean found_buffer = FALSE;
+	for (i = 0; i < n_properties; i++)
+	{
+		if (!g_strcmp0 (properties[i].pspec->name, "buffer") &&
+			G_VALUE_HOLDS_POINTER (properties[i].value) &&
+			G_IS_OBJECT (g_value_get_pointer (properties[i].value)))
+		{
+			found_buffer = TRUE;
+		}
+	}
+
+	if (!found_buffer)
+	{
+		undo->priv->buffer = NULL;
+		g_warning("GtkTextBuffer is not passed to XpadUndo constructor, undo will not work!\n");
+	}
+	else
+	{
+		// Set up signals
+	   g_signal_connect (G_OBJECT (undo->priv->buffer), "insert-text", G_CALLBACK (xpad_undo_insert_text), undo);
+	   g_signal_connect (G_OBJECT (undo->priv->buffer), "delete-range", G_CALLBACK (xpad_undo_delete_range), undo);
+	   g_signal_connect (G_OBJECT (undo->priv->buffer), "begin-user-action", G_CALLBACK (xpad_undo_begin_user_action), undo);
+	   g_signal_connect (G_OBJECT (undo->priv->buffer), "end-user-action", G_CALLBACK (xpad_undo_end_user_action), undo);
+	}
+
+	return obj;
+}
+
+static void
+xpad_undo_dispose (GObject *object)
+{
+	XpadUndo *undo = XPAD_UNDO (object);
+
+	if (undo->priv->buffer && G_IS_OBJECT (undo->priv->buffer))
+	{
+		g_object_unref (undo->priv->buffer);
+		undo->priv->buffer = NULL;
+	}
+
+	G_OBJECT_CLASS (xpad_undo_parent_class)->dispose (object);
+}
+
+static void
+xpad_undo_finalize (GObject *object)
+{
+	XpadUndo *undo = XPAD_UNDO (object);
+
+	// remove all elements except for the left guard
+	xpad_undo_clear_history (undo);
+
+	// remove left guard
+	g_list_free (undo->priv->history_start);
+
+	G_OBJECT_CLASS (xpad_undo_parent_class)->finalize (object);
+}
+
+static void
+xpad_undo_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+	XpadUndo *undo = XPAD_UNDO (object);
+
+	switch (prop_id)
+	{
+	case PROP_BUFFER:
+		if (undo->priv->buffer && G_IS_OBJECT (undo->priv->buffer))
+			g_object_unref (undo->priv->buffer);
+		if (G_VALUE_HOLDS_POINTER (value) && G_IS_OBJECT (g_value_get_pointer (value)))
+		{
+			undo->priv->buffer = g_value_get_pointer (value);
+			g_object_ref (undo->priv->buffer);
+		}
+		break;
+
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+    }
+}
+
+static void
+xpad_undo_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+	XpadUndo *undo = XPAD_UNDO (object);
+
+	switch (prop_id)
+	{
+	case PROP_BUFFER:
+		g_value_set_pointer (value, undo->priv->buffer);
+		break;
+
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+xpad_undo_begin_user_action (GtkTextBuffer *buffer, XpadUndo *undo)
+{
+	undo->priv->user_action++;
+}
+
+static void
+xpad_undo_end_user_action (GtkTextBuffer *buffer, XpadUndo *undo)
+{
+	if (undo->priv->user_action > 0)
+		undo->priv->user_action--;
+}
+
+// Removed current element and returns a pointer
+// to the previos
+static GList*
+xpad_undo_remove_action_elem (GList *curr)
+{
+	if (curr->data)
+	{
+		UserAction *action = curr->data;
+		g_free (action->text);
+		g_free (action);
+		if (curr->prev)
+			curr->prev->next = curr->next;
+		if (curr->next)
+			curr->next->prev = curr->prev;
+		GList *new_curr = curr->prev;
+		curr->prev = NULL;
+		curr->next = NULL;
+		g_list_free (curr);
+		curr = new_curr;
+	}
+	return curr;
+}
+
+// Redo is impossible after text insertion/deletion,
+// only right after Undo (another Redo),
+// so we have to remove every history entry
+// after the current one if there is any
+static void
+xpad_undo_clear_redo_history (XpadUndo *undo)
+{
+	while (undo->priv->history_curr->next)
+		xpad_undo_remove_action_elem (undo->priv->history_curr->next);
+}
+	
+static void xpad_undo_clear_history (XpadUndo *undo)
+{
+	while (undo->priv->history_start->next)
+		xpad_undo_remove_action_elem (undo->priv->history_start->next);
+	undo->priv->history_curr = undo->priv->history_start;
+}
+
+static void
+xpad_undo_insert_text (GtkTextBuffer *buffer, GtkTextIter *location, gchar *text, gint len, XpadUndo *undo)
+{
+	if (undo->priv->frozen)
+		return;
+
+	if (undo->priv->user_action)
+	{
+		xpad_undo_clear_redo_history (undo);
+
+		gint pos = gtk_text_iter_get_offset (location);
+		gint n_utf8_chars = g_utf8_strlen (text, len);
+
+		// Merge similar actions
+		// This is how Undo works in most editors, if there is a series of
+		// 1-letter insertions - they are merge for Undo
+		if (undo->priv->history_curr->data)
+		{
+			UserAction *prev_action = undo->priv->history_curr->data;
+
+			if (prev_action->action_type == USER_ACTION_INSERT_TEXT)
+			{
+				// series of 1-letter insertions
+				if ((n_utf8_chars == 1 && prev_action->n_utf8_chars == 1) || (n_utf8_chars == 1 && prev_action->merged))
+				{
+					// if there was a space stop merging unless that was a series of spaces
+					if ((!g_unichar_isspace (prev_action->text[0]) && !g_ascii_isspace (text[0])) ||
+						(g_unichar_isspace (prev_action->text[0]) && g_unichar_isspace (text[0])))
+					{
+						gchar *joined_str = g_strjoin(NULL, prev_action->text, text, NULL);
+						g_free (prev_action->text);
+						prev_action->text = joined_str;
+						prev_action->len_in_bytes += len;
+						prev_action->end += prev_action->len_in_bytes;
+						prev_action->n_utf8_chars = prev_action->n_utf8_chars + n_utf8_chars;
+						prev_action->merged = TRUE;
+						return;
+					}
+				}
+			}
+		}
+
+		UserAction *action = g_new (UserAction, 1);
+		action->action_type = USER_ACTION_INSERT_TEXT;
+		action->text = g_strdup (text);
+		action->start = pos;
+		action->end = pos + len;
+		action->len_in_bytes = abs (action->end - action->start);
+		action->n_utf8_chars = n_utf8_chars;
+		action->merged = FALSE;
+
+		// since each operation clears redo we know that there
+		// is nothing after history_curr at this point so we
+		// insert right after it. history_start won't change
+		// since it is a left guard - not NULL
+		GList *dummy_start = g_list_append (undo->priv->history_curr, action); // supress warning, we have left guard for start
+		undo->priv->history_curr = g_list_next (undo->priv->history_curr);
+	}
+}
+
+static void
+xpad_undo_delete_range (GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end, XpadUndo *undo)
+{
+	if (undo->priv->frozen)
+		return;
+
+	if (undo->priv->user_action)
+	{
+		xpad_undo_clear_redo_history (undo);
+
+		gchar *text = gtk_text_iter_get_text (start, end);
+		gint start_offset = gtk_text_iter_get_offset (start);
+		gint end_offset = gtk_text_iter_get_offset (end);
+		gint len = abs (end_offset - start_offset);
+		gint n_utf8_chars = g_utf8_strlen (text, len);
+
+		UserAction *action = g_new (UserAction, 1);
+		action->action_type = USER_ACTION_DELETE_RANGE;
+		action->text = g_strdup (text);
+		action->start = start_offset;
+		action->end = end_offset;
+		action->len_in_bytes = len;
+		action->n_utf8_chars = n_utf8_chars;
+		action->merged = FALSE;
+
+		GList *dummy_start = g_list_append (undo->priv->history_curr, action); // supress warning, we have left guard for start
+		undo->priv->history_curr = g_list_next (undo->priv->history_curr);
+	}
+}
+
+void xpad_undo_exec_undo (XpadUndo *undo)
+{
+	if (undo->priv->frozen)
+		return;
+
+	// No undo without buffer
+	if (undo->priv->buffer == NULL || !G_IS_OBJECT (undo->priv->buffer))
+		return;
+
+	// Undo current element if it is not a start of the list
+	if (undo->priv->history_curr->data)
+	{
+		UserAction *action = undo->priv->history_curr->data;
+
+		switch (action->action_type)
+		{
+		case USER_ACTION_INSERT_TEXT:
+			xpad_text_buffer_delete_range (undo->priv->buffer,
+				action->start,
+				action->end);
+			break;
+		case USER_ACTION_DELETE_RANGE:
+			xpad_text_buffer_insert_text (undo->priv->buffer,
+				action->start,
+				action->text,
+				action->len_in_bytes);
+			break;
+		}
+
+		undo->priv->history_curr = g_list_previous (undo->priv->history_curr);
+	}
+}
+
+void xpad_undo_exec_redo (XpadUndo *undo)
+{
+	if (undo->priv->frozen)
+		return;
+
+	// No undo without buffer
+	if (undo->priv->buffer == NULL || !G_IS_OBJECT (undo->priv->buffer))
+		return;
+
+	// Redo next element if there is any
+	if (undo->priv->history_curr->next)
+	{
+		UserAction *action = undo->priv->history_curr->next->data;
+
+		switch (action->action_type)
+		{
+		case USER_ACTION_DELETE_RANGE:
+			xpad_text_buffer_delete_range (undo->priv->buffer,
+				action->start,
+				action->end);
+			break;
+		case USER_ACTION_INSERT_TEXT:
+			xpad_text_buffer_insert_text (undo->priv->buffer,
+				action->start,
+				action->text,
+				action->len_in_bytes);
+			break;
+		}
+
+		undo->priv->history_curr = g_list_next (undo->priv->history_curr);
+	}
+}
+
+void xpad_undo_freeze (XpadUndo *undo)
+{
+	undo->priv->frozen = TRUE;
+}
+
+void xpad_undo_thaw (XpadUndo *undo)
+{
+	undo->priv->frozen = FALSE;
+}
+
diff -Ndru o/xpad-undo.h p/xpad-undo.h
--- o/xpad-undo.h	1970-01-01 03:00:00.000000000 +0300
+++ p/xpad-undo.h	2010-03-31 02:41:00.000000000 +0300
@@ -0,0 +1,64 @@
+/*
+
+Copyright (c) 2001-2007 Michael Terry
+Copyright (c) 2010 Sergei Riaguzov
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3 of the License, or
+(at your option) any later version.
+
+This program 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+*/
+
+#ifndef __XPAD_UNDO_H__
+#define __XPAD_UNDO_H__
+
+#include <gtk/gtk.h>
+#include "xpad-text-buffer.h"
+
+G_BEGIN_DECLS
+
+#define XPAD_TYPE_UNDO          (xpad_undo_get_type ())
+#define XPAD_UNDO(o)            (G_TYPE_CHECK_INSTANCE_CAST ((o), XPAD_TYPE_UNDO, XpadUndo))
+#define XPAD_UNDO_CLASS(k)      (G_TYPE_CHECK_CLASS_CAST((k), XPAD_TYPE_UNDO, XpadUndoClass))
+#define XPAD_IS_UNDO(o)         (G_TYPE_CHECK_INSTANCE_TYPE ((o), XPAD_TYPE_UNDO))
+#define XPAD_IS_UNDO_CLASS(k)   (G_TYPE_CHECK_CLASS_TYPE ((k), XPAD_TYPE_UNDO))
+#define XPAD_UNDO_GET_CLASS(o)  (G_TYPE_INSTANCE_GET_CLASS ((o), XPAD_TYPE_UNDO, XpadUndoClass))
+
+typedef struct XpadUndoClass XpadUndoClass;
+typedef struct XpadUndoPrivate XpadUndoPrivate;
+typedef struct XpadUndo XpadUndo;
+
+struct XpadUndo
+{
+	GObject parent;
+
+	/* private */
+	XpadUndoPrivate *priv;
+};
+
+struct XpadUndoClass
+{
+	GObjectClass parent_class;
+};
+
+GType xpad_undo_get_type (void);
+
+XpadUndo* xpad_undo_new (XpadTextBuffer *buffer);
+void xpad_undo_exec_undo (XpadUndo *undo);
+void xpad_undo_exec_redo (XpadUndo *undo);
+void xpad_undo_freeze (XpadUndo *undo);
+void xpad_undo_thaw (XpadUndo *undo);
+
+G_END_DECLS
+
+#endif /* __XPAD_UNDO_H__ */

Follow ups