URI: 
       tAdd basic undo/redo support - ledit - Text editor (WIP)
  HTML git clone git://lumidify.org/ledit.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/git/ledit.git (encrypted, but very slow)
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit 5d691e9f97b979d571d38c101554d72e91cb7db5
   DIR parent 8779bb819724ae7d05491112ae700063f49cde93
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Tue, 29 Jun 2021 19:06:30 +0200
       
       Add basic undo/redo support
       
       Diffstat:
         M IDEAS                               |       1 +
         M Makefile                            |       4 ++--
         M buffer.c                            |     350 ++++++++++++++++++++++---------
         M buffer.h                            |      21 +++++++++++++--------
         M cache.c                             |       1 +
         M common.h                            |       2 ++
         A lbuf.c                              |      51 +++++++++++++++++++++++++++++++
         A lbuf.h                              |      12 ++++++++++++
         M ledit.c                             |     160 +++++++++++++++++++++++++------
         M search.c                            |       1 +
         A undo.c                              |     250 +++++++++++++++++++++++++++++++
         A undo.h                              |      23 +++++++++++++++++++++++
       
       12 files changed, 738 insertions(+), 138 deletions(-)
       ---
   DIR diff --git a/IDEAS b/IDEAS
       t@@ -1,3 +1,4 @@
        * allow editing same file in multiple places at same time (like in acme)
        * add different (more basic) text backend
        * visual selection mode - allow to switch cursor between selection ends
       +* https://drewdevault.com/2021/06/27/You-cant-capture-the-nuance.html
   DIR diff --git a/Makefile b/Makefile
       t@@ -9,8 +9,8 @@ MANPREFIX = ${PREFIX}/man
        BIN = ${NAME}
        MAN1 = ${BIN:=.1}
        
       -OBJ = ${BIN:=.o} cache.o buffer.o memory.o util.o search.o
       -HDR = cache.h buffer.h memory.h common.h util.h search.h
       +OBJ = ${BIN:=.o} cache.o buffer.o memory.o util.o search.o lbuf.o undo.o
       +HDR = cache.h buffer.h memory.h common.h util.h search.h lbuf.h undo.h
        
        CFLAGS_LEDIT = -g -Wall -Wextra -D_POSIX_C_SOURCE=200809L `pkg-config --cflags x11 xkbfile pangoxft xext`
        LDFLAGS_LEDIT = ${LDFLAGS} `pkg-config --libs x11 xkbfile pangoxft xext` -lm
   DIR diff --git a/buffer.c b/buffer.c
       t@@ -11,8 +11,10 @@
        
        #include "memory.h"
        #include "common.h"
       +#include "lbuf.h"
        #include "buffer.h"
        #include "cache.h"
       +#include "undo.h"
        
        /*
         * Important notes:
       t@@ -147,13 +149,16 @@ ledit_wipe_line_cursor_attrs(ledit_buffer *buffer, int line) {
                l->dirty = 1;
        }
        
       +/* FIXME: To simplify this a bit, maybe just copy text to lbuf first and
       +   then insert it in one go instead of having this complex logic */
        void
        ledit_insert_text_from_line(
            ledit_buffer *buffer,
            int dst_line, int dst_index,
       -    int src_line, int src_index, int src_len) {
       +    int src_line, int src_index, int src_len,
       +    lbuf *text_ret) {
                ledit_insert_text_from_line_base(
       -            buffer, dst_line, dst_index, src_line, src_index, src_len
       +            buffer, dst_line, dst_index, src_line, src_index, src_len, text_ret
                );
                ledit_recalc_line(buffer, dst_line);
        }
       t@@ -162,23 +167,42 @@ void
        ledit_insert_text_from_line_base(
            ledit_buffer *buffer,
            int dst_line, int dst_index,
       -    int src_line, int src_index, int src_len) {
       +    int src_line, int src_index, int src_len,
       +    lbuf *text_ret) {
                assert(dst_line != src_line);
                ledit_line *ll = ledit_get_line(buffer, src_line);
                if (src_len == -1)
                        src_len = ll->len - src_index;
       +        if (text_ret != NULL) {
       +                lbuf_grow(text_ret, src_len);
       +                text_ret->len = src_len;
       +        }
                if (src_index >= ll->gap) {
                        /* all text to insert is after gap */
                        ledit_insert_text_base(
                            buffer, dst_line, dst_index,
                            ll->text + src_index + ll->cap - ll->len, src_len
                        );
       +                if (text_ret != NULL) {
       +                        memcpy(
       +                            text_ret->text,
       +                            ll->text + src_index + ll->cap - ll->len,
       +                            src_len
       +                        );
       +                }
                } else if (ll->gap - src_index >= src_len) {
                        /* all text to insert is before gap */
                        ledit_insert_text_base(
                            buffer, dst_line, dst_index,
                            ll->text + src_index, src_len
                        );
       +                if (text_ret != NULL) {
       +                        memcpy(
       +                            text_ret->text,
       +                            ll->text + src_index,
       +                            src_len
       +                        );
       +                }
                } else {
                        /* insert part of text before gap */
                        ledit_insert_text_base(
       t@@ -191,6 +215,18 @@ ledit_insert_text_from_line_base(
                            ll->text + ll->gap + ll->cap - ll->len,
                            src_len - ll->gap + src_index
                        );
       +                if (text_ret != NULL) {
       +                        memcpy(
       +                            text_ret->text,
       +                            ll->text + src_index,
       +                            ll->gap - src_index
       +                        );
       +                        memcpy(
       +                            text_ret + ll->gap - src_index,
       +                            ll->text + ll->gap + ll->cap - ll->len,
       +                            src_len - ll->gap + src_index
       +                        );
       +                }
        
                }
        }
       t@@ -343,9 +379,9 @@ ledit_insert_text_with_newlines_base(
                int cur_line = line_index;
                int cur_index = index;
                while ((cur = strchr_len(last, '\n', rem_len)) != NULL) {
       -                ledit_insert_text_base(buffer, cur_line, cur_index, last, cur - last);
                        /* FIXME: inefficient because there's no gap buffer yet */
       -                ledit_append_line_base(buffer, cur_line, -1);
       +                ledit_append_line_base(buffer, cur_line, cur_index);
       +                ledit_insert_text_base(buffer, cur_line, cur_index, last, cur - last);
                        cur_index = 0;
                        cur_line++;
                        last = cur + 1;
       t@@ -453,7 +489,7 @@ ledit_append_line_base(ledit_buffer *buffer, int line_index, int text_index) {
                        ledit_line *l = ledit_get_line(buffer, line_index);
                        ledit_insert_text_from_line_base(
                            buffer, line_index + 1, 0,
       -                    line_index, text_index, -1
       +                    line_index, text_index, -1, NULL
                        );
                        delete_line_section_base(
                            buffer, line_index,
       t@@ -653,21 +689,17 @@ ledit_copy_text(ledit_buffer *buffer, char *dst, int line1, int byte1, int line2
         * - *dst is null-terminated
         * - the range must be sorted already
         * - returns the length of the text, not including the NUL */
       -size_t
       -ledit_copy_text_with_resize(
       +void
       +ledit_copy_text_to_lbuf(
            ledit_buffer *buffer,
       -    char **dst, size_t *alloc,
       +    lbuf *buf,
            int line1, int byte1,
            int line2, int byte2) {
                assert(line1 < line2 || (line1 == line2 && byte1 <= byte2));
                size_t len = ledit_textlen(buffer, line1, byte1, line2, byte2);
       -        /* len + 1 because of nul */
       -        if (len + 1 > *alloc) {
       -                *alloc = *alloc * 2 > len + 1 ? *alloc * 2 : len + 1;
       -                *dst = ledit_realloc(*dst, *alloc);
       -        }
       -        ledit_copy_text(buffer, *dst, line1, byte1, line2, byte2);
       -        return len;
       +        lbuf_grow(buf, len + 1);
       +        ledit_copy_text(buffer, buf->text, line1, byte1, line2, byte2);
       +        buf->len = len;
        }
        
        /* get char with logical index i from line */
       t@@ -830,12 +862,14 @@ ledit_delete_range(
            ledit_buffer *buffer, int line_based,
            int line_index1, int byte_index1,
            int line_index2, int byte_index2,
       -    int *new_line_ret, int *new_byte_ret) {
       +    int *new_line_ret, int *new_byte_ret,
       +    ledit_range *final_range_ret, lbuf *text_ret) {
                ledit_delete_range_base(
                    buffer, line_based,
                    line_index1, byte_index1,
                    line_index2, byte_index2,
       -            new_line_ret, new_byte_ret
       +            new_line_ret, new_byte_ret,
       +            final_range_ret, text_ret
                );
                /* need to start recalculating one line before in case first
                   line was deleted and offset is now wrong */
       t@@ -849,7 +883,12 @@ ledit_delete_range_base(
            ledit_buffer *buffer, int line_based,
            int line_index1, int byte_index1,
            int line_index2, int byte_index2,
       -    int *new_line_ret, int *new_byte_ret) {
       +    int *new_line_ret, int *new_byte_ret,
       +    ledit_range *final_range_ret, lbuf *text_ret) {
       +        /* FIXME: Oh boy, this is nasty */
       +        /* range line x, range byte x */
       +        int rgl1 = 0, rgb1 = 0, rgl2 = 0, rgb2 = 0;
       +        int new_line = 0, new_byte = 0;
                if (line_based) {
                        int x, softline1, softline2;
                        ledit_line *line1 = ledit_get_line(buffer, line_index1);
       t@@ -865,51 +904,76 @@ ledit_delete_range_base(
                                PangoLayoutLine *pl2 = pango_layout_get_line_readonly(line1->layout, l2);
                                /* don't delete entire line if it is the last one remaining */
                                if (l1 == 0 && l2 == softlines - 1 && buffer->lines_num > 1) {
       -                                ledit_delete_line_entry_base(buffer, line_index1);
       -                                /* note: line_index1 is now the index of the next
       -                                   line since the current one was just deleted */
       -                                if (line_index1 < buffer->lines_num) {
       -                                        *new_line_ret = line_index1;
       +                                if (line_index1 < buffer->lines_num - 1) {
       +                                        /* cursor can be moved to next hard line */
       +                                        new_line = line_index1;
                                                ledit_x_softline_to_pos(
       -                                            ledit_get_line(buffer, line_index1),
       -                                            x, 0, new_byte_ret
       +                                            ledit_get_line(buffer, line_index1 + 1),
       +                                            x, 0, &new_byte
                                                );
       +                                        rgl1 = line_index1;
       +                                        rgb1 = 0;
       +                                        rgl2 = line_index1 + 1;
       +                                        rgb2 = 0;
                                        } else {
       -                                        /* note: logically, this must be >= 0 because
       -                                           buffer->lines_num > 1 && line_index1 >= buffer->lines_num */
       -                                        *new_line_ret = line_index1 - 1;
       +                                        /* cursor has to be be moved to previous hard line
       +                                           because last line in buffer is deleted */
       +                                        /* note: logically, line_index1 - 1 must be >= 0 because
       +                                           buffer->lines_num > 1 && line_index1 >= buffer->lines_num - 1 */
       +                                        new_line = line_index1 - 1;
                                                ledit_line *prevline = ledit_get_line(buffer, line_index1 - 1);
                                                softlines = pango_layout_get_line_count(prevline->layout);
       -                                        ledit_x_softline_to_pos(prevline, x, softlines - 1, new_byte_ret);
       +                                        ledit_x_softline_to_pos(prevline, x, softlines - 1, &new_byte);
       +                                        rgl1 = line_index1 - 1;
       +                                        rgb1 = prevline->len;
       +                                        rgl2 = line_index1;
       +                                        rgb2 = line1->len;
                                        }
       +                                if (text_ret) {
       +                                        ledit_copy_text_to_lbuf(
       +                                            buffer, text_ret,
       +                                            rgl1, rgb1,
       +                                            rgl2, rgb2
       +                                        );
       +                                }
       +                                ledit_delete_line_entry_base(buffer, line_index1);
                                } else {
       -                                /* FIXME: sanity checks that the length is actually positive, etc. */
       +                                assert(pl2->start_index + pl2->length - pl1->start_index >= 0);
       +                                rgl1 = rgl2 = line_index1;
       +                                rgb1 = pl1->start_index;
       +                                rgb2 = pl2->start_index + pl2->length;
       +                                if (text_ret) {
       +                                        ledit_copy_text_to_lbuf(
       +                                            buffer, text_ret,
       +                                            rgl1, rgb1,
       +                                            rgl2, rgb2
       +                                        );
       +                                }
                                        delete_line_section_base(
       -                                    buffer, line_index1, pl1->start_index,
       -                                    pl2->start_index + pl2->length - pl1->start_index
       +                                    buffer, line_index1, rgb1, rgb2 - rgb1
                                        );
                                        if (l2 == softlines - 1 && line_index1 < buffer->lines_num - 1) {
       -                                        *new_line_ret = line_index1 + 1;
       +                                        new_line = line_index1 + 1;
                                                ledit_x_softline_to_pos(
                                                    ledit_get_line(buffer, line_index1 + 1),
       -                                            x, 0, new_byte_ret
       +                                            x, 0, &new_byte
                                                );
                                        } else if (l2 < softlines - 1) {
       -                                        *new_line_ret = line_index1;
       +                                        new_line = line_index1;
                                                ledit_x_softline_to_pos(
                                                    ledit_get_line(buffer, line_index1),
       -                                            x, l1, new_byte_ret
       +                                            x, l1, &new_byte
                                                );
                                        } else if (l1 > 0) {
       -                                        *new_line_ret = line_index1;
       +                                        new_line = line_index1;
                                                ledit_x_softline_to_pos(
                                                    ledit_get_line(buffer, line_index1),
       -                                            x, l1 - 1, new_byte_ret
       +                                            x, l1 - 1, &new_byte
                                                );
                                        } else {
                                                /* the line has been emptied and is the last line remaining */
       -                                        *new_line_ret = 0;
       -                                        *new_byte_ret = 0;
       +                                        new_line = 0;
       +                                        new_byte = 0;
                                        }
                                }
                        } else {
       t@@ -937,96 +1001,186 @@ ledit_delete_range_base(
                                int softlines = pango_layout_get_line_count(ll2->layout);
                                if (sl1 == 0 && sl2 == softlines - 1) {
                                        if (l1 == 0 && l2 == buffer->lines_num - 1) {
       +                                        rgl1 = l1;
       +                                        rgl2 = l2;
       +                                        rgb1 = 0;
       +                                        rgb2 = ll2->len;
       +                                        if (text_ret) {
       +                                                ledit_copy_text_to_lbuf(
       +                                                    buffer, text_ret,
       +                                                    rgl1, rgb1,
       +                                                    rgl2, rgb2
       +                                                );
       +                                        }
                                                delete_line_section_base(buffer, l1, 0, ll1->len);
                                                ledit_delete_line_entries_base(buffer, l1 + 1, l2);
       -                                        *new_line_ret = 0;
       -                                        *new_byte_ret = 0;
       +                                        new_line = 0;
       +                                        new_byte = 0;
                                        } else {
       -                                        ledit_delete_line_entries_base(buffer, l1, l2);
       -                                        if (l1 >= buffer->lines_num) {
       -                                                *new_line_ret = buffer->lines_num - 1;
       -                                                ledit_line *new_lline = ledit_get_line(buffer, *new_line_ret);
       +                                        if (l2 == buffer->lines_num - 1) {
       +                                                new_line = l1 - 1;
       +                                                ledit_line *new_lline = ledit_get_line(buffer, new_line);
                                                        int new_softlines = pango_layout_get_line_count(new_lline->layout);
       -                                                ledit_x_softline_to_pos(new_lline, x, new_softlines - 1, new_byte_ret);
       +                                                ledit_x_softline_to_pos(new_lline, x, new_softlines - 1, &new_byte);
       +                                                rgl1 = l1 - 1;
       +                                                rgb1 = new_lline->len;
       +                                                rgl2 = l2;
       +                                                rgb2 = ll2->len;
                                                } else {
       -                                                *new_line_ret = l1;
       +                                                new_line = l1;
       +                                                ledit_line *nextline = ledit_get_line(buffer, l2 + 1);
                                                        ledit_x_softline_to_pos(
       -                                                    ledit_get_line(buffer, l1),
       -                                                    x, 0, new_byte_ret
       +                                                    nextline, x, 0, &new_byte
                                                        );
       +                                                rgl1 = l1;
       +                                                rgb1 = 0;
       +                                                rgl2 = l2 + 1;
       +                                                rgb2 = nextline->len;
                                                }
       +                                        if (text_ret) {
       +                                                ledit_copy_text_to_lbuf(
       +                                                    buffer, text_ret,
       +                                                    rgl1, rgb1,
       +                                                    rgl2, rgb2
       +                                                );
       +                                        }
       +                                        ledit_delete_line_entries_base(buffer, l1, l2);
                                        }
                                } else if (sl1 == 0) {
       +                                rgl1 = l1;
       +                                rgb1 = 0;
       +                                rgl2 = l2;
       +                                rgb2 = pl2->start_index + pl2->length;
       +                                if (text_ret) {
       +                                        ledit_copy_text_to_lbuf(
       +                                            buffer, text_ret,
       +                                            rgl1, rgb1,
       +                                            rgl2, rgb2
       +                                        );
       +                                }
                                        delete_line_section_base(buffer, l2, 0, pl2->start_index + pl2->length);
       +                                new_line = l1;
       +                                ledit_x_softline_to_pos(ll2, x, 0, &new_byte);
                                        ledit_delete_line_entries_base(buffer, l1, l2 - 1);
       -                                *new_line_ret = l1;
       -                                ledit_x_softline_to_pos(ledit_get_line(buffer, l1), x, 0, new_byte_ret);
                                } else if (sl2 == softlines - 1) {
       -                                delete_line_section_base(buffer, l1, pl1->start_index, ll1->len - pl1->start_index);
       -                                ledit_delete_line_entries_base(buffer, l1 + 1, l2);
       -                                if (l1 + 1 >= buffer->lines_num) {
       -                                        *new_line_ret = buffer->lines_num - 1;
       -                                        ledit_line *new_lline = ledit_get_line(buffer, *new_line_ret);
       -                                        int new_softlines = pango_layout_get_line_count(new_lline->layout);
       -                                        ledit_x_softline_to_pos(new_lline, x, new_softlines - 1, new_byte_ret);
       +                                rgl1 = l1;
       +                                rgb1 = pl1->start_index;
       +                                rgl2 = l2;
       +                                rgb2 = ll2->len;
       +                                if (l2 + 1 == buffer->lines_num) {
       +                                        new_line = l1;
       +                                        ledit_x_softline_to_pos(ll1, x, sl1 - 1, &new_byte);
                                        } else {
       -                                        *new_line_ret = l1 + 1;
       +                                        new_line = l1 + 1;
                                                ledit_x_softline_to_pos(
       -                                            ledit_get_line(buffer, l1 + 1),
       -                                            x, 0, new_byte_ret
       +                                            ledit_get_line(buffer, l2 + 1),
       +                                            x, 0, &new_byte
       +                                        );
       +                                }
       +                                if (text_ret) {
       +                                        ledit_copy_text_to_lbuf(
       +                                            buffer, text_ret,
       +                                            rgl1, rgb1,
       +                                            rgl2, rgb2
                                                );
                                        }
       +                                delete_line_section_base(buffer, l1, pl1->start_index, ll1->len - pl1->start_index);
       +                                ledit_delete_line_entries_base(buffer, l1 + 1, l2);
                                } else {
       -                                /* FIXME: should this join the two lines? */
       +                                /* FIXME: this could be made nicer by just using the range to
       +                                   delete all in one go at the end */
       +                                rgl1 = l1;
       +                                rgb1 = pl1->start_index;
       +                                rgl2 = l2;
       +                                rgb2 = pl2->start_index + pl2->length;
       +                                if (text_ret) {
       +                                        ledit_copy_text_to_lbuf(
       +                                            buffer, text_ret,
       +                                            rgl1, rgb1,
       +                                            rgl2, rgb2
       +                                        );
       +                                }
                                        delete_line_section_base(buffer, l1, pl1->start_index, ll1->len - pl1->start_index);
       -                                delete_line_section_base(buffer, l2, 0, pl2->start_index + pl2->length);
       -                                if (l2 > l1 + 1)
       -                                        ledit_delete_line_entries_base(buffer, l1 + 1, l2 - 1);
       -                                *new_line_ret = l1 + 1;
       -                                ledit_x_softline_to_pos(ledit_get_line(buffer, l1 + 1), x, 0, new_byte_ret);
       +                                ledit_insert_text_from_line_base(
       +                                    buffer,
       +                                    l1, pl1->start_index,
       +                                    l2, pl2->start_index + pl2->length,
       +                                    ll2->len - (pl2->start_index + pl2->length), NULL
       +                                );
       +                                ledit_delete_line_entries_base(buffer, l1 + 1, l2);
       +                                new_line = l1;
       +                                int new_softlines = pango_layout_get_line_count(ll1->layout);
       +                                /* it's technically possible that the remaining part of the
       +                                   second line is so small that it doesn't generate a new
       +                                   softline, so there needs to be a special case - this is
       +                                   a bit weird because the cursor will seem to stay on the
       +                                   same line, but it now includes the rest of the second line
       +                                   (FIXME: this is probably not the best thing to do) */
       +                                ledit_x_softline_to_pos(
       +                                    ll1, x, sl1 + 1 < new_softlines ? sl1 + 1 : sl1, &new_byte
       +                                );
                                }
                        }
                } else {
                        if (line_index1 == line_index2) {
       -                        int b1, b2;
       +                        rgl1 = rgl2 = line_index1;
                                if (byte_index1 < byte_index2) {
       -                                b1 = byte_index1;
       -                                b2 = byte_index2;
       +                                rgb1 = byte_index1;
       +                                rgb2 = byte_index2;
                                } else {
       -                                b1 = byte_index2;
       -                                b2 = byte_index1;
       +                                rgb1 = byte_index2;
       +                                rgb2 = byte_index1;
       +                        }
       +                        if (text_ret) {
       +                                ledit_copy_text_to_lbuf(
       +                                    buffer, text_ret,
       +                                    rgl1, rgb1,
       +                                    rgl2, rgb2
       +                                );
                                }
       -                        delete_line_section_base(buffer, line_index1, b1, b2 - b1);
       -                        *new_line_ret = line_index1;
       -                        *new_byte_ret = b1;
       +                        delete_line_section_base(buffer, line_index1, rgb1, rgb2 - rgb1);
       +                        new_line = line_index1;
       +                        new_byte = rgb1;
                        } else {
       -                        int l1, l2, b1, b2;
                                if (line_index1 < line_index2) {
       -                                l1 = line_index1;
       -                                b1 = byte_index1;
       -                                l2 = line_index2;
       -                                b2 = byte_index2;
       +                                rgl1 = line_index1;
       +                                rgb1 = byte_index1;
       +                                rgl2 = line_index2;
       +                                rgb2 = byte_index2;
                                } else {
       -                                l1 = line_index2;
       -                                b1 = byte_index2;
       -                                l2 = line_index1;
       -                                b2 = byte_index1;
       +                                rgl1 = line_index2;
       +                                rgb1 = byte_index2;
       +                                rgl2 = line_index1;
       +                                rgb2 = byte_index1;
                                }
       -                        ledit_line *line1 = ledit_get_line(buffer, l1);
       -                        ledit_line *line2 = ledit_get_line(buffer, l2);
       -                        line1->len = b1;
       -                        if (b2 > 0) {
       -                                ledit_insert_text_base(
       -                                    buffer, l1, b1,
       -                                    line2->text + b2,
       -                                    line2->len - b2
       +                        if (text_ret) {
       +                                ledit_copy_text_to_lbuf(
       +                                    buffer, text_ret,
       +                                    rgl1, rgb1,
       +                                    rgl2, rgb2
                                        );
                                }
       -                        *new_line_ret = l1;
       -                        *new_byte_ret = b1;
       -                        ledit_delete_line_entries_base(buffer, l1 + 1, l2);
       +                        ledit_line *line1 = ledit_get_line(buffer, rgl1);
       +                        ledit_line *line2 = ledit_get_line(buffer, rgl2);
       +                        delete_line_section_base(buffer, rgl1, rgb1, line1->len - rgb1);
       +                        ledit_insert_text_from_line_base(
       +                            buffer, rgl1, rgb1, rgl2, rgb2, line2->len - rgb2, NULL
       +                        );
       +                        new_line = rgl1;
       +                        new_byte = rgb1;
       +                        ledit_delete_line_entries_base(buffer, rgl1 + 1, rgl2);
                        }
                        if (buffer->state->mode == NORMAL)
       -                        *new_byte_ret = ledit_get_legal_normal_pos(buffer, *new_line_ret, *new_byte_ret);
       +                        new_byte = ledit_get_legal_normal_pos(buffer, new_line, new_byte);
       +        }
       +        if (final_range_ret) {
       +                final_range_ret->line1 = rgl1;
       +                final_range_ret->byte1 = rgb1;
       +                final_range_ret->line2 = rgl2;
       +                final_range_ret->byte2 = rgb2;
                }
       +        if (new_line_ret)
       +                *new_line_ret = new_line;
       +        if (new_byte_ret)
       +                *new_byte_ret = new_byte;
        }
   DIR diff --git a/buffer.h b/buffer.h
       t@@ -3,7 +3,7 @@ typedef struct {
                int byte1;
                int line2;
                int byte2;
       -} ledit_selection;
       +} ledit_range;
        
        typedef struct ledit_buffer ledit_buffer;
        
       t@@ -39,7 +39,8 @@ struct ledit_buffer {
                long total_height; /* total pixel height of all lines */
                double display_offset; /* current pixel offset of viewport - this
                                        * is a double to make scrolling smoother */
       -        ledit_selection sel; /* current selection; all entries -1 if no selection */
       +        ledit_range sel; /* current selection; all entries -1 if no selection */
       +        ledit_undo_stack *undo;
        };
        
        ledit_buffer *ledit_create_buffer(ledit_common_state *state);
       t@@ -58,9 +59,9 @@ int ledit_next_utf8(ledit_line *line, int index);
        int ledit_prev_utf8(ledit_line *line, int index);
        size_t ledit_textlen(ledit_buffer *buffer, int line1, int byte1, int line2, int byte2);
        void ledit_copy_text(ledit_buffer *buffer, char *dst, int line1, int byte1, int line2, int byte2);
       -size_t ledit_copy_text_with_resize(
       +void ledit_copy_text_to_lbuf(
            ledit_buffer *buffer,
       -    char **dst, size_t *alloc,
       +    lbuf *buf, /* oh, isn't that a very non-confusing name? */
            int line1, int byte1,
            int line2, int byte2
        );
       t@@ -90,12 +91,14 @@ void ledit_delete_range_base(
            ledit_buffer *buffer, int line_based,
            int line_index1, int byte_index1,
            int line_index2, int byte_index2,
       -    int *new_line_ret, int *new_byte_ret
       +    int *new_line_ret, int *new_byte_ret,
       +    ledit_range *final_range_ret, lbuf *text_ret
        );
        void ledit_insert_text_from_line_base(
            ledit_buffer *buffer,
            int dst_line, int dst_index,
       -    int src_line, int src_index, int src_len
       +    int src_line, int src_index, int src_len,
       +    lbuf *text_ret
        );
        
        void ledit_insert_text(ledit_buffer *buffer, int line_index, int index, char *text, int len);
       t@@ -113,10 +116,12 @@ void ledit_delete_range(
            ledit_buffer *buffer, int line_based,
            int line_index1, int byte_index1,
            int line_index2, int byte_index2,
       -    int *new_line_ret, int *new_byte_ret
       +    int *new_line_ret, int *new_byte_ret,
       +    ledit_range *final_range_ret, lbuf *text_ret
        );
        void ledit_insert_text_from_line(
            ledit_buffer *buffer,
            int dst_line, int dst_index,
       -    int src_line, int src_index, int src_len
       +    int src_line, int src_index, int src_len,
       +    lbuf *text_ret
        );
   DIR diff --git a/cache.c b/cache.c
       t@@ -6,6 +6,7 @@
        
        #include "common.h"
        #include "memory.h"
       +#include "lbuf.h"
        #include "buffer.h"
        #include "cache.h"
        
   DIR diff --git a/common.h b/common.h
       t@@ -1,3 +1,5 @@
       +/* FIXME: it's ugly to put this here */
       +typedef struct ledit_undo_stack ledit_undo_stack;
        enum ledit_mode {
                NORMAL = 1,
                INSERT = 2,
   DIR diff --git a/lbuf.c b/lbuf.c
       t@@ -0,0 +1,51 @@
       +#include <stdlib.h>
       +#include <string.h>
       +
       +#include "memory.h"
       +#include "lbuf.h"
       +
       +lbuf *
       +lbuf_new(void) {
       +        lbuf *buf = ledit_malloc(sizeof(lbuf));
       +        buf->text = NULL;
       +        buf->cap = buf->len = 0;
       +        return buf;
       +}
       +
       +void
       +lbuf_grow(lbuf *buf, size_t sz) {
       +        /* always leave room for extra \0 */
       +        if (sz + 1 > buf->cap) {
       +                /* FIXME: what are the best values here? */
       +                buf->cap = buf->cap * 2 > sz + 1 ? buf->cap * 2 : sz + 1;
       +                buf->text = ledit_realloc(buf->text, buf->cap);
       +        }
       +}
       +
       +void
       +lbuf_shrink(lbuf *buf) {
       +        if ((buf->len + 1) * 4 < buf->cap) {
       +                buf->cap /= 2;
       +                buf->text = ledit_realloc(buf->text, buf->cap);
       +        }
       +}
       +
       +void
       +lbuf_destroy(lbuf *buf) {
       +        free(buf->text);
       +        free(buf);
       +}
       +
       +void
       +lbuf_cpy(lbuf *dst, lbuf *src) {
       +        lbuf_grow(dst, src->len);
       +        memcpy(dst->text, src->text, src->len);
       +        dst->len = src->len;
       +}
       +
       +lbuf *
       +lbuf_dup(lbuf *src) {
       +        lbuf *dst = lbuf_new();
       +        lbuf_cpy(dst, src);
       +        return dst;
       +}
   DIR diff --git a/lbuf.h b/lbuf.h
       t@@ -0,0 +1,12 @@
       +/* FIXME: RENAME THIS */
       +typedef struct {
       +        size_t len, cap;
       +        char *text;
       +} lbuf;
       +
       +lbuf *lbuf_new(void);
       +void lbuf_grow(lbuf *buf, size_t sz);
       +void lbuf_shrink(lbuf *buf);
       +void lbuf_destroy(lbuf *buf);
       +void lbuf_cpy(lbuf *dst, lbuf *src);
       +lbuf *lbuf_dup(lbuf *src);
   DIR diff --git a/ledit.c b/ledit.c
       t@@ -1,6 +1,6 @@
        /* FIXME: Only redraw part of screen if needed */
        /* FIXME: overflow in repeated commands */
       -/* FIXME: Fix lag when scrolling */
       +/* FIXME: Fix lag when scrolling - combine repeated mouse motion events */
        /* FIXME: Fix lag when selecting with mouse */
        /* FIXME: Use PANGO_PIXELS() */
        /* FIXME: Fix cursor movement, especially buffer->trailing and writing at end of line */
       t@@ -31,10 +31,12 @@
        
        #include "memory.h"
        #include "common.h"
       +#include "lbuf.h"
        #include "buffer.h"
        #include "search.h"
        #include "cache.h"
        #include "util.h"
       +#include "undo.h"
        
        enum key_type {
                KEY_NONE = 0,
       t@@ -87,6 +89,12 @@ struct key_stack_elem {
                int data2; /* misc. data 2 */
        };
        
       +/* buffer for storing yanked text */
       +lbuf *paste_buffer = NULL;
       +/* temporary buffer used for storing text
       +   in order to add it to the undo stack */
       +lbuf *tmp_buffer = NULL;
       +
        static struct {
                size_t len, alloc;
                struct key_stack_elem *stack;
       t@@ -216,8 +224,7 @@ show_message(char *text, int len) {
        
        struct {
                Atom xtarget;
       -        char *primary;
       -        size_t primary_alloc;
       +        lbuf *primary;
                char *clipboard;
        } xsel;
        
       t@@ -243,6 +250,7 @@ set_mode(enum ledit_mode mode) {
                XftDrawRect(bottom_bar.mode_draw->xftdraw, &state.bg, 0, 0, bottom_bar.mode_w, bottom_bar.mode_h);
                pango_xft_render_layout(bottom_bar.mode_draw->xftdraw, &state.fg, bottom_bar.mode, 0, 0);
                recalc_text_size();
       +        ledit_change_mode_group(buffer);
        }
        
        void
       t@@ -253,8 +261,9 @@ clipcopy(void)
                free(xsel.clipboard);
                xsel.clipboard = NULL;
        
       -        if (xsel.primary != NULL) {
       -                xsel.clipboard = ledit_strdup(xsel.primary);
       +        /* FIXME: don't copy if text empty (no selection)? */
       +        if (xsel.primary->text != NULL) {
       +                xsel.clipboard = ledit_strdup(xsel.primary->text);
                        clipboard = XInternAtom(state.dpy, "CLIPBOARD", 0);
                        XSetSelectionOwner(state.dpy, clipboard, state.win, CurrentTime);
                }
       t@@ -402,7 +411,7 @@ selrequest(XEvent *e)
                         */
                        clipboard = XInternAtom(state.dpy, "CLIPBOARD", 0);
                        if (xsre->selection == XA_PRIMARY) {
       -                        seltext = xsel.primary;
       +                        seltext = xsel.primary->text;
                        } else if (xsre->selection == clipboard) {
                                seltext = xsel.clipboard;
                        } else {
       t@@ -482,15 +491,63 @@ get_new_line_softline(
                }
        }
        
       +/* FIXME: don't overwrite buffer->cur_line, etc. here? */
       +static void
       +delete_range(
       +    int line_based, int selected,
       +    int line_index1, int byte_index1,
       +    int line_index2, int byte_index2) {
       +        (void)selected; /* FIXME */
       +        ledit_range cur_range, del_range;
       +        cur_range.line1 = buffer->cur_line;
       +        cur_range.byte1 = buffer->cur_index;
       +        ledit_delete_range(
       +            buffer, line_based,
       +            line_index1, byte_index1,
       +            line_index2, byte_index2,
       +            &buffer->cur_line, &buffer->cur_index,
       +            &del_range, paste_buffer
       +        );
       +        cur_range.line2 = buffer->cur_line;
       +        cur_range.byte2 = buffer->cur_index;
       +        ledit_push_undo_delete(
       +            buffer, paste_buffer, del_range, cur_range, 1
       +        );
       +}
       +
       +static void
       +insert_text(int line, int index, char *text, int len, int start_group) {
       +        if (len < 0)
       +                len = strlen(text);
       +        /* FIXME: this is kind of hacky... */
       +        lbuf ins_buf = {.text = text, .len = len, .cap = len};
       +        ledit_range cur_range, del_range;
       +        cur_range.line1 = buffer->cur_line;
       +        cur_range.byte1 = buffer->cur_index;
       +        del_range.line1 = line;
       +        del_range.byte1 = index;
       +        ledit_insert_text_with_newlines(
       +            buffer, line, index, text, len,
       +            &buffer->cur_line, &buffer->cur_index
       +        );
       +        cur_range.line2 = buffer->cur_line;
       +        cur_range.byte2 = buffer->cur_index;
       +        del_range.line2 = buffer->cur_line;
       +        del_range.byte2 = buffer->cur_index;
       +        ledit_push_undo_insert(
       +            buffer, &ins_buf, del_range, cur_range, start_group
       +        );
       +}
       +
        static int
        delete_selection(void) {
                if (buffer->sel.line1 != buffer->sel.line2 || buffer->sel.byte1 != buffer->sel.byte2) {
       -                ledit_delete_range(
       -                    buffer, 0,
       +                delete_range(
       +                    0, 0,
                            buffer->sel.line1, buffer->sel.byte1,
       -                    buffer->sel.line2, buffer->sel.byte2,
       -                    &buffer->cur_line, &buffer->cur_index
       +                    buffer->sel.line2, buffer->sel.byte2
                        );
       +                /* FIXME: maybe just set this to the current cursor pos? */
                        buffer->sel.line1 = buffer->sel.line2 = -1;
                        buffer->sel.byte1 = buffer->sel.byte2 = -1;
                        ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
       t@@ -545,11 +602,10 @@ key_d(void) {
        static void
        key_d_cb(int line, int char_pos, enum key_type type) {
                int line_based = type == KEY_MOTION_LINE ? 1 : 0;
       -        ledit_delete_range(
       -            buffer, line_based,
       +        delete_range(
       +            line_based, 0,
                    buffer->cur_line, buffer->cur_index,
       -            line, char_pos,
       -            &buffer->cur_line, &buffer->cur_index
       +            line, char_pos
                );
                ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
        }
       t@@ -565,6 +621,7 @@ key_x(void) {
                        num = e->count;
                if (num <= 0)
                        num = 1;
       +        /* FIXME: actually do something */
        }
        
        static void
       t@@ -575,7 +632,7 @@ push_num(int num) {
                        e->key = KEY_NUMBER;
                        e->followup = KEY_NUMBER|KEY_NUMBERALLOWED;
                }
       -        /* FIXME: error checking */
       +        /* FIXME: error (overflow) checking */
                e->count *= 10;
                e->count += num;
        }
       t@@ -662,6 +719,7 @@ push_key_stack(void) {
        
        /* Note: for peek and pop, the returned element is only valid
         * until the next element is pushed */
       +/* Note on the note: that's not entirely true for peek */
        static struct key_stack_elem *
        peek_key_stack(void) {
                if (key_stack.len > 0)
       t@@ -947,7 +1005,6 @@ setup(int argc, char *argv[]) {
                pango_layout_set_font_description(bottom_bar.mode, state.font);
                /* FIXME: only create "dummy draw" at first and create with proper size when needed */
                bottom_bar.mode_draw = ledit_create_draw(&state, 10, 10);
       -        set_mode(INSERT);
                bottom_bar.line = pango_layout_new(state.context);
                pango_layout_set_font_description(bottom_bar.line, state.font);
                bottom_bar.line_draw = ledit_create_draw(&state, 10, 10);
       t@@ -962,12 +1019,17 @@ setup(int argc, char *argv[]) {
        
                ledit_init_cache(&state);
                buffer = ledit_create_buffer(&state);
       +        /* FIXME: move this to create_buffer */
       +        ledit_init_undo_stack(buffer);
       +        set_mode(INSERT);
        
                key_stack.len = key_stack.alloc = 0;
                key_stack.stack = NULL;
        
       -        xsel.primary = NULL;
       -        xsel.primary_alloc = 0;
       +        paste_buffer = lbuf_new();
       +        tmp_buffer = lbuf_new();
       +
       +        xsel.primary = lbuf_new();
                xsel.clipboard = NULL;
                xsel.xtarget = XInternAtom(state.dpy, "UTF8_STRING", 0);
                if (xsel.xtarget == None)
       t@@ -982,6 +1044,7 @@ cleanup(void) {
                /* FIXME: cleanup everything else */
                ledit_cleanup_search();
                ledit_destroy_cache();
       +        ledit_destroy_undo_stack(buffer);
                ledit_destroy_buffer(buffer);
                XDestroyWindow(state.dpy, state.win);
                XCloseDisplay(state.dpy);
       t@@ -1153,10 +1216,7 @@ sort_selection(int *line1, int *byte1, int *line2, int *byte2) {
        /* lines and bytes need to be sorted already! */
        static void
        copy_selection_to_x_primary(int line1, int byte1, int line2, int byte2) {
       -        (void)ledit_copy_text_with_resize(
       -            buffer, &xsel.primary, &xsel.primary_alloc,
       -            line1, byte1, line2, byte2
       -        );
       +        ledit_copy_text_to_lbuf(buffer, xsel.primary, line1, byte1, line2, byte2);
                XSetSelectionOwner(state.dpy, XA_PRIMARY, state.win, CurrentTime);
                /*
                FIXME
       t@@ -1343,19 +1403,27 @@ backspace(void) {
                } else if (buffer->cur_index == 0) {
                        if (buffer->cur_line != 0) {
                                ledit_line *l1 = ledit_get_line(buffer, buffer->cur_line - 1);
       +                        delete_range(0, 0, buffer->cur_line - 1, l1->len, buffer->cur_line, 0);
       +                        /*
                                int old_len = l1->len;
                                ledit_insert_text_from_line(
                                    buffer, buffer->cur_line - 1, l1->len,
       -                            buffer->cur_line, 0, -1
       +                            buffer->cur_line, 0, l2->len, tmp_buffer
                                );
                                ledit_delete_line_entry(buffer, buffer->cur_line);
                                buffer->cur_line--;
                                buffer->cur_index = old_len;
       +                        */
                        }
                } else {
       +                ledit_line *l = ledit_get_line(buffer, buffer->cur_line);
       +                int i = ledit_prev_utf8(l, buffer->cur_index);
       +                delete_range(0, 0, buffer->cur_line, buffer->cur_index, buffer->cur_line, i);
       +                /*
                        buffer->cur_index = ledit_delete_unicode_char(
                            buffer, buffer->cur_line, buffer->cur_index, -1
                        );
       +                */
                }
                ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
        }
       t@@ -1367,19 +1435,24 @@ delete_key(void) {
                        /* NOP */
                } else if (buffer->cur_index == cur_line->len) {
                        if (buffer->cur_line != buffer->lines_num - 1) {
       +                        delete_range(0, 0, buffer->cur_line, cur_line->len, buffer->cur_line + 1, 0);
       +                        /*
                                int old_len = cur_line->len;
       -                        /* FIXME: THIS CURRENTLY DOESN'T RECALC LINE SIZE! */
                                ledit_insert_text_from_line(
                                    buffer, buffer->cur_line, cur_line->len,
                                    buffer->cur_line + 1, 0, -1
                                );
                                ledit_delete_line_entry(buffer, buffer->cur_line + 1);
       -                        buffer->cur_index = old_len;
       +                        */
                        }
                } else {
       +                int i = ledit_next_utf8(cur_line, buffer->cur_index);
       +                delete_range(0, 0, buffer->cur_line, buffer->cur_index, buffer->cur_line, i);
       +                /*
                        buffer->cur_index = ledit_delete_unicode_char(
                            buffer, buffer->cur_line, buffer->cur_index, 1
                        );
       +                */
                }
                ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
        }
       t@@ -1466,14 +1539,17 @@ cursor_right(void) {
        
        static void
        return_key(void) {
       -        delete_selection();
       -        ledit_append_line(buffer, buffer->cur_line, buffer->cur_index);
       +        int start_group = 1;
       +        if (delete_selection())
       +                start_group = 0;
       +        insert_text(buffer->cur_line, buffer->cur_index, "\n", -1, start_group);
       +        /* ledit_append_line(buffer, buffer->cur_line, buffer->cur_index); */
                /* FIXME: these aren't needed, right? This only works in insert mode
                 * anyways, so there's nothing to wipe */
       -        ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
       +        /* ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
                buffer->cur_line++;
                ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
       -        buffer->cur_index = 0;
       +        buffer->cur_index = 0; */
        }
        
        static void
       t@@ -1690,6 +1766,23 @@ show_line(void) {
                show_message(str, len);
        }
        
       +/* FIXME: return status! */
       +static void
       +undo(void) {
       +        set_selection(0, 0, 0, 0);
       +        ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
       +        ledit_undo(buffer);
       +        ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
       +}
       +
       +static void
       +redo(void) {
       +        set_selection(0, 0, 0, 0);
       +        ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line);
       +        ledit_redo(buffer);
       +        ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index);
       +}
       +
        /* FIXME: maybe sort these and use binary search */
        static struct key keys_en[] = {
                {NULL, 0, XK_BackSpace, INSERT, KEY_ANY, KEY_ANY, &backspace},
       t@@ -1728,7 +1821,11 @@ static struct key keys_en[] = {
                {"/",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &enter_searchedit_forward},
                {NULL, 0, XK_Return, COMMANDEDIT|SEARCHEDIT, KEY_ANY, KEY_ANY, &end_lineedit},
                {"n",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &search_next},
       -        {"N",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &search_prev}
       +        {"N",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &search_prev},
       +        {"u",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &undo},
       +        {"U",  0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &redo},
       +        {"z",  ControlMask, 0, INSERT, KEY_ANY, KEY_ANY, &undo},
       +        {"y",  ControlMask, 0, INSERT, KEY_ANY, KEY_ANY, &redo}
        };
        
        static struct key keys_ur[] = {
       t@@ -1859,12 +1956,15 @@ key_press(XEvent event) {
                                state.message_shown--;
                } else if (state.mode == INSERT && !found && n > 0) {
                        delete_selection();
       +                insert_text(buffer->cur_line, buffer->cur_index, buf, n, 1);
       +                /*
                        ledit_insert_text_with_newlines(
                            buffer,
                            buffer->cur_line, buffer->cur_index,
                            buf, n,
                            &buffer->cur_line, &buffer->cur_index
                        );
       +                */
                        ensure_cursor_shown();
                        if (state.message_shown > 0)
                                state.message_shown--;
   DIR diff --git a/search.c b/search.c
       t@@ -6,6 +6,7 @@
        
        #include "memory.h"
        #include "common.h"
       +#include "lbuf.h"
        #include "buffer.h"
        #include "search.h"
        
   DIR diff --git a/undo.c b/undo.c
       t@@ -0,0 +1,250 @@
       +#include <string.h>
       +#include <assert.h>
       +#include <stdlib.h>
       +
       +#include <X11/Xlib.h>
       +#include <X11/Xutil.h>
       +#include <pango/pangoxft.h>
       +/* FIXME: move some parts of common to ledit.c so
       +   this include isn't needed */
       +#include <X11/extensions/Xdbe.h>
       +
       +#include "memory.h"
       +#include "common.h"
       +#include "lbuf.h"
       +#include "buffer.h"
       +#include "cache.h"
       +#include "undo.h"
       +
       +enum operation {
       +        UNDO_INSERT,
       +        UNDO_DELETE
       +};
       +
       +typedef struct {
       +        lbuf *text;
       +        enum operation type;
       +        enum ledit_mode mode;
       +        ledit_range op_range;
       +        ledit_range cursor_range;
       +        int group;
       +        int mode_group;
       +} undo_elem;
       +
       +struct ledit_undo_stack {
       +        /* FIXME: size_t? */
       +        int len, cur, cap;
       +        undo_elem *stack;
       +        int change_mode_group;
       +};
       +
       +/* FIXME: maybe make these work directly on the stack instead of buffer */
       +void
       +ledit_init_undo_stack(ledit_buffer *buffer) {
       +        buffer->undo = ledit_malloc(sizeof(ledit_undo_stack));
       +        buffer->undo->len = buffer->undo->cap = 0;
       +        buffer->undo->cur = -1;
       +        buffer->undo->stack = NULL;
       +        buffer->undo->change_mode_group = 0;
       +}
       +
       +void
       +ledit_destroy_undo_stack(ledit_buffer *buffer) {
       +        free(buffer->undo->stack);
       +        free(buffer->undo);
       +}
       +
       +/* FIXME: resize text buffers when they aren't needed anymore */
       +static undo_elem *
       +push_undo_elem(ledit_buffer *buffer) {
       +        ledit_undo_stack *s = buffer->undo;
       +        assert(s->cur >= -1);
       +        s->cur++;
       +        s->len = s->cur + 1;
       +        if (s->len > s->cap) {
       +                size_t cap = s->len * 2;
       +                s->stack = ledit_realloc(s->stack, cap * sizeof(undo_elem));
       +                for (size_t i = s->cap; i < cap; i++) {
       +                        s->stack[i].text = NULL;
       +                }
       +                s->cap = cap;
       +        }
       +        return &s->stack[s->cur];
       +}
       +
       +static undo_elem *
       +peek_undo_elem(ledit_buffer *buffer) {
       +        ledit_undo_stack *s = buffer->undo;
       +        if (s->cur < 0)
       +                return NULL;
       +        return &s->stack[s->cur];
       +}
       +
       +void
       +ledit_change_mode_group(ledit_buffer *buffer) {
       +        buffer->undo->change_mode_group = 1;
       +}
       +
       +/* FIXME: The current cursor position could be taken directly from the
       +   buffer, but maybe it's better this way to make it a bit more explicit? */
       +static void
       +push_undo(
       +    ledit_buffer *buffer, lbuf *text,
       +    ledit_range insert_range,
       +    ledit_range cursor_range,
       +    int start_group, enum operation type) {
       +        undo_elem *old = peek_undo_elem(buffer);
       +        int last_group = old == NULL ? 0 : old->group;
       +        int last_mode_group = old == NULL ? 0 : old->mode_group;
       +        undo_elem *e = push_undo_elem(buffer);
       +        e->group = start_group ? !last_group : last_group;
       +        e->mode_group = buffer->undo->change_mode_group ? !last_mode_group : last_mode_group;
       +        buffer->undo->change_mode_group = 0;
       +        e->op_range = insert_range;
       +        e->cursor_range = cursor_range;
       +        e->mode = buffer->state->mode;
       +        e->type = type;
       +        if (e->text != NULL)
       +                lbuf_cpy(e->text, text);
       +        else
       +                e->text = lbuf_dup(text);
       +}
       +
       +void
       +ledit_push_undo_insert(
       +    ledit_buffer *buffer, lbuf *text,
       +    ledit_range insert_range,
       +    ledit_range cursor_range,
       +    int start_group) {
       +        push_undo(
       +            buffer, text, insert_range,
       +            cursor_range, start_group, UNDO_INSERT
       +        );
       +}
       +
       +void
       +ledit_push_undo_delete(
       +    ledit_buffer *buffer, lbuf *text,
       +    ledit_range insert_range,
       +    ledit_range cursor_range,
       +    int start_group) {
       +        push_undo(
       +            buffer, text, insert_range,
       +            cursor_range, start_group, UNDO_DELETE
       +        );
       +}
       +
       +ledit_undo_status
       +ledit_undo(ledit_buffer *buffer) {
       +        undo_elem *e;
       +        ledit_undo_stack *s = buffer->undo;
       +        if (s->cur < 0)
       +                return UNDO_OLDEST_CHANGE;
       +        int group = s->stack[s->cur].group;
       +        int mode_group = s->stack[s->cur].mode_group;
       +        int min_line = buffer->lines_num - 1;
       +        int mode_group_same = 0;
       +        while (s->cur >= 0 &&
       +               (s->stack[s->cur].group == group || (mode_group_same =
       +                ((buffer->state->mode == NORMAL ||
       +                  buffer->state->mode == VISUAL) &&
       +                 s->stack[s->cur].mode == INSERT &&
       +                 s->stack[s->cur].mode_group == mode_group)))) {
       +                e = &s->stack[s->cur];
       +                /* if the mode group is the same, we need to update the group,
       +                   otherwise it can happen that some iterations are performed
       +                   because the mode group (but not the normal group) is the
       +                   same, and then the next normal group is also undone because
       +                   it has the same group id as the original group here */
       +                if (mode_group_same)
       +                        group = e->group;
       +                switch (e->type) {
       +                case UNDO_INSERT:
       +                        /* FIXME: should the paste buffer also be modified? */
       +                        ledit_delete_range_base(
       +                            buffer, 0,
       +                            e->op_range.line1, e->op_range.byte1,
       +                            e->op_range.line2, e->op_range.byte2,
       +                            NULL, NULL, NULL, NULL
       +                        );
       +                        break;
       +                case UNDO_DELETE:
       +                        ledit_insert_text_with_newlines_base(
       +                            buffer, e->op_range.line1, e->op_range.byte1,
       +                            e->text->text, e->text->len, NULL, NULL
       +                        );
       +                        break;
       +                default:
       +                        fprintf(stderr, "Error with undo. This should not happen. Fix the code please.\n");
       +                        break;
       +                }
       +                /* FIXME: make sure this is always sorted already */
       +                if (e->op_range.line1 < min_line)
       +                        min_line = e->op_range.line1;
       +                s->cur--;
       +                buffer->cur_line = e->cursor_range.line1;
       +                buffer->cur_index = e->cursor_range.byte1;
       +        }
       +        if (buffer->state->mode == NORMAL) {
       +                buffer->cur_index = ledit_get_legal_normal_pos(
       +                    buffer, buffer->cur_line, buffer->cur_index
       +                );
       +        }
       +        ledit_recalc_from_line(buffer, min_line > 0 ? min_line - 1 : min_line);
       +        return UNDO_NORMAL;
       +}
       +
       +ledit_undo_status
       +ledit_redo(ledit_buffer *buffer) {
       +        undo_elem *e;
       +        ledit_undo_stack *s = buffer->undo;
       +        if (s->cur >= s->len - 1)
       +                return UNDO_NEWEST_CHANGE;
       +        s->cur++;
       +        int group = s->stack[s->cur].group;
       +        int mode_group = s->stack[s->cur].mode_group;
       +        int min_line = buffer->lines_num - 1;
       +        int mode_group_same = 0;
       +        while (s->cur < s->len &&
       +               (s->stack[s->cur].group == group || (mode_group_same =
       +                ((buffer->state->mode == NORMAL ||
       +                  buffer->state->mode == VISUAL) &&
       +                 s->stack[s->cur].mode == INSERT &&
       +                 s->stack[s->cur].mode_group == mode_group)))) {
       +                e = &s->stack[s->cur];
       +                if (mode_group_same)
       +                        group = e->group;
       +                switch (e->type) {
       +                case UNDO_INSERT:
       +                        ledit_insert_text_with_newlines_base(
       +                            buffer, e->op_range.line1, e->op_range.byte1,
       +                            e->text->text, e->text->len, NULL, NULL
       +                        );
       +                        break;
       +                case UNDO_DELETE:
       +                        ledit_delete_range_base(
       +                            buffer, 0,
       +                            e->op_range.line1, e->op_range.byte1,
       +                            e->op_range.line2, e->op_range.byte2,
       +                            NULL, NULL, NULL, NULL
       +                        );
       +                        break;
       +                default:
       +                        fprintf(stderr, "Error with redo. This should not happen. Fix the code please.\n");
       +                        break;
       +                }
       +                if (e->op_range.line1 < min_line)
       +                        min_line = e->op_range.line1;
       +                s->cur++;
       +                buffer->cur_line = e->cursor_range.line2;
       +                buffer->cur_index = e->cursor_range.byte2;
       +        }
       +        s->cur--;
       +        if (buffer->state->mode == NORMAL) {
       +                buffer->cur_index = ledit_get_legal_normal_pos(
       +                    buffer, buffer->cur_line, buffer->cur_index
       +                );
       +        }
       +        ledit_recalc_from_line(buffer, min_line > 0 ? min_line - 1 : min_line);
       +        return UNDO_NORMAL;
       +}
   DIR diff --git a/undo.h b/undo.h
       t@@ -0,0 +1,23 @@
       +typedef enum {
       +        UNDO_NORMAL,
       +        UNDO_OLDEST_CHANGE,
       +        UNDO_NEWEST_CHANGE
       +} ledit_undo_status;
       +
       +void ledit_init_undo_stack(ledit_buffer *buffer);
       +void ledit_destroy_undo_stack(ledit_buffer *buffer);
       +ledit_undo_status ledit_undo(ledit_buffer *buffer);
       +ledit_undo_status ledit_redo(ledit_buffer *buffer);
       +void ledit_push_undo_insert(
       +    ledit_buffer *buffer, lbuf *text,
       +    ledit_range insert_range,
       +    ledit_range cursor_range,
       +    int start_group
       +);
       +void ledit_push_undo_delete(
       +    ledit_buffer *buffer, lbuf *text,
       +    ledit_range insert_range,
       +    ledit_range cursor_range,
       +    int start_group
       +);
       +void ledit_change_mode_group(ledit_buffer *buffer);