Fix crash with auto-indent
[claws.git] / src / undo.c
index bbb60225b6cdd9ee8c123b49d0d8d387bfac327f..61c4573404d907cd81ccc388bf390e22f63424a7 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
- * Copyright (C) 1999-2009 Hiroyuki Yamamoto and the Claws Mail team
+ * Copyright (C) 1999-2012 Hiroyuki Yamamoto and the Claws Mail team
  *
  * 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
@@ -22,6 +22,7 @@
 
 #ifdef HAVE_CONFIG_H
 #  include "config.h"
+#include "claws-features.h"
 #endif
 
 #include <glib.h>
@@ -45,6 +46,15 @@ struct _UndoInfo
        gint mergeable;
 };
 
+struct _UndoWrap
+{
+       gint lock;
+       gchar *pre_wrap_content;
+       gint start_pos;
+       gint end_pos;
+       gint len_change;
+};
+
 static void undo_free_list     (GList         **list_pointer);
 static void undo_check_size    (UndoMain       *undostruct);
 static gint undo_merge         (GList          *list,
@@ -83,7 +93,7 @@ UndoMain *undo_init(GtkWidget *text)
        GtkTextView *textview = GTK_TEXT_VIEW(text); 
        GtkTextBuffer *textbuf = gtk_text_view_get_buffer(textview);
 
-       g_return_val_if_fail(text != NULL, NULL);
+       cm_return_val_if_fail(text != NULL, NULL);
 
        undostruct = g_new0(UndoMain, 1);
        undostruct->textview = textview;
@@ -154,7 +164,7 @@ static void undo_free_list(GList **list_pointer)
 void undo_set_change_state_func(UndoMain *undostruct, UndoChangeStateFunc func,
                                gpointer data)
 {
-       g_return_if_fail(undostruct != NULL);
+       cm_return_if_fail(undostruct != NULL);
 
        undostruct->change_state_func = func;
        undostruct->change_state_data = data;
@@ -286,8 +296,8 @@ static void undo_add(const gchar *text,
        UndoInfo *undoinfo;
        GtkAdjustment *vadj;
 
-       g_return_if_fail(text != NULL);
-       g_return_if_fail(end_pos >= start_pos);
+       cm_return_if_fail(text != NULL);
+       cm_return_if_fail(end_pos >= start_pos);
 
        undo_free_list(&undostruct->redo);
 
@@ -297,7 +307,7 @@ static void undo_add(const gchar *text,
                                      undostruct->change_state_data);
 
        if (undostruct->paste != 0) {
-               if (action == UNDO_ACTION_INSERT) 
+               if (action == UNDO_ACTION_INSERT)
                        action = UNDO_ACTION_REPLACE_INSERT;
                else 
                        action = UNDO_ACTION_REPLACE_DELETE;
@@ -311,9 +321,10 @@ static void undo_add(const gchar *text,
 
        undo_check_size(undostruct);
 
-       vadj = GTK_ADJUSTMENT(GTK_TEXT_VIEW(undostruct->textview)->vadjustment);
+       vadj = GTK_ADJUSTMENT(gtk_text_view_get_vadjustment(
+                               GTK_TEXT_VIEW(undostruct->textview)));
        undoinfo = undo_object_new(g_strdup(text), start_pos, end_pos, action,
-                                  vadj->value);
+                                  gtk_adjustment_get_value(vadj));
 
        if (end_pos - start_pos != 1 || text[0] == '\n')
                undoinfo->mergeable = FALSE;
@@ -342,14 +353,14 @@ void undo_undo(UndoMain *undostruct)
        GtkTextIter iter, start_iter, end_iter;
        GtkTextMark *mark;
 
-       g_return_if_fail(undostruct != NULL);
+       cm_return_if_fail(undostruct != NULL);
 
        if (undostruct->undo == NULL) return;
 
        /* The undo data we need is always at the top op the
           stack. So, therefore, the first one */
        undoinfo = (UndoInfo *)undostruct->undo->data;
-       g_return_if_fail(undoinfo != NULL);
+       cm_return_if_fail(undoinfo != NULL);
        undoinfo->mergeable = FALSE;
        undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
        undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
@@ -366,7 +377,7 @@ void undo_undo(UndoMain *undostruct)
 
        /* Move the view (scrollbars) to the correct position */
        gtk_adjustment_set_value
-               (GTK_ADJUSTMENT(textview->vadjustment),
+               (GTK_ADJUSTMENT(gtk_text_view_get_vadjustment(textview)),
                 undoinfo->window_position);
        
        switch (undoinfo->action) {
@@ -383,18 +394,30 @@ void undo_undo(UndoMain *undostruct)
                gtk_text_buffer_get_iter_at_offset(buffer, &start_iter, undoinfo->start_pos);
                gtk_text_buffer_get_iter_at_offset(buffer, &end_iter, undoinfo->end_pos);
                gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
-               /* "pull" another data structure from the list */
+               /* "pull" previous matching DELETE data structure from the list */
                if (undostruct->undo){
                        undoinfo = (UndoInfo *)undostruct->undo->data;
                        undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
                        undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
-                       g_return_if_fail(undoinfo != NULL);
-                       g_return_if_fail(undoinfo->action == UNDO_ACTION_REPLACE_DELETE);
+                       cm_return_if_fail(undoinfo != NULL);
+                       cm_return_if_fail(undoinfo->action == UNDO_ACTION_REPLACE_DELETE);
                        gtk_text_buffer_insert(buffer, &start_iter, undoinfo->text, -1);
                }
                break;
        case UNDO_ACTION_REPLACE_DELETE:
-               g_warning("This should not happen. UNDO_REPLACE_DELETE");
+               gtk_text_buffer_get_iter_at_offset(buffer, &start_iter, undoinfo->start_pos);
+               gtk_text_buffer_insert(buffer, &start_iter, undoinfo->text, -1);
+               /* "pull" previous matching INSERT data structure from the list */
+               if (undostruct->undo){
+                       undoinfo = (UndoInfo *)undostruct->undo->data;
+                       undostruct->redo = g_list_prepend(undostruct->redo, undoinfo);
+                       undostruct->undo = g_list_remove(undostruct->undo, undoinfo);
+                       cm_return_if_fail(undoinfo != NULL);
+                       cm_return_if_fail(undoinfo->action == UNDO_ACTION_REPLACE_INSERT);
+                       gtk_text_buffer_get_iter_at_offset(buffer, &start_iter, undoinfo->start_pos);
+                       gtk_text_buffer_get_iter_at_offset(buffer, &end_iter, undoinfo->end_pos);
+                       gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
+               }
                break;
        default:
                g_assert_not_reached();
@@ -429,12 +452,12 @@ void undo_redo(UndoMain *undostruct)
        GtkTextIter iter, start_iter, end_iter;
        GtkTextMark *mark;
 
-       g_return_if_fail(undostruct != NULL);
+       cm_return_if_fail(undostruct != NULL);
 
        if (undostruct->redo == NULL) return;
 
        redoinfo = (UndoInfo *)undostruct->redo->data;
-       g_return_if_fail (redoinfo != NULL);
+       cm_return_if_fail (redoinfo != NULL);
        undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
        undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
 
@@ -449,7 +472,7 @@ void undo_redo(UndoMain *undostruct)
        gtk_text_buffer_place_cursor(buffer, &iter);
 
        /* Move the view to the right position. */
-       gtk_adjustment_set_value(textview->vadjustment
+       gtk_adjustment_set_value(gtk_text_view_get_vadjustment(textview)
                                 redoinfo->window_position);
 
        switch (redoinfo->action) {
@@ -467,18 +490,28 @@ void undo_redo(UndoMain *undostruct)
                gtk_text_buffer_get_iter_at_offset(buffer, &end_iter, redoinfo->end_pos);
                gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
                debug_print("UNDO_ACTION_REPLACE %s\n", redoinfo->text);
-               /* "pull" another data structure from the list */
+               /* "pull" previous matching INSERT data structure from the list */
                redoinfo = (UndoInfo *)undostruct->redo->data;
-               g_return_if_fail(redoinfo != NULL);
+               cm_return_if_fail(redoinfo != NULL);
                undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
                undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
-               g_return_if_fail(redoinfo->action == UNDO_ACTION_REPLACE_INSERT);
+               cm_return_if_fail(redoinfo->action == UNDO_ACTION_REPLACE_INSERT);
                gtk_text_buffer_insert(buffer, &start_iter, redoinfo->text, -1);
                break;
        case UNDO_ACTION_REPLACE_INSERT:
-               /* This is needed only if we redo from a middle-click button */
                gtk_text_buffer_get_iter_at_offset(buffer, &iter, redoinfo->start_pos);
                gtk_text_buffer_insert(buffer, &iter, redoinfo->text, -1);
+               /* "pull" previous matching DELETE structure from the list */
+               redoinfo = (UndoInfo *)undostruct->redo->data;
+               /* Do nothing if we redo from a middle-click button
+                * and next action is not UNDO_ACTION_REPLACE_DELETE */
+               if (redoinfo && redoinfo->action == UNDO_ACTION_REPLACE_DELETE) {
+                       undostruct->undo = g_list_prepend(undostruct->undo, redoinfo);
+                       undostruct->redo = g_list_remove(undostruct->redo, redoinfo);
+                       gtk_text_buffer_get_iter_at_offset(buffer, &start_iter, redoinfo->start_pos);
+                       gtk_text_buffer_get_iter_at_offset(buffer, &end_iter, redoinfo->end_pos);
+                       gtk_text_buffer_delete(buffer, &start_iter, &end_iter);
+               }
                break;
        default:
                g_assert_not_reached();
@@ -502,7 +535,7 @@ void undo_block(UndoMain *undostruct)
 {
        GtkTextBuffer *buffer;
 
-       g_return_if_fail(GTK_IS_TEXT_VIEW(undostruct->textview));
+       cm_return_if_fail(GTK_IS_TEXT_VIEW(undostruct->textview));
 
        buffer = gtk_text_view_get_buffer(undostruct->textview);
        g_signal_handlers_block_by_func(buffer, undo_insert_text_cb, undostruct);
@@ -515,7 +548,7 @@ void undo_unblock(UndoMain *undostruct)
 {
        GtkTextBuffer *buffer;
 
-       g_return_if_fail(GTK_IS_TEXT_VIEW(undostruct->textview));
+       cm_return_if_fail(GTK_IS_TEXT_VIEW(undostruct->textview));
 
        buffer = gtk_text_view_get_buffer(undostruct->textview);
        g_signal_handlers_unblock_by_func(buffer, undo_insert_text_cb, undostruct);
@@ -524,10 +557,154 @@ void undo_unblock(UndoMain *undostruct)
                                          undostruct);
 }
 
+/* Init the WrapInfo structure */
+static void init_wrap_undo(UndoMain *undostruct)
+{
+       GtkTextBuffer *buffer;
+       GtkTextIter start, end;
+
+       cm_return_if_fail(undostruct != NULL);
+       cm_return_if_fail(undostruct->wrap_info == NULL);
+
+       undostruct->wrap_info = g_new0(UndoWrap, 1);
+
+       /* Save the whole buffer as original contents. We'll retain the
+        * changed region when exiting wrap mode.
+        */
+       buffer = gtk_text_view_get_buffer(undostruct->textview);
+       gtk_text_buffer_get_start_iter(buffer, &start);
+       gtk_text_buffer_get_end_iter(buffer, &end);
+       undostruct->wrap_info->pre_wrap_content
+               = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
+
+       undostruct->wrap_info->lock = 0;
+
+       /* start_pos == -1 means nothing changed yet. */
+       undostruct->wrap_info->start_pos = -1;
+       undostruct->wrap_info->end_pos = -1;
+       undostruct->wrap_info->len_change = 0;
+}
+
+static void end_wrap_undo(UndoMain *undostruct)
+{
+       GtkTextBuffer *buffer;
+       GtkTextIter start, end;
+       gchar *old_contents = NULL;
+       gchar *cur_contents = NULL;
+       gchar *new_contents = NULL;
+
+       cm_return_if_fail(undostruct != NULL);
+       cm_return_if_fail(undostruct->wrap_info != NULL);
+
+       /* If start_pos is still == -1, it means nothing changed. */
+       if (undostruct->wrap_info->start_pos == -1)
+               goto cleanup;
+
+       cm_return_if_fail(undostruct->wrap_info->end_pos > undostruct->wrap_info->start_pos);
+       cm_return_if_fail(undostruct->wrap_info->end_pos - undostruct->wrap_info->len_change > undostruct->wrap_info->start_pos);
+
+       /* get the whole new (wrapped) contents */
+       buffer = gtk_text_view_get_buffer(undostruct->textview);
+       gtk_text_buffer_get_start_iter(buffer, &start);
+       gtk_text_buffer_get_end_iter(buffer, &end);
+       cur_contents = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
+
+       debug_print("wrapping done from %d to %d, len change: %d\n",
+               undostruct->wrap_info->start_pos,
+               undostruct->wrap_info->end_pos,
+               undostruct->wrap_info->len_change);
+
+       /* keep the relevant old unwrapped part, which is what
+        * was between start_pos & end_pos - len_change
+        */
+       old_contents = g_utf8_substring(
+                       undostruct->wrap_info->pre_wrap_content,
+                       undostruct->wrap_info->start_pos,
+                       undostruct->wrap_info->end_pos
+                       - undostruct->wrap_info->len_change);
+
+       /* and get the changed contents, from start_pos to end_pos. */
+       new_contents = g_utf8_substring(
+                       cur_contents,
+                       undostruct->wrap_info->start_pos,
+                       undostruct->wrap_info->end_pos);
+
+       /* add the deleted (unwrapped) text to the undo pile */
+       undo_add(old_contents,
+                undostruct->wrap_info->start_pos,
+                undostruct->wrap_info->end_pos
+                 - undostruct->wrap_info->len_change,
+                UNDO_ACTION_REPLACE_DELETE,
+                undostruct);
+
+       /* add the inserted (wrapped) text to the undo pile */
+       undo_add(new_contents,
+                undostruct->wrap_info->start_pos,
+                undostruct->wrap_info->end_pos,
+                UNDO_ACTION_REPLACE_INSERT,
+                undostruct);
+
+       g_free(old_contents);
+       g_free(cur_contents);
+       g_free(new_contents);
+cleanup:
+       g_free(undostruct->wrap_info->pre_wrap_content);
+       g_free(undostruct->wrap_info);
+       undostruct->wrap_info = NULL;
+}
+
+static void update_wrap_undo(UndoMain *undostruct, const gchar *text, int start, 
+                            int end, UndoAction action)
+{
+       gint len = end - start;
+
+       /* If we don't yet have a start position, or farther than
+        * current, store it.
+        */
+       if (undostruct->wrap_info->start_pos == -1
+        || start < undostruct->wrap_info->start_pos) {
+               undostruct->wrap_info->start_pos = start;
+       }
+
+       if (action == UNDO_ACTION_INSERT) {
+               /* If inserting, the end of the region is at the end of the
+                * change, and the total length of the changed region
+                * increases.
+                */
+               if (end > undostruct->wrap_info->end_pos) {
+                       undostruct->wrap_info->end_pos = end;
+               }
+               undostruct->wrap_info->len_change += len;
+       } else if (action == UNDO_ACTION_DELETE) {
+               /* If deleting, the end of the region is at the start of the
+                * change, and the total length of the changed region
+                * decreases.
+                */
+               if (start > undostruct->wrap_info->end_pos) {
+                       undostruct->wrap_info->end_pos = start;
+               }
+               undostruct->wrap_info->len_change -= len;
+       }
+}
+
+/* Set wrapping mode, in which changes are agglomerated until
+ * the end of wrapping mode.
+ */
 void undo_wrapping(UndoMain *undostruct, gboolean wrap)
 {
-//     debug_print("undo wrapping now %d\n", wrap);
-       undostruct->wrap = wrap;
+       if (wrap) {
+               /* Start (or go deeper in) wrapping mode */
+               if (undostruct->wrap_info == NULL)
+                       init_wrap_undo(undostruct);
+               undostruct->wrap_info->lock++;
+       } else if (undostruct->wrap_info != NULL) {
+               /* exit (& possible stop) one level of wrapping mode */
+               undostruct->wrap_info->lock--;
+               if (undostruct->wrap_info->lock == 0)
+                       end_wrap_undo(undostruct);
+       } else {
+               g_warning("undo already out of wrap mode");
+       }
 }
 
 void undo_insert_text_cb(GtkTextBuffer *textbuf, GtkTextIter *iter,
@@ -536,28 +713,22 @@ void undo_insert_text_cb(GtkTextBuffer *textbuf, GtkTextIter *iter,
 {
        gchar *text_to_insert;
        gint pos;
+       glong utf8_len;
+
        if (prefs_common.undolevels <= 0) return;
 
        pos = gtk_text_iter_get_offset(iter);
-       if (undostruct->wrap && undostruct->undo) {
-               UndoInfo *last_undo = undostruct->undo->data;
-               if (last_undo && (last_undo->action == UNDO_ACTION_INSERT
-                                 || last_undo->action == UNDO_ACTION_REPLACE_INSERT)
-               &&  last_undo->start_pos < pos && last_undo->end_pos > pos) {
-                       GtkTextIter start,end;
-                       last_undo->end_pos += g_utf8_strlen(new_text, -1);
-                       gtk_text_buffer_get_iter_at_offset(textbuf, &start, last_undo->start_pos);
-                       gtk_text_buffer_get_iter_at_offset(textbuf, &end, last_undo->end_pos);
-                       g_free(last_undo->text);
-                       last_undo->text = gtk_text_buffer_get_text(textbuf, &start, &end, FALSE);
-                       debug_print("add:undo upd %d-%d\n", last_undo->start_pos, last_undo->end_pos);
-                       return;
-               } else debug_print("add:last: %d, %d-%d (%d)\n", last_undo->action,
-                       last_undo->start_pos, last_undo->end_pos, pos);
-       } 
        Xstrndup_a(text_to_insert, new_text, new_text_length, return);
-       debug_print("add:undo add %d-%ld\n", pos, pos + g_utf8_strlen(text_to_insert, -1));
-       undo_add(text_to_insert, pos, pos + g_utf8_strlen(text_to_insert, -1),
+       utf8_len = g_utf8_strlen(text_to_insert, -1);
+
+       if (undostruct->wrap_info != NULL) {
+               update_wrap_undo(undostruct, text_to_insert, 
+                                pos, pos + utf8_len, UNDO_ACTION_INSERT);
+               return;
+       }
+
+       debug_print("add:undo add %d-%ld\n", pos, utf8_len);
+       undo_add(text_to_insert, pos, pos + utf8_len,
                 UNDO_ACTION_INSERT, undostruct);
 }
 
@@ -575,22 +746,9 @@ void undo_delete_text_cb(GtkTextBuffer *textbuf, GtkTextIter *start,
        start_pos = gtk_text_iter_get_offset(start);
        end_pos   = gtk_text_iter_get_offset(end);
 
-       if (undostruct->wrap && undostruct->undo) {
-               UndoInfo *last_undo = undostruct->undo->data;
-               if (last_undo && (last_undo->action == UNDO_ACTION_INSERT
-                                 || last_undo->action == UNDO_ACTION_REPLACE_INSERT)
-               &&  last_undo->start_pos < start_pos && last_undo->end_pos > end_pos) {
-                       GtkTextIter start,end;
-                       last_undo->end_pos -= g_utf8_strlen(text_to_delete, -1);
-                       gtk_text_buffer_get_iter_at_offset(textbuf, &start, last_undo->start_pos);
-                       gtk_text_buffer_get_iter_at_offset(textbuf, &end, last_undo->end_pos);
-                       g_free(last_undo->text);
-                       last_undo->text = gtk_text_buffer_get_text(textbuf, &start, &end, FALSE);
-                       debug_print("del:undo upd %d-%d\n", last_undo->start_pos, last_undo->end_pos);
-                       return;
-               } else debug_print("del:last: %d, %d-%d (%d)\n", last_undo->action,
-                       last_undo->start_pos, last_undo->end_pos, start_pos);
-               
+       if (undostruct->wrap_info != NULL) {
+               update_wrap_undo(undostruct, text_to_delete, start_pos, end_pos, UNDO_ACTION_DELETE);
+               return;
        } 
        debug_print("del:undo add %d-%d\n", start_pos, end_pos);
        undo_add(text_to_delete, start_pos, end_pos, UNDO_ACTION_DELETE,